четверг, 19 апреля 2012 г.

О повторном использовании кода

Сегодня существуют разные мнения по поводу успешности объектной технологии. С одной стороны, большинство современных mainstream языков программирования являются объектно-ориентированными, с другой стороны, нередко можно услышать критику ООП, дескать, объектно-ориентированное программирование «провалилось» и не оправдало тех надежд, которые были возложены на нее индустрией разработки ПО. Все, мол, ожидали наступления вселенского счастья в виде увеличения повторного использования, упрощения сопровождения, да и вообще, обещали, что думать придется кому-то другому, а я за это буду деньги получать.

Причин у подобных разочарований тоже несколько. Во-первых, такое отношение к ООП может быть следствием завышенных ожиданий, а ведь Фред Брукс еще двадцать лет назад писал о том, что не стоит ждать «серебряных пуль», способных на порядок увеличить продуктивность труда программиста. Во-вторых, никто из серьезных сторонников ООП (типа Гради Буча или Бертрана Мейера) не обещали, что все будет просто. ООП – это не волшебная палочка, которая сделает из любого УГ конфетку, даже обертку которой можно будет использовать повторно.

Так вот вопрос: как же можно достичь той заветной мечты, когда системы можно будет строить из готовых компонентов, не написав при этом ни строчки кода? Я не уверен, что в таком виде эта мечта вообще осуществима из-за неотъемлемой сложности ПО, а также потому, что часто само решение влияет на решаемую задачу. Тем не менее, если направить энергию, затрачиваемую на повторное использование в правильное русло, то с разумным количеством трудозатрат можно поднять «реюз» кода на достойный уровень.

Как мне кажется, причин у столь плачевного состояния дел с повторным использованием две: (1) разработчики уделяют повторному использованию слишком мало внимания и (2) разработчики уделяют повторному использованию слишком много внимания. Да, это звучит, по меньшей мере, глупо, но давайте я немного перефразирую: проблема с повторным использованием в том, что усилия прикладываются не вовремя и не всегда туда, куда нужно.

Стандартный жизненный цикл ПО

Для начала, давайте рассмотрим типичный жизненный цикл одной итерации разработки ПО:

image

Классический жизненный цикл разработки ПО является итеративным, при этом каждая итерация состоит из (более или менее) одних и тех же этапов. В некоторых случаях этапы проектирования и реализации могут объединяться в один, а перед фазой анализа иногда может быть анализ осуществимости, но суть от этого не меняется. Теперь давайте попробуем ответить на вопрос: на каком из этих этапов принимается решение о том, что определенные классы (или модули целиком) достойны повторного использования? Обычно это решение принимается на лету где-то между анализом и проектирование, иногда оно принимается на этапе проектирования, иногда – во время реализации.

В целом, ничего плохого в этом нет, пока процесс обобщения не начинает влиять на дизайн системы, затягивать сроки и приводить к увеличению сложности, вместо того, чтобы с ней бороться.

Преждевременное обобщение

Каждый уважающий себя разработчик знает цитату Кнута о злобной преждевременной оптимизации, которая может привести к невероятно быстрому коду (правда, обычно не в тех местах, что нужно), прочитать и понять который со временем не сможет и сам автор. Но поскольку сейчас наиболее затратной частью многих систем является не производительность, а эффективность разработки, то вместо преждевременной оптимизации все чаще можно столкнуться с проблемой «преждевременного обобщения» (premature generalization).

По сути, преждевременное обобщение сводится к созданию более сложного решения в надежде на повторное использование или на «гибкость», которая позволит справиться с будущими изменениями. Но, как это обычно бывает, «гибкость» оказывается не такой и гибкой, требования начинают меняться не в ту сторону, в результате трудозатраты не оправдываются и команда получает более сложное решение, хотя могла бы обойтись и более простым. Иногда бывают и более паталогические случаи: например, с самого начала за дело может взяться «архитектор» с непреклонным желанием построить свой «фреймворк» с блэкджеком и девицами когда еще нет четкого видения того, что нужно команде, проекту и заказчику. В результате, каждый проект обрастает кривыми библиотеками и фреймворками, использовать которые неудобно, а сопровождать – дорого.

