воскресенье, февраля 17, 2008

Простой дизайн

Размышления о том, в чем заключается роль опыта в проектировании, в чем опасность готовых решений или каркасов.



Недавно у меня завязалась дискуссия с одним коллегой на форуме RSDN. Обсуждался вопрос о способе расчета массы объекта, состоящего из множества составных частей (которые тоже состоят из частей). Я предложил использовать простейший вариант реализации, основанный на наследовании. Коллега предложил более сложный вариант с использованием интерфейса и выделенного класса (я специально не упоминаю детали, чтобы избежать обсуждения этого вопроса здесь). Дискуссия разгорелась довольно жаркая. Суть ее свелась к спору о том, что лучше: простое решение, учитывающее только текущие требования, либо более сложное, которое учитывает возможные дополнительные требования, которые могут возникнуть при дальнейшем развитии системы. Причем, как оказалось, корень противоречия лежит в самом подходе к проектированию системы.

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

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

Один из коллег высказал такую мысль:
«Это, в общем, вопрос компромисса:
Слишком частные решения как правило ограничивают масштабируемость приложения и увеличивают затраты на переработку. В том числе переработку архитектуры.
Слишком общие решения могут безосновательно увеличить стоимость разработки и снизить производительность.
Так что тут больше вопрос интуиции.
Начинающие разработчики, как правило, делают первую ошибку, разработчики среднего уровня — вторую»


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

Раньше я сам придерживался этой точки зрения. Но сейчас я считаю, что это ошибка.

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

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

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

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

Очень сложно получить хороший дизайн сразу и окончательно, хотя бы потому, что это всегда результат компромисса. Еще сложнее получить хороший дизайн, когда помимо реально существующих требований мы закладываем в него возможности «на будущее». Наоборот, выделив главное, отбросив несущественное, у нас появляется гораздо больше шансов создать предельно простой дизайн, который в тоже время наилучшим образом удовлетворяет самым важным требованиям. Наиболее ярко этот подход проявился в методологии XP, в знаменитом принципе YAGNI (You Aren't Gonna Need It) – «тебе это не пригодится».

Оппоненты данного подхода обычно говорят: «зачем я буду несколько раз переделывать свою систему, когда я сразу могу внести в нее все необходимые свойства, благо есть архитектурные паттерны, framework-и, software factory и т.д.». Ответ прост. Проектирование процесс «грязный» (подверженный ошибкам) и итеративный. Создать «сразу» окончательный дизайн невозможно. Поэтому наиболее выигрышной стратегией будет не «предвосхищение требований», а «ожидание изменений».

Совершенно показателен в этом плане пример использования готовых библиотек и «фабрик ПО». Несколько лет назад Microsoft начал публиковать так называемые Application Blocks, исходные коды библиотек для решения типовых задач, возникающих при построении приложений: конфигурирования, логирования, кэширования, бизнес логики и т.д. Это были действительно интересные библиотеки, которые обобщали наиболее передовой опыт. Затем все эти блоки были сведены в Enterprise Library. Это уже был своего рода каркас для построения распределенных корпоративных приложений. Сейчас на основе Enterprise Library созданы Software factory – библиотеки дополнены шаблонами и мастерами design time для Visual Studio. Само название «Фабрика ПО» подразумевает, что она позволяет максимально упростить и ускорить создание приложений, предоставляя каркас содержащий «все необходимое». На практике получается не все так гладко. Разработка идет очень быстро и хорошо, до тех пор, пока не возникает задача, на которую не были рассчитаны базовые библиотеки. Тут приходится вносить изменения в эти библиотеки, а поскольку они рассчитаны на максимально широкий класс задач, то они имеют достаточно сложный и нетривиальный дизайн, и вносить изменения в них весьма не просто. Это повторяется из проекта в проект – резкий старт, огромная экономия времени на рутинных аспектах и затем такой же резкий стопор в разработке, погружение в детали библиотек, переделки, а зачастую дублирование для реализации недостающих функций. В результате проект занимает примерно столько же времени, как и при разработке с нуля. А дизайн системы представляет собой огромный блестящий каркас, заполненный функционалом едва ли на десятую часть, окруженный достаточно неприглядными подпорками.

Альтернатива этому – итеративное проектирование с поддержанием максимально простого дизайна каждой части системы на низком уровне, и структурной целостности на более высоком уровне. Неизбежные изменения гораздо проще вносить, когда дизайн прост, чем когда он сложен. К тому же в этом случае ваша система никогда не будет обременена тоннами неиспользуемого или бесполезного кода. Я скажу более, оптимальная стратегия проектирования для систем с длительным жизненным циклом - вносить изменения в систему как можно позже, когда их необходимость становится совсем уж очевидной. Так вы не только уменьшите количество изменений, но и улучшите их «качество».

А как же framework-и и фабрики ПО? Это замечательные вещи, но использование их в проекте, особенно впервые, это значительный риск. И в тоже время, если вы уже использовали тот или иной каркас, вы зачастую можете обнаружить, что именно для вашего конкретного случая этот каркас, как говорится, именно то, что доктор прописал. Вот в чем роль опыта :) И в тоже время каркасы, это просто кладезь best practice архитектурных решений и поэтому они достойны всяческого внимания и изучения.

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

alamar комментирует...

Начинающие разработчики, как правило, делают первую ошибку, разработчики среднего уровня — вторую»

В тему: http://steve-yegge.blogspot.com/2008/02/portrait-of-n00b.html

Это замечательные вещи, но использование их в проекте, особенно впервые, это значительный риск.
Ну. Например, logging точно стоит использовать уже готовый, а ORM - уже не факт вовсе. Как отличить один случай от другого - не знаю.

Анонимный комментирует...

Начинающие программисты совершают свою первую ошипку когда идут в программисты :)))

Sergey Rozovik комментирует...

А куда же им деваться? Убить себя ап стену?

Анонимный комментирует...

Говорила же мама - иди в гинекологи - руки фсегда в тепле будут

Бороздин Андрей комментирует...

В общем согласен с Сергеем. Если пишешь копонент “на вырост”не зная как он будет расти, то фактически создаешь функцинал без каких либо требованийи по этому получишь:
1) бОльшие затраты на разработку, что естественно, т.к. надо сделать больше работы;
2) функционал может оказаться не востребованным или востребован, но не таким образом;
3) функционал будет содержать большое количество багов, т.к. не особо тестировался или тестировался, но не в тех рамках;
В итоге получается, что переделать и исправить код, который писался «на вырост» будет не проще и не дешевле, чем потом его разработать. Как правило в таких случаях у программеров возникает желание «все выкинуть и заново написать», что за частую и происходит.