суббота, 27 апреля 2013 г.

Дизайн и борьба со сложностью

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

Интересно, что подходы борьбы со сложностью не особенно зависят от парадигмы программирования (или парадигмы проектирования, если хотите). Так, например, об этом писал Гради Буч в контексте объектно-ориентированного дизайна:

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

А затем:

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

UPDATE: здесь под “иерархической структурой” понимается многослойность приложения на основе композиции и агрегации, а "иерархии наследования" всегда работают "на одном уровне абстракции"

image

С другой стороны, примерно о том же самом пишут Абельсон и Сассман, авторы книги «Структура и интерпретация компьютерных программ», адепты функционального программирования:

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

Об этом же писал и Фред Брукс, объясняя, что сложность растет нелинейно при увеличении размера системы. Именно поэтому принцип единой ответственности так важен, и именно поэтому я предложил с самого начала разбить модуль расчета заработной платы на более мелкие составляющие. Любая сложная система является иерархичной, и плоские графы объектов является важным признаком плохого дизайна.

Система, построенная из большого количества мелких классов будет сложной до тех пор, пока мелкие классы не будут объединяться в более высокоуровневые блоки. В результате нам нужно стремиться к решению, когда на каждом “слое” приложения мы сможем оперировать относительно небольшим количеством классов для достижения более высокоуровневого поведения.

Буч пишет, что “максимальное количество структурных единиц информации, за которыми человеческий мозг может одновременно следить, приблизительно равно семи плюс-минус два.” Это значит, что если при реализации класса или модуля вам приходится думать о большем количестве понятий, то решение будет сложнее, чем оно могло бы быть.

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

Упражнение для оценки роста сложности

Устройте себе как-нибудь такое ментальное упражнение. Возьмите любой свой класс бизнес-логики и вместо конструктора и деструктора, сделайте метод Initialize и Destroy и добавьте два булевых поля: initialized и destroyed, которые будут показывать проинициализирован объект или уже уничтожен. И теперь обновите реализацию этого класса с учетом состояния этих двух полей и посмотрите, как изменится его сложность. Вы наверняка заметите, что класс с достаточно простой логикой стал уже не таким простым, как изначально.

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

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

  1. 1. "переход от наследования к агрегации"

    2. "Любая сложная система является иерархичной, и плоские графы объектов является важным признаком плохого дизайна"

    Наверное стоит уточнить что иерархичность - не следствие применения наследования. А то если не вдумываться можно увидеть противоречие высказываний (безусловно его тут нет).

    ОтветитьУдалить
  2. @gregit: Да, спасибо за это уточнение.

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

    Сложные иерархии строятся как раз на основе композиции и агрегации, а "иерархии наследования" всегда работают "на одном уровне абстракции".

    ОтветитьУдалить
  3. Слушай, я для тех - кто комменты не читает. Ты бы вынес последнее предложение из пред. коммента в "Заключение". По-моему - это квинтэссенция различия иерархии наследования и иерархии слоев. Мне кажется это важно.

    ОтветитьУдалить
  4. Слушай, я для тех - кто комменты не читает. Ты бы вынес последнее предложение из пред. коммента в "Заключение". По-моему - это квинтэссенция различия иерархии наследования и иерархии слоев. Мне кажется это важно.

    ОтветитьУдалить
  5. @eugene: обновил пост с разъяснением, что такое "иреархия".

    ОтветитьУдалить