Design

Управление сложностью - главный технический императив разработки ПО. Все решения должны приниматься в контексте борьбы со сложностью.

Один из методов борьбы со сложностью - design it twice. В первый раз вы просто делаете систему что бы заработало, а во втором подходе рефакторите и меняете ее что бы был хороший дизайн.

The increments of software development should be abstractions, not features.

Делайте грубую работу в начале и тонкую в конце.

Принципы:

Принципы дизайна:

  • Скрывайте подробности реализации.

  • Избегайте глобальных переменных.

  • Выберите небольшой набор непересекающихся примитивов.

  • Не делайте ничего за спиной пользователя.

  • Делайте одно нормальное решение.

  • Выполняйте одну и ту же операцию везде одинаковым способом.

  • Освобождайте ресурсы на том же уровне на котором их запрашиваете.

  • Обрабатывайте ошибки чем быстрее тем лучше.

Complexity

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

Под сложностью понимается все относящееся к структуре и содержанию кода, мешающее нам понимать и модифицировать систему.

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

  • Первый способ борьбы со сложностью - это упростить ее. Сделать очевидной. Можно убрать special cases или можно сделать кодовую базу consistent.

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

It's easier to tell wheter a design is simple than to create a simple design.

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

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

Общая сложность системы определяется сложностью каждого компонента системы умноженного на время которое программист работает с этой частью системы. Поэтому спрятать сложность там где с ней никто не столкнется примерно то же самое как если бы полностью убрать ее. Как следствие из этой формулы: наиболее часто используемые части системы должны быть наиболее простыми.

С=pcptpС =\sum_p c_pt_p

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

Симптомы сложности:

  • Change amplification - изменение в одном месте ведет за собой каскад изменений в других местах.

  • Cognitive load - сколько разработчику требуется времени что бы разобраться в коде и решить поставленную задачу.

  • Unknown Unknowns - неизвестные неизвестные. Система не очевидна в том как она работает.

У сложности две причины:

  • Dependencies - зависимости между кодом существуют когда какая-то часть кода не может быть понята и/или изменена в отрыве. Зависимости - это фундаментальное свойство программ и не может быть полностью разрешено. Зависимости создают change amplification и увеличивают cognitive load.

  • Obscurity - это все возможные непонятки, возникающие из за того, что важная информация о системе не очевидна. Это может быть как плохая документация, так или ее отсутствие. Неявные зависимости, невнятные имена переменных. Сложный дизайн так же влияет, поскольку требует сложной документации. Лучший способ уменьшить неизвестность системы это упростить дизайн. Obscurity creates unknown unknowns and also contributes to cognitive load.

Сложность является инкрементальной. Она аккумулируется маленькими партиями с течением времени. Нужна постоянная работа для сдерживания и уменьшения сложности. Just working code isn't enough. Нужно выбирать стратегический подход к программированию когда продумывается дизайн для решения задачи с минимальной добавочной сложностью. Такой подход называется strategic programming и это разновидность investment mindset.

Например, для решения задачи делается подходящий дизайн, а потом еще описывается это все в виде документации.

Modules

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

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

The best modules provide powerful functionality yet have simple interface. You can call this modules deep modules or cohesive modules. Deep modules have interfaces that makes the common case as simple as possible, yet still provide significant functionality.

Cohesion refers to logical unity of an abstraction -- it singleness of purpose. Cohesiveness is the degree to which parts of the abstraction is related logically. Cohesiveness is an ordinal measure from low to high. We prefer highly-cohesive abstractions because they are sensible and stable.

It's more important for the module to have simple interface than simple implementation. Модули имеют больше пользователей чем разработчиков, поэтому простота использования более важна.

Encapsulation

Modules should be deep. Simple interface and rich functionality.

Сокрытие деталей реализации - это естественный дизайн хорошего модуля. Интерфейс должен содержать все необходимые элементы для использования этого модуля, но не более того.

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

Типичным примером information leakage является temporal decomposition. Это когда задача разбита на модули таким образом, что бы формировался некоторый порядок операций модуля во времени. When designing modules, focus on the knowledge that needed to perform each task, not the order in which task occurs.