Проблема в том, что затраты на дизайн, реализацию и сопровождения публично доступного кода на несколько порядков выше стоимости сопровождения простого кастомного решения. Начинается все с того, что реюзабельный код требует более полной и подробной документации, лучшего качества и покрытия тестами, примеров использования и пользовательской документации. Даже если код предназначен для повторного использования внутри команды, то его качество и простота использования должны быть такими, чтобы программисту было экономически выгодно разобраться в вашем решении, а не городить свой собственный любимый огород. И я уже не говорю о необходимости культуры повторного использования, без которой вся идея «реюза» накроется медным тазом благодаря всеми любимому симптому NIH (Not Invented Here).

Когда же речь заходит не о библиотеках, а о «гибкости» конкретных решений, то мы сталкиваемся с похожей проблемой. Многие считают, что «обобщенное» решение (с кучей возможностей для расширения) является лучшим способом справиться с будущими изменениями. Возможно это и так, но практика показывает, что с этого пути очень легко скатиться к громоздкому коду, который сложно понять (ведь он может все) и сопровождать. Если вы считаете, что некоторый аспект поведения может измениться, то достаточно спрятать его за абстрактным интерфейсом и сделать этот аспект деталью реализации. Как и в случае с производительностью, программисты (и архитекторы) плохо угадывают, что именно будет изменяться в будущем и какие новые потребности появятся. А раз так, то нет ничего лучше простого решения, которое можно легко адаптировать под изменяющиеся требования путем его модификации.

Все эти сложности не говорят о том, что заниматься повторным использованием глупо. Нет. Аналогично тому, как оптимизация производительности должна осуществляться после профилирования, так и обобщение должно осуществляться вОвремя: не во время разработки самой «фичи», а на более поздних этапах, когда команда понимает, куда будут направлены возможные изменения и, что нужно обобщать, а что – нет.

Модифицированный жизненный цикл итерации

image

Здесь нужно понять очень важный момент: шаг обобщения не является сам по себе достаточным условием для создания реюзабельного кода, точно также как процесс профилирования не гарантирует того, что вы сможете повысить производительность найденного bottle neck-а. Это лишь этап, во время которого простое и адекватное решение, полученное на этапе проектирования и реализации «причесывается» дополнительно для доведения его до уровня повторного использования: убираются лишние зависимости, улучшается документация, добавляются дополнительные юнит-тесты. Если во время реализации все скомкать в одну кучу и сделать из системы такой клубок, что без бутыля не разобраться, то не о каком будущем «реюзе» речи быть не может. Но если начать с простой и понятной архитектуры, которая хорошо решает поставленную задачу, то объединить несколько однотипных решений в одно будет значительно легче, когда будет уже точно понятно, что у них общего.

ПРИМЕЧАНИЕ
В данном случае я не рассматриваю разработку библиотек, как отдельного продукта. Библиотека изначально заточена на повторное использование и там многое делается по-другому; достаточно полистать Framework Design Guidelines, чтобы понять – при разработке библиотеки на первый план выходит ее качество, удобство использования, отсутствие ломающих изменений, интуитивная понятность, «иерархичность» и много другое. Здесь же речь идет не о библиотеках для широкой общественности, а о проблемах с «реюзом» в типичных enterprise приложениях.

Этап обобщения не обязательно должен быть формальным или проходить на каждой итерации разработки, но рано или поздно команде понадобится приложить дополнительные усилия для доведения кода (и дизайна) до ума, а значит, о нем должны знать руководители проекта.

Заключение

К сожалению, даже этап обобщения не является залогом повышения «реюза» кода. Первая проблема связана с выбиванием у руководства дополнительного времени на непонятное обобщение, вместо того, чтобы клепать новую функциональность следующей итерации. Да, все твердят о важности повторного использования, но когда заходит речь о дополнительных расходах, то близорукость руководителей проекта может сыграть свою роль и в обобщении будет отказано. В этом есть и доля истины; вторая проблема связана с тем, что наличие качественного кода, доступного для повторного использования, не гарантирует его использования. Для нормального «реюза» нужна еще и культура, а всеми любимый NIH и жажда велосипедостроения могут быть настолько сильными, что любые попытки повторного использования ни к чему не приведут.

