Программирование — достаточно молодая область знаний, однако, в ней уже существуют базовые принципы «хорошего кода», рассматриваемые большинством разработчиков как аксиомы. Все слышали о 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 тоже ведет к сильной связности, хотя это и не так очевидно. Разделяя один интерфейс на более мелкие части вы группируете их по функционалу, оставляя в каждом интерфейсе только наиболее связанные между собой методы.
Зависимости: 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. Могут ли использованные мной библиотеки решить поставленную подзадачу?
Каждый пункт списка соотносится с определенным уровнем пирамиды. При этом выбор, сделанный на каждом уровне не должен противоречить выбору, сделанному на предыдущих уровнях. Это особенно важно при проектировании системы с нуля, когда «неопределенность» в плане будущей архитектуры гораздо выше и шире возможный выбор.
Так, например, выбранная вами библиотека не должна конфликтовать с используемым фреймворком. Если это происходит — вы меняете библиотеку, а не фреймворк. Фреймворк должен поддерживать (или хотя бы делать возможным) использование выбранной на предыдущем этапе методологии и так далее.
В целом понимание иерархии фокусирует вас на более базовых принципах (нижних ступенях). Кроме того изучение связей между элементами пирамиды позволяет лучше понять картину «в целом», обобщить и систематизировать свои знания. Для более глубокого понимания какого-либо нового принципа его будет полезно попытаться включить в эту систему, понять — на каком уровне он должен располагаться.
Заключение
Эта статья достаточно субъективна и не претендует на исчерпывающее описание всех существующих принципов и методологий. Более того, уже во время написания первого черновика, пара принципов переместилась со своих насиженных мест на другие уровни. Возможно, со временем пирамида будет дополнена новыми элементами или даже уровнями. Если вам кажется, что в ней что-то находится не на своем месте или вы хотите дополнить ее — буду рад конструктивной критике и предложениям в комментариях.
Автор: bocharovf