General Purpose Modules

General-purpose modules are deeper. Модули должны подчиняться principle of least abstraction: интерфейс модуля должен быть настолько абстрактным, что бы решить весь текущий класс задач, но не более. То есть интерфейс модуля должен быть somewhat general purpose.

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

Separate general-purpose and special-purpose код.

Different Layer, Different Abstraction

В хорошо спроектированной системе каждый уровень отвечает за свою абстракцию. Правило: different layer -- different abstraction. Каждый следующий уровень строится путем интеграции над уже существующим.

Система не должна содержать два разных уровня имеющих одинаковые абстракции. Красным флагом такой проблемы являются pass-through methods. Pass-through methods типичный пример поверхностных модулей с плохой абстракцией.

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

Errors and Exceptions

Prevent bugs. Preventing but far more efficient strategy for bugs than for fixing it.

Make the program robust. Program continue to run when unexpected conditions occurs. Correctness and robustness is different concerns. Correctness is about number of bugs. Robustness is about how your program behaves when an error or unexpected conditions occur.

  • Don't let bugs accumulate.

  • Use assertions and design by contract.

  • Be conservative in what you do, be liberal in what you accept from others (Robustness Principle).

Нужно очень осторожно подходить к обработке ошибок поскольку ничто так не увеличивает сложность системы как обработка ошибок.

Throwing exceptions is easy. Handling them is hard. The complexity of exceptions comes from the exceptions handling code. The best way to reduce the complexity damage caused by exceptions handling is to reduce number of places where exceptions have to be handled. Overall, the best way to reduce bugs is to make software simpler.

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

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

Третья стратегия заключается в том агрегировать ошибки и обрабатывать их в одном месте на самом верху и восстанавливать систему оттуда.

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

При работе с ошибками нужно часто выбирать что мы собираемся обрабатывать, а что нет. Общее правило: never catch exceptions unless you can correct error it represents.

Проверять на ошибки нужно на границах системы, а внутри системы нужно полагаться на доверие. Не стоит сильно увлекаться defensive programming. If no module can be trusted, then defensive programming is imperative. If all modules can be trusted, then defensive programming is irrelevant.

Documentation

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

Self-documenting code это миф, который не работает. Этого недостаточно. Если разработчики должны прочесть код модуля для того, что бы его использовать, то в этом нет никакой абстракции.

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

  • Low-level comments adds precision

  • High-level commends enhance intuition.

Хорошая документация помогает с cognitive load и с change amplification.

Consistency

Consistency - это на самом деле один из принципов дизайна. If system consistent? it means that similar things are done in similar way, and dissimilar things are done in different ways. Consistency reduce cognitive load and unknown unknowns and leads to low amount of mistakes.

Consistency and clean names makes code obvious. Software should be designed for ease of reading, not ease of writing.

Consistency make good things better and bad actions worser.

Есть вещи, которые делают код сложным:

  • Event-driven programming

  • Generic classes and containers

  • Code that violates user expectations

Principle of Least Abstraction

Computer Scientists - create abstractions, but Developers - create solutions.

Зеленые узлы - это use-cases, белые узлы - это абстракции. Каждый use-case может быть решен при помощи какой-то абстракции. Задача разработчика в том, что бы используя самые простые абстракции покрыть как можно больше use-cases. Перемещение по узлам дерева имеет свою стоимость.

Library - это абстракции среднего уровня. Frameworks - это абстракции более высокого уровня, покрывающая множество use-cases, часть из которых вам даже и не нужна. Чем выше абстракция, тем большую цену нужно платить за её adoption.

Power of Abstraction - то число use cases которые абстракция покрывает. Сила программиста определяется тем, какими абстракциями он владеет, сколько use-case они покрывают.

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

Utility, concreteness -- мера немедленной полезности решения, немедленная полезность. It just works. Находится на другим конце спектра от абстракции.

Абстракция обладает большой power, конкретное решение обладает большой полезностью.

Каждый раз переходя на более верхний или нижний уровень абстракции разработчик платит цену (cost).

Principle of Least Power - наша цель покрыть наибольшее число зеленых листьев минимальным числом красных узлов.

Много проблем в программировании возникает из за плохого понимания того, где мы должны находится в уровнях абстракции. Не обольщайтесь элегантностью абстракций. Как правило элегантные абстракции обладают большой power и стоимость их адаптации для конкретных use-cases будет слишком высокой.

Обращайте внимание на примеры. Примеры покрывают конкретные use-cases.

Master levels of abstraction.

Minimal API surface

Проектирование

Проектирование – это постоянное принятие решений о вашей программе на всех уровнях и этапах существования.

Это фундаментальный процесс, пронизывающий вашу программу на всех уровнях.

  • П. – это принятие решений об имени функции и количества и типах аргументов, которые необходимы для её работы.

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

  • П. – это когда вы, прочитав задание, говорите «да я знаю как это делается» - значит вы уже инсайдом приняли ряд решений, о вашей программе.

  • П. – это умение выбрать одно решение перед другим, принимая, что одно будет более соответствовать требованиям, чем другое. П. – это умение выбрать одну альтернативу в перевес другой.

  • П. – это десятки вопросов, которые вы должны себе задать. Вот пример, в книге МакКонелла «Совершенный Код» в конце почти каждой главы есть чеклист вопросов, которые нужно у себя спросить, что бы гарантировать тот или иной аспект качества программы.

    • Пример из «Совершенного кода»

    • Из моего опыта, когда я занимался системой обработки событий, есть очень много способов это сделать, принимаешь решения:

      • Как именно программа уведомляет, какие именно события хочет отслеживать?

      • Как именно формируется информация о событии?

      • Как слушатель событий пристраивается к целевому объекту, где происходят события?

      • Что представляет собой событие?

      • Синхронно или асинхронно обрабатывается событие?

      • Всего, 5 листов вопросов!

  • П. - значительная часть разработки программного обеспечения связана с постоянным вопросом – куда следует поместить этот код?

  • П. – это принятие решений о системе версионирования, об инфрастукруте, о каждой кнопке дизайна, о каждом исключении. Ключевой вопрос – почему так и не иначе?

  • Даже простая программа требует сотен решений, а большая программа – десятков тысяч решений, почему это так, а не иначе.

  • Конкретное программирование – это все принятие решений. Таким образом, можно свести задачу программирования – к задаче оптимального принятия решений в контексте требований к программе.

  • Принимаемые решения должны неотвратимо, как гравитация, приводить нас к реализации требований в нашей программе.

Проблема – это любой аспект вашей программы, по поводу которого надо принять решение.

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

Примеры проблем:

  • Как должна реализовываться определенная часть программы?

  • Что сделать, что бы это узнать?

  • Как организовать декомпозицию программы.

  • Какой формат сохраняемых результатов.

  • Сколько параметров должно быть у этой функции.

  • Какое имя у этой переменной.

  • Как сделать компонент A независимым от компонента B.

  • Как сворачивать программу в трей.

  • Что бы была красивая архитектура.

  • Сдать проект вовремя.

Но невозможно принять решение, если у вас нет информации необходимой для принятия этого решения, или выбора одного из множеств. Какую СУБД выбрать для решения задачи, если вы ни одну из них не пользовали? Поэтому проектирование дополняется вторым, комплементарным к нему процессом.

Анализ – это ваше понимание проблемы и то, как оно отображается на принятие решений, которые нужно запрограммировать. Это может быть как понимание в голове, так и выраженное в какой-нибудь структурированной физической форме (на листке, схема классов, UML, спецификация и т.д.). Ваше понимание проблемы– это анализ.

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

Собственно, процессы анализа и проектирования неразрывно связаны друг с другом. Из вашего анализа следует, какие решения вы примете и на сколько они будут хорошими. Далее, каждое ваше принятое решение – продвигает вас в глубь понимания. Наш мозг так хорошо это делает, что мы легко совмещаем эти два процесса, выполняя их в неразрывном потоке.

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

Постепенно, за много итераций, через несколько месяцев совместной работы со специалистами по перевозкам, мы создали совершенно другую модель. Неспециалисту она казалась гораздо менее очевидной, но зато специалисты были довольны. Акценты сместились на деловые операции по доставке груза.

Корабли никуда не делись из модели, но теперь это были абстракции в виде "рейсов" - отдельных перевозок на корабле, поезде или другом транспортном средстве. Сам по себе корабль как объект стал второстепенным; его могли заменить в последний момент из-за необходимости ремонта или по скользящему графику, тогда как рейс проходил строго по плану. Контейнер же исчез из модели вообще. Точнее, он все-таки фигурировал в программе управления грузами в другой, более сложной форме, но в контексте исходной программы он превратился в мелкую подробность выполнения деловой операции. Физическое перемещение груза отступило на второй план по сравнению с передачей юридической ответственности за груз. А на первый план вышли менее очевидные объекты, такие как "транспортные накладные".

Когда новые моделировщики появлялись в проекте, каковы были их первые предложения? Да все те же: надо добавить недостающие классы "корабль" и "контейнер". Но эти люди не были глупы - они просто еще не приобрели знакомство с предметом.

К процессу анализа я отношу так же прототипирование и работу над proof of concept (доказательство концепта – доказательство существования). Инженерия - экспериментальная дисциплина, и наперед все предусмотреть и продумать невозможно. И чем более рискованный и инновационный у вас проект, тем невозможнее это невозможно. Информации не хватает - надо пробовать, и анализировать результат, и в данном случае эксперимент - критерий истины.

Я где-то слышал, что когда компания тойота разрабатывала свой первый гибридный автомобиль, проектировщики запустили в разработку сразу 80 двигателей, из которых дожили до конца первой стадии только 5, в конце осталось только два равноценных. Разработчики не могли выбрать какой-либо конкретно двигатель и запустили оба в производство. Машины выпускались какое-то время с двумя, пока время и какие-то события не решило в пользу одного.

Значит ли это, что не надо пытаться думать наперед? Тоже нет. Это значит, что не надо обманывать себя. Если для получения понимания надо написать код, пишите, начиная с самой непонятной части, продвигаясь маленькими шажками, держа в голове, что цель этой деятельности на раннем этапе - получить в первую очередь это самое понимание, а не отлаженный код.

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

Итак, дизайн – это отражение декомпозиции вашей программы – её разбиения на составные части. Реальность такова, что со временем проекты становятся только сложнее и надо с этой сложностью, что то делать. Самый явный способ – это декомпозиция – разбиение системы на части.

В процессе проектирования (сиречь постоянного принятия решения о вашей программе в ней рождается некоторая основополагающая структура. Когда количество ваших решений переходит в качество – тогда рождается дизайн.

Структура вашей программы и её качества, возникшие вследствие декомпозиции, называется дизайном. Дизайн - логическая и физическая структура компонентов системы и взаимосвязей между ними, сформированная всеми стратегическими и тактическими проектными решениями и следствия, которые следуют из этих решений. Хороший дизайн легко понять, и дешево модифицировать. Обыкновенно, локальные правки в хороший дизайн можно вносить не зная всей картины, без риска все сломать.

Архитектура (architecture) - это набор ограничений, специфичных для системы или проекта, набор своего рода "правил проектирования", которому вы следуете, когда делаете дизайн.

Архитектура - ограничивает пространство решений ваших проблем.

Например, эти ограничения могут выражаться в

  • Конкретных используемых технологиях,

  • некоторых договоренностях касательно структурирования кода (определенный набор слоев и/или сервисов например), и диктуемая этими ограничениями необходимость задействования некоторого кода (фреймворка, например).

Конкретные ограничения уже не так принципиальны.

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

Архитектура - это то, что вы не сможете "отрефакторить", и из-за чего вы будете переписывать систему с нуля, если оно плохое. Дизайн - это мясо, архитектура - скелет. Архитектура должна удешевить разработку в долговременной перспективе, чтобы система не треснула по швам при добавлении очередной порции требований. Короче говоря, "архитектура" - это когда вы думаете сильно вперед.

Last updated