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

Разбираясь со всем этим по отдельности, меня заинтересовал вопрос — как они взаимосвязаны? Пытаясь выстроить иерархию и вдохновившись небезызвестной пирамидой Маслоу, я построил свою пирамиду «архитектуры приложения».

О том, что из этого вышло — читайте под катом.

О пирамиде

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

Уровни пирамиды


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

Безусловные ограничения


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

Очевидно, что такие ограничения влияют на весь проект — на его архитектуру, выбор технологий и способ управления командой.

Бизнес требования


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

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

Сложность: KISS, YAGNI


Вряд ли кто-то будет спорить, что сложность системы является одним из самых важных факторов, влияющих на все остальные аспекты и, в конечном счете, на успех проекта.
Принципами, направленными на борьбу со сложностью в разработке приложений, являются KISS (Keep it simple, stupid) и YAGNI (You aren't gonna need it).

Принцип KISS призывает упрощать:
большинство систем работают лучше всего, если они остаются простыми, а не усложняются
А YAGNI не проектировать наперед сверх меры:
Вам это не понадобится
Оба они очень абстрактны и годятся для любого приложения, что делает их основополагающими.

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

Связность: DRY, SRP, ISP, high cohesion


Старая шутка про слона, которого нужно есть по частям в полной мере годится для любой сложной задачи. В том числе, и для разработки крупных приложений. За корректное разделение слона задачи на небольшие, изолированные и точно сформулированные подзадачи отвечают два принципа: DRY (Don't repeat yourself) и SRP (The Single Responsibility Principle).

Принцип DRY гласит:
Каждая часть знания должна иметь единственное, непротиворечивое и авторитетное представление в рамках системы
А принцип SRP:
каждый объект должен иметь одну ответственность

Может показаться, что оба принципа — это одно и то же, только разными словами, но это не так. На самом деле они дополняют друг друга. Например, руководствуясь только DRY, вы можете создать один объект, рассылающий почту и рассчитывающий налог. Если нигде больше в коде нет других объектов, с аналогичной функциональностью — условие удовлетворено. SRP же заставит вас разделить ответственности, возложив их на разные объекты.

Принцип ISP (Interface segregation principle) утверждает, что:
Клиенты не должны зависеть от методов, которые они не используют.
Неожиданно, с этим принципом у меня возникли самые большие проблемы. Он чем-то похож на YAGNI — «клиенту могут и не понадобятся эти методы интерфейса». С другой стороны у него очень много и от SRP — «один интерфейс для одной задачи». Его можно было бы отнести к обобщению «Сложность» и поставить на один уровень с YAGNI и KISS, но эти два принципа более абстрактны.

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

Сильная связанность или зацепление (high cohesion) — это метрика, показывающая, насколько хорошо код сгруппирован по функционалу. Выполнение принципов DRY и SRP ведет к коду с сильной связностью, в котором части, выполняющие одну и ту же задачу расположены «близко» друг к другу, а разные — изолированы. Выполнение ISP тоже ведет к сильной связности, хотя это и не так очевидно. Разделяя один интерфейс на более мелкие части вы группируете их по функционалу, оставляя в каждом интерфейсе только наиболее связанные между собой методы.

GRASP: high cohesion, loose / low coupling
Выше сильная связанность названа «метрикой», хотя ее в полной мере можно назвать принципом. High cohesion, наряду с Low coupling является частями GRASP (General responsibility assignment software patterns), который в этой статье не рассматривается.

Зависимости: IoC, DIP, loose coupling


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

IoC (Inversion of control) предполагает наличие некоторого фреймворка, который будет передавать управление компонентам нашей программы в нужный момент. При этом компоненты могут ничего не знать друг о друге.

DIP (Dependency inversion principle) гласит:
Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Слабая связность (loose coupling) — это не принцип, а метрика, показывающая, насколько компоненты системы независимы друг от друга. Слабо связанные компоненты не зависят от внешних изменений и легко могут быть использованы повторно. IoC и DIP являются средствами для достижения слабой связности компонентов в системе.

Расширение: OCP, LSP


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

Принципами, относящимися к расширению функционала, являются OCP (Open/closed principle) и LSP (Liskov substitution principle).

OCP определяет, что
программные сущности (классы, модули, функции и т. п.) должны быть открыты для расширения, но закрыты для изменения
А LSP:
Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.
Грамотная реализация этих принципов позволит в будущем изменять (расширять) функционал приложения не изменяя уже написанный код, а создавая новый.

Методологии: TDD, DDD, BDD


Помимо принципов, существует еще довольно большой набор методологий, например BDD (Behavior-driven development) и TDD (Test-driven development). В общем случае методология, в отличие от принципа, определяет некоторый процесс, применяемый к разработке приложения.
Методологии очень разнообразны, решают разные задачи и часто, при разработке приложения, используется их комбинация. Но какую бы из них вы ни выбрали, у вас возникнут серьезные проблемы, если попытаться применить их к коду, нарушающему предыдущие принципы. Например: легко ли будет применить TDD и писать тесты для кода, который нарушает DIP и содержит ссылки на конкретные реализации?

Архитектурные стили и фреймворки


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

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

Библиотеки и инструменты


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

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

Зачем все это нужно?


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

Если изменения вносятся в существующую систему, то он примерно таков:

1. Реализуемы ли мои изменения с учетом отведенного мне бюджета, времени и прочих объективных ограничений?
2. Не противоречат ли они бизнес-требованиям?
3. Достаточно ли просто то, что я собираюсь сделать? Есть ли более простые способы сделать это?
4. Как мне разделить мою задачу на компоненты (подзадачи) так, чтобы каждый из компонентов выполнял только одно действие? Не дублирую ли я уже существующий функционал?
5. Как мои компоненты будут связаны с остальной программой? Как уменьшить количество связей? Буду ли я иметь возможность использовать мои компоненты повторно или заменить один компонент на другой в будущем?
6. Смогу ли я расширить функционал моих компонентов, не изменяя их? Заложил ли я возможности расширения в те компоненты, вероятность изменения которых в будущем особенно велика?
7. Не противоречат ли мои изменения выбранной мной методологии?
8. Соотносятся ли мои изменения с лучшими практиками используемого мной фреймворка? Не нарушают ли они общий архитектурный стиль моего кода?
9. Могут ли использованные мной библиотеки решить поставленную подзадачу?

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

Так, например, выбранная вами библиотека не должна конфликтовать с используемым фреймворком. Если это происходит — вы меняете библиотеку, а не фреймворк. Фреймворк должен поддерживать (или хотя бы делать возможным) использование выбранной на предыдущем этапе методологии и так далее.

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

Заключение


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

Комментарии (15)


  1. ahmpro
    11.09.2017 19:08
    +4

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


    1. bocharovf Автор
      11.09.2017 21:48
      +1

      Намешали всего в кучу
      Куча — это что-то неупорядоченное. А тут как раз наоборот — выстраивается иерархия.
      У вас какие-то конкретные возражения по расположению уровней/элементов или Вы против идеи «иерархичности» в целом?


      1. napa3um
        11.09.2017 23:24
        +8

        У вас в пирамиде располагаются колесо, разметка дороги, коробка передач, первое начало термодинамики и гаишник.


        1. flancer
          12.09.2017 07:52
          +2

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


          1. napa3um
            12.09.2017 07:56
            +5

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


            1. flancer
              12.09.2017 08:25

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


              1. napa3um
                12.09.2017 08:29
                +6

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


                1. flancer
                  12.09.2017 08:41

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


        1. bocharovf Автор
          12.09.2017 08:00
          +1

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


          1. napa3um
            12.09.2017 08:02
            +2

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


  1. KeyJoo
    12.09.2017 07:35
    +1

    Я б, в строители пирамид* пошёл, пусть меня научат…
    Умение свести всё к единой структуре, от которой будет польза — это уже половина пройденного пути. Я только за+, чтобы побольше было такого рода синтеза. Хотя в пирамиде слои вначале что-то смутили (их очередность). Но чек-лист взял своё!


  1. archik
    12.09.2017 09:51

    В пирамиде и в заголовке есть аббревиатура DDD, однако она нигде не расшифровывается и никак не упомянается. Зачем вы так обижаете Эрика Эванса и Domain-Driven-Design? :)


  1. Sinatr
    12.09.2017 10:06

    шутка про слона, которого нужно есть по частям
    какая шутка?


    1. KeyJoo
      12.09.2017 11:04

      Я бы этого слона тоже в пирамиду запихнул, в слой методологий.


  1. ghost404
    12.09.2017 23:34
    +2

    Немножко странная схема получилась и чек-лист тоже.


    1. DDD напрямую связана с бизнес логикой. Соответственно слой с DDD должен идти сразу за слоем "Бизнес-требования" и до всяких там KISS и YAGNI.
    2. Вы даже в статье говорите, что IoC связан с фреймворком. И как же оно будет прыгать через 2 уровня?
    3. DIP это не только зависимости ваших компонентов друг от друга, но и зависимости от фраймвора и библиотек. То есть, по идее, правильней сформулировать, что DIP это уровень между вашим компонентом и окружением в роли которого выступает фраймворк и библиотеки.
    4. Уровни "Расширение" и "Связанность" скорей относятся к особенностям реализации компонента. Я бы скорей выделял уровень "Реализация" с вложенными в него уровнями "Расширение" и "Связанность" между частями реализуемого компонента, а сверху над ним уровень DIP, связывания компонента с окружением.
    5. Методологии TDD и BDD я бы вынес вообще куда-то в сторонку. Это относится скорее к методам написания кода. Это примерно тоже самое, что включить в пирамиду уровень "Редактор" (IDE, Vim, Блокнот). Редакторы тоже будут влиять на время и стоимость разработки. Я конечно утрирую, но все равно непонятно почему они находятся над DIP, LSP и прочими.

    В чек-лист меня смущает порядок пунктов и путаница со временем/состоянием.


    7. Не противоречат ли мои изменения выбранной мной методологии?

    То есть вы уже сделали изменение и проверяете сделали ли вы его по TDD?


    8. Соотносятся ли мои изменения с лучшими практиками используемого мной фреймворка? Не нарушают ли они общий архитектурный стиль моего кода?

    Почему вы решаете вопросы совместимости после выполнения работы?


    9. Могут ли использованные мной библиотеки решить поставленную подзадачу?

    А если не могут, то что вы писали все это время решая поставленную задачу?


    1. Реализуемы ли мои изменения с учетом отведенного мне бюджета, времени и прочих объективных ограничений?

    А здесь вы говорите об исполнении задачи в будущем.


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


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