Но, в любом случае, мой совет сводится к тому, чтобы при выборе простота vs «гибкость» к будущим изменениям, вы останавливались именно на простоте. Ведь если решение простое, то его несложно модифицировать под будущие потребности, или обобщить для повторного использования. Ну а если ничего такого не понадобится, то вы просто сэкономите силы и время, и будете жить с этим простым решением долгие годы.

ПРИМЕЧАНИЕ
Идею с этапом обобщения предложил Бертран Мейер в свое книге «Объектно-ориентированное конструирование программных систем», так что немного подробностей можно найти в разделе "28.5 Обобщение" (кстати, это и правда отличная книга, в которой можно найти много интересных идей об ООП и разработке ПО).

11 комментариев:

  1. "...которая сделает из любого УГ конфетку, даже обложку которой можно будет использовать повторно."

    Возможно тут имелось ввиду "обертку"?

    ОтветитьУдалить
  2. Все же есть грань, за которой "потом" обобщать будет очень сложно.
    Легко сказать "уменьшаем зависимости", а на деле - переписываем почти всё, а если вообще не было выделено сущностей (не было в них необходимости) и логика размазана? Так что основа повторного использования кода закладывается сразу (вопрос только в какой степени), ИМХО...

    ОтветитьУдалить
  3. Сейчас открыл наугад несколько файлов из текущего проекта (С++, .NET) и явно большая часть кода это обращение к внешним/собственным библиотекам/классам. На самом деле очень похоже на ОО конструктор.

    Плюс конечно центральная, самая интересная часть программы и должна быть уникальна. Иначе для чего ее создавать когда можно использовать существующие решения :)

    ОтветитьУдалить
  4. @torabravo: точно, спасибо.

    @Alexander: Да я, вроде бы, об этом же и пишу:)

    ОтветитьУдалить
  5. Хм, Сереж, все-таки совсем просто - это как-то не кошерно. Эйнштейн - "Сделай настолько просто, насколько это возможно, но не проще". Это я к тому, что должен быть компромисс между простотой и правильностью. У нас в проекте простота - это копипаст. Работает - сто пудово работает - в другом месте же работало. А если сделать по уму - надо выделять сущности, обобщать, а оно надо???? Поэтому я бы так сказал, сделать как можно проще, не нарушая заповедей :). Шутка конечно, скажем так сделать как можно проще не выходя за рамки здравого смысла.

    ОтветитьУдалить
  6. Женя, там же мыслей две: делай сразу хорошо, но не идеально, а потом дотюнь до реюза уже только то, что нужно.

    ОтветитьУдалить
  7. Я бы еще хотел добавить, что любой программный проект эволюционирует и создатель этого проекта должен в меру свой разумности выделять общие блоки, которые будут переиспользованы. Обычно бывает так: приходят новые требования, их реализуют в текущем проекте, не всегда правильно/красиво и т.д., но сразу. После реализации становится понятно, где необходимо что-то изменить, чтобы улучшить внутреннее качество продукта. Я видел всего лишь один проект, где был такой процесс.
    Именно из-за такого эволюционного процесса берутся те 9 месяцев старика Брукса. Извне жизнь проекту дают требования клиентов и бизнеса, изнутри понятная, простая, однозначная структура проекта.
    Прям таки Инь и Янь :)

    ОтветитьУдалить
  8. Имхо, как правило, не раньше, чем после нескольких месяцев (иногда лет) ЭКСПЛУАТАЦИИ команде становится понятно, как надо было изначально проектировать (и даже обследовать) систему )))

    ОтветитьУдалить
    Ответы
    1. Это, ИМХО, зависит от размеров системы. В большинстве сложных систем количество осей столь большое, что не вполне ясно, как нужно было повернуть, чтобы получить максимальную выгоду:)

      Удалить
  9. Мечта "настоящего программиста" - создать суперсистему, чтобы на ее основе другие программисты могли создавать свои суперсистемы (с не помню откуда). Отсюда и стремление к преждевременной универсализации, которое погубило не один проект.

    ОтветитьУдалить
    Ответы
    1. А мне чего-то кажется, что желание создавать всякие фреймворки - это некая форма прокрастинации: вместо решения проблем, мы делаем универсальный инструмент решания проблем. При этом, если мы его не сделаем, то ничего страшного не будет:)

      Удалить