«Программист занимается строительством моста через пропасть, по одну сторону которой находятся машины (и простейшие примитивы, начиная с 0 и 1), а по другую — бесконечное множество прикладных задач».
Эдсгер Вибе Дейкстра
Привет, Хабр! Меня зовут Грант, я уже 5 лет занимаюсь backend-разработкой в SimbirSoft, а с 2021 года возглавляю Backend-отдел компании в Краснодаре. В последние несколько лет неоднократно замечаю, что на входном интервью соискатели часто путают принципы разработки. Поэтому в этой статье решил разобрать, как проще их понимать, какие связи между ними прослеживаются, стоит ли учить все или что-то можно пропустить. А еще попробую раскрыть некоторые особенности, какие секреты скрывают популярные принципы разработки.
Кому будет интересно?
Разработчикам — для расширения кругозора, изучения новых инструментов для разработки, большего понимания кода других.
Тимлидам, техлидам — для изучения опыта выстраивания процессов разработки, в том числе в процессе организации code review и пр.
Project Manager — косвенно, в разрезе сроков и бюджета проекта. Если вы участвуете еще и в формировании таких документов, как workflow, будет еще полезней.
Вместо вступления
Начнем с того, что ответим на вопрос: «Зачем нам принципы разработки?». Для себя я выделяю четыре довода:
• изменение требований,
• командная работа,
• много решений,
• забота о будущем.
Изменения требований. В манифесте методологии Agile на втором месте стоит следующая ценность: «Готовность к изменениям важнее следования первоначальному плану». Или более конкретный принцип: «Изменение требований приветствуется, даже на поздних стадиях разработки. Agile-процессы позволяют использовать изменения для обеспечения заказчику конкурентного преимущества».
Таким образом, мы как разработчики информационных систем часто сталкиваемся с требованиями, которые меняются на ходу. Порой даже небольшие изменения в дизайне одной формы приводят к изменению слоя доступа данных, бизнес-логики и даже работы взаимосвязанных микросервисов.
А принципы разработки помогают сделать гибкую систему, открытую к изменениям.
Конечно, не всегда программа или ее компоненты будут меняться. Если изменения не предвидятся, например, потому что мы планируем в будущем полностью переписать разработанное MVP, то тратить ресурсы на то, чтобы заложить какую-то гибкость, не стоит.
Командная работа. Когда мы говорим о высоких материях и наше абстрактное мышление приводит к лабиринтам, которые могли зародиться только в нашей голове, то не стоит забывать о том, что кроме вас эту систему должны понимать еще и другие коллеги в вашей команде ?
А принципы разработки помогают одинаково мыслить и быстрее понимать примененные пути решений друг друга.
Конечно, если вы не одинокий волк-разработчик)
Много решений. Сам процесс разработки содержит большое количество идей и техник, что рождает разные степени свободы. И чтобы разобраться во всем этом, необходима некая инструкция или, по крайней мере, перечень проблем и их решений.
А принципы разработки содержат решения проблем и даже некоторые особенности тех или иных решений.
Забота о будущем. Как из небольших кирпичиков возводят здания, так и мы создаем из подсистем системы, а из них еще более крупные системы или компоненты. При этом очень важно заранее выбрать хорошие «кирпичи». Или если среди них попадаются и хорошие, и плохие, то важно хотя бы знать, как их укладывать друг с другом.
А принципы разработки помогают создавать масштабируемые системы с заделом на будущее.
Если говорить о практическом применении, то принципы разработки могут так или иначе влиять на:
скорость выпуска фич,
скорость интеграции между командами,
скорость принятия изменений в коде (change-list, pull request, merge request) при code review,
простоту, гибкость и тестируемость кода (mock, зависимости, абстракции),
скорость передачи дел другой команде,
техническую документированность системы,
проектирование систем с переиспользуемыми компонентами.
Но мы ведь не настолько наивны, правда?
Основная задача разработчика, конечно же, писать код. И как бы PM или TL ни пытался выстроить процессы, организовать выпуск более качественного кода, гибких и быстрых решений, внедрить правила, писать код будет разработчик. И для него важно, чтобы процессы были жизнеспособны и не добавляли проблем.
Поэтому постараюсь рассуждать не однобоко, на все «красивое» противопоставлять суровые реалии, ведь именно разработчики реализуют задачи, и каждое изменение процесса может вызвать у них боль и непонимание.
Принципы
О каких принципах мы говорим? Да вот же они! Собрал наиболее известные.
Если вспомните еще — пишите в комментах ? Учтите, что мы не говорим про паттерны (будь то паттерны проектирования или корпоративные шаблоны и прочее), хотя они очень близки с принципами. Для их различия мне нравится такая мысль: «Паттерны — это способ для переиспользования полезных принципов».
Принцип/ группа |
Пояснение, источник |
SOLID |
Группа из 5 принципов: SRP (single responsibility principle), OCP (open-closed principle), LSP (Liskov substitution principle), ISP (interface segregation principle), DIP (dependency inversion principle). Источник: Р. Мартин «Чистая Архитектура», Часть III Принципы дизайна |
GRASP |
Группа из 9 принципов: Information Expert, Creator, Controller, Low Coupling, High Cohesion, Polymorphism, Pure Fabrication, Indirection, Protected Variations. Источник: К. Ларман «Применение UML 2.0 и шаблонов проектирования», Главы 17.10-17.14, 25 |
KISS |
Keep It Simple, Stupid / Делай проще |
DRY |
Don’t Repeat Yourself / Не повторяйся |
YAGNI |
You Aren’t Gonna Need It / Вам это не понадобится |
BDUF |
Big Design Up Front / Глобальное проектирование прежде всего |
APO |
Avoid Premature Optimization / Избегайте преждевременной оптимизации. Источник: The Art of Computer Programming, Knut |
LoD |
Law of Demeter / Закон Деметры Источник: Lieberherr, Karl J. and Ian M. Holland. “Assuring good style for object-oriented programs.” IEEE Software 6 (1989): 38-48. |
LKP |
Principle of Least Knowledge / Принцип наименьшего знания, аналог LoD |
Okkama Razor |
Бритва Оккама: «Не следует привлекать новые сущности без крайней на то необходимости» |
Principle of Least Astonishment |
Правило наименьшего удивления, principle of least surpris. |
Принцип Эйнштейна |
Сделай настолько просто, насколько возможно, но не проще |
Римский принцип |
Разделяй и властвуй Источник: The Yale Book of Quotations, 2006, p. 610. |
Принцип Калашникова |
Избыточная сложность — это уязвимость |
Tell-Don't-Ask |
Говори, не спрашивай. |
CQS |
Command-query Separation: каждая функция является либо командой (command), которая выполняет действие, либо запросом (query) |
Measure Twice and Cut Once |
Семь раз отмерь, один раз отрежь |
Unix-философия |
Группа принципов. Состоит из 17 правил, описанных Эриком Рэймондом в книге «Искусство программирования в Unix». Rule of Modularity: Write simple parts connected by clean interfaces. Rule of Clarity: Clarity is better than cleverness. Rule of Composition: Design programs to be connected to other programs. Rule of Separation: Separate policy from mechanism; separate interfaces from engines. Rule of Simplicity: Design for simplicity; add complexity only where you must. Rule of Parsimony: Write a big program only when it is clear by demonstration that nothing else will do. Rule of Transparency: Design for visibility to make inspection and debugging easier. Rule of Robustness: Robustness is the child of transparency and simplicity. Rule of Representation: Fold knowledge into data so program logic can be stupid and robust. Rule of Least Surprise: In interface design, always do the least surprising thing. Rule of Silence: When a program has nothing surprising to say, it should say nothing. Rule of Repair: When you must fail, fail noisily and as soon as possible. Rule of Economy: Programmer time is expensive; conserve it in preference to machine time. Rule of Generation: Avoid hand-hacking; write programs to write programs when you can. Rule of Optimization: Prototype before polishing. Get it working before you optimize it. Rule of Diversity: Distrust all claims for “one true way”. Rule of Extensibility: Design for the future, because it will be here sooner than you think. |
Интуитивный подход |
Мне кажется, это достаточно популярный принцип. Тут можно предполагать, что разработчик полагается на интуицию и собственный опыт |
Анализ
Есть ли тут вообще повод для анализа? Конечно, мы знаем о множестве книг и статьей по поводу различных принципов разработки, однако есть несколько но.
1) Во-первых, когда мы видим любой перечень из более чем 7±2 элементов, их становится сложно воспринимать. Об этом нам также говорит «кошелек Миллера». В нашем списке больше 10 принципов, некоторые из которых объединены в группы. Поэтому надо как-то перевести такое множество из плоскости с бо́льшим количеством в плоскость с меньшим количеством переменных. И позже я покажу, как выделили важные детали и отбросили ненужное.
2) Во-вторых, у тех, кто только начал свой путь в IT, возникает множество вопросов по поводу принципов разработки:
Где взять перечень всех принципов?
Есть ли у принципов приоритет важности?
Как и в каком порядке их применять?
Когда их стоит начинать учить?
Итак, почему все так сложно?
Я выделил следующие причины, которые усложняют понимание и применение принципов разработки:
Разное время создания, разные авторы, разные источники. Некоторые принципы утрачивают актуальность или актуальны с оговоркой.
Области знаний, откуда они пришли, разные, поэтому принципы формулируются и классифицируются совершенно по-разному.
Отсутствие общей инструкции по применению: как принципы связаны, есть ли взаимозаменяемость, в какой последовательности их применять.
Наличие уровня абстракции — они не для конкретного языка или технологии, у них разный уровень абстракции (уровень кода, модульный уровень, уровень компонентов, архитектурный уровень или несколько сразу).
Ну и конечно, огромное количество принципов.
Ситуацию с принципами делает более загадочной и то, что даже опытные разработчики не всегда хорошо к ним относятся, считая их не практичными. Отчасти это правда, так как принципы не говорят нам напрямую, например, делать ли конкретный метод асинхронным, использовать ли фабрику для создания экземпляров определенных объектов, применять ли политику отказоустойчивости запросов, какой брокер сообщений использовать в определенном месте, какой протокол выбрать для взаимодействия сервисов, применять ли библиотеку AutoMapper, использовать ли подход CQRS и так далее.
Но в этой статье мы как раз попытаемся немного примирить принципы и разработку.
Решение проблем с принципами
Очевидное решение: группировка
Итак, первое, что можно придумать, чтобы упростить восприятие принципов — сгруппировать их по некоторым признакам.
Можно заметить, что некоторые принципы звучат как призывы, другие — как философские высказывания, а третьи более абстрактные. Таким образом, условно их можно сгруппировать так:
Группа |
Принципы |
«Призывы» |
KISS (Делай проще), DRY (Не повторяйся), YAGNI (Тебе это не понадобится), APO (Избегайте преждевременной оптимизации), Measure Twice and Cut Once (Семь раз отмерь, один раз отрежь), Принцип Эйнштейна (Сделай настолько просто, насколько возможно, но не проще), Римский принцип (Разделяй и властвуй), Tell-Don't-Ask (Говори, не спрашивай) |
«Звучат по-философски» |
Бритва Оккама (Okkama Razor), Unix-философия (17 принципов), Принцип Калашникова (Избыточная сложность - это уязвимость), Принцип наименьшего знания, Правило наименьшего удивления, Закон Деметры |
«Дизайн и архитектура» |
GRASP, SOLID, BDUF, CQS |
Такая группировка может помочь запомнить принципы, но не подобраться к их сути.
Поэтому больше пользы от второго подхода — объединить одинаковые принципы по их сути, которые повторяют некоторую логику друг друга:
Принципы «Не усложняй»: KISS (Делай проще), Принцип Эйнштейна (Сделай настолько просто, насколько возможно, но не проще), Бритва Оккама, Принцип Калашникова (Избыточная сложность — это уязвимость), Правило наименьшего удивления
Принципы «Не оставляй лишнего»: DRY (Не повторяйся), YAGNI (Тебе это не понадобится), APO (Избегайте преждевременной оптимизации),
Принципы «Упрости зависимости»: Закон Деметры, Принцип наименьшего знания, SRP (зависимость от акторов), ISP, Римский принцип, Tell-Don't-Ask, Low Coupling, High Cohesion
Принципы «Заложи гибкость»: BDUF, OCP, DIP, ISP, Polymorphism, Low Coupling, Indirection
Таким образом, у нас осталось всего 4 группы принципов: Не усложняй, Не оставляй лишнего, Упрости зависимости и Заложи гибкость.
Однако некоторые принципы могут попасть в несколько групп. Например, ISP (принцип разделения интерфейсов) говорит нам выделить интерфейсы, чтобы объекты не зависели от тех методов, которыми они не пользуются. Таким образом, для определенного объекта зависимость уменьшается, но при этом появляется еще и гибкость (можно создавать объекты, которые реализуют только этот или только другой интерфейс или их комбинации).
Но это не все. Как проще понимать группы принципов? Раскроем тайны внутренних связей групп принципов.
Тайна SOLID
Теперь рассмотрим принципы SOLID. Чтобы понять, как связаны эти 5 принципов между собой, достаточно увидеть, а какие вообще решения применяются, чтобы соответствовать принципам SOLID?
И, огогошечки (!), из таблицы видно, что тут основных приемов можно выделить всего-то парочку:
Декомпозиция (делить или расслаивать классы, интерфейсы или модули на более мелкие)
Абстрагирование (выделить интерфейс, абстрактный класс или базовый модуль)
Конечно, не все так гладко ложится на эти два приема. Но это самый частый случай. Менее популярные приемы тоже могут использоваться:
OCP (например, когда применяют методы расширения)
LSP (когда необходимо следовать контрактному проектированию, design by contract).
Но не будем забегать вперёд. Про эти и другие нюансы SOLID мы порассуждаем ниже.
Серые кардиналы GRASP
Из 9 принципов GRASP можно выделить явных лидеров, за которыми следуют остальные:
Low Coupling
High Cohesion
и Protected Variations.
Начнем с первых двух, которые, судя по мнению многих авторов, необходимо рассматривать совместно. Напомню, что Coupling определяет, насколько жестко один элемент связан с другими, а Cohesion — мера сфокусированности обязанностей класса.
Итак, разработчик постоянно должен балансировать между двумя противодействующими проблемами: желанием инкапсулировать абстракции и необходимостью сделать видимыми определенные абстракции для других модулей. Другими словами, разработчик стремится создавать модули, которые являются сфокусированными (путем группировки логически связанных абстракций) и слабо связанными (путем минимизации зависимостей между модулями). С учетом этого Грэди Буч и его соавторы по книге “Object-Oriented Analysis and Design with Application” определяют понятие модульности следующим образом: «Модульность — это свойство системы, разбитой на множество модулей с высокой степенью зацепления и слабым связыванием».
Для любителей ML (Machine learning) можно привести пример того, как лучше понимать модульность. Всё просто, это же задача кластеризации! Каждый кластер — это и есть модуль, и нам требуется выделить кластеры именно таким образом, чтобы внутри кластера были схожие объекты (среднее расстояние между объектами в кластере должно быть как можно меньше) — High Cohesion, а между кластерами объекты должны существенно отличаться (среднее расстояние между объектами из разных кластеров должно быть как можно больше) — Low Coupling.
Таким образом, далее вместо пары Low Coupling и High Cohesion можем просто использовать термин «модульность».
Наконец, что касается «серого кардинала» по имени Protected Variations. Напомним, что этот принцип борется с тем, чтобы изменения спроектированных элементов не оказывало нежелательного влияния на другие элементы.
И чтобы понимать, насколько важна роль Protected Variations, просто перечислим подходы, которые способствуют соблюдению этого принципа: инкапсуляция, полиморфизм, Low Coupling, Indirection, Open-Closed principle, Liskov substitution principle, закон Деметры, брокеры, виртуальные машины и др. На лицо большой авторитет :)
А теперь интересное: давайте противопоставим шесть оставшихся принципов GRASP нашим трем «серым кардиналам».
В таблице хорошо видно, что суть проблем 6 из 9 принципов GRASP сводятся к 3 другим: Модульности (которая объединяет Low Coupling и High Cohesion) и Protected Variations.
Таким образом, важно не запоминать все 9 принципов GRASP, а понимать проблемы, которые стоят всего лишь за двумя принципами: Модульность и Protected Variations.
Но и это опять не всё. Как уже было озвучено в главе «Анализ», сложность понимания принципов возникает в том числе потому, что непонятна связь между принципами. Давайте не будем обходить стороной взаимосвязь таких гигантов, как SOLID и GRASP, и рассмотрим их совместно.
Родственные связи: GRASP Vs. SOLID
Дальние родственники High Cohesion и SRP. Забавный факт в том, что многие, говоря о сути SRP, начинают рассказывать про High Cohesion. Ловушка как минимум в переводе SRP — принцип единственной ответственности, из-за чего можно подумать, что класс или модуль должны отвечать за что-то одно. Но мы уже говорили, что SRP — это когда модуль отвечает за одного актора, а это не то же самое, что отвечать за что-то одно.
А вот High Cohesion как раз таки предлагает делать классы с небольшим количеством методов, которые функционально тесно связаны между собой. Таким образом, класс будет с однотипной функциональностью или, как «звучит» SRP, иметь единственную ответственность.
Братья DIP и Indirection. Вспомним, что Indirection борется с прямым связыванием путем присвоения обязанностей промежуточному объекту. С другой стороны, DIP утверждает, что наиболее гибкими получаются системы, в которых зависимости в исходном коде направлены на абстракции, а не на конкретные реализации. И чтобы достичь зависимости от абстракций, необходимо выделить промежуточные классы абстракций, которые потом объединяются с классами политиками в одной сборке. Есть еще один вариант реализации DIP, но он также связан с выделением промежуточных абстракций и извлечением их в отдельные сборки. Таким образом, для организации слабой связанности и повышения гибкости и DIP и Indirection предлагают выделить промежуточный элемент.
OCP и LSP — дети Protected Variations. Принцип LSP говорит нам, что подтипы не должны изменять поведение программы, таким образом предполагает защиту от влияния изменений в различных реализациях интерфейса или расширениях суперкласса. Как вы уже догадались, принцип OCP также защищает от изменений компоненты уровнем выше от изменений в компонентах уровнем ниже. Получается, что Protected Variations важный принцип, который поддерживается различными другими принципами, и OCP с LSP — не исключение.
Мы еще не обсудили множество связей принципов SOLID и GRASP с такими принципами, как KISS, DRY, YAGNI и другими, но методику сравнения вы уже поняли, а в предложенной ранее группировке принципов по их сути можно было заметить, что SOLID и GRASP попадают в такие группы, как «Упрости зависимости» и «Заложи гибкость». Если вам будет интересно, то можем поглубже раскрыть этот момент в будущих публикациях.
Какие навыки нужны для применения принципов?
Мы уже поговорили про то, как можно проще запоминать и воспринимать большое количество известных принципов. Теперь попытаемся ответить на вопрос, а какими навыками нужно обладать, чтобы применять эти принципы.
Если вспомнить про SOLID, у которого для решения проблем всего 2 основных приема, то для практического применения этой группы принципов вы должны обладать следующими навыками:
Умение абстрагироваться, то есть отрекаться от ряда свойств объектов, чтобы выделять верхнеуровневые модули или абстракции, создавать базовые абстрактные классы и интерфейсы, контракты, политики. Для тренировки попробуйте абстрагироваться от дверного проема и нарисовать получившуюся иерархию ?
Умение декомпозировать систему на подсистемы, например, на горизонтальные слои. Но никто не говорил, что нельзя расслаивать на вертикальные слои, как в случае вертикальной архитектуры. То есть необходимо уметь декомпозировать по разным осям и отличать наследование от композиции.
Для GRASP с его серыми кардиналами потребуется:
Понимать, что такое модульность, то есть уметь определять тонкую грань между высоким и низким сцеплением, высокой и низкой связанностью. Иначе можно расслоить так, что будет High Coupling и Low Cohesion ?
Умение раздавать ответственность классам или модулям. То есть, когда вы разделяете целое на части, важно понимать, за что теперь отвечает каждая часть, а самое главное — зачем такое распределение обязанностей.
Уметь выявлять точки возможных вариаций и неустойчивости, распределять обязанности так, чтобы обеспечить устойчивый интерфейс.
Рекомендации по встраиванию принципов в разработку
Итак, принципы не так страшны и не так многогранны. По сути надо помнить несколько проблем и бороться с ними известными несколькими способами.
Но как же выстроить культуру разработки так, чтобы принципы были применимы? Давайте рассмотрим советы, которые охватывают некоторые процессы, связанные с разработкой.
Развитие навыков по применению принципов
Часто сталкиваюсь с тем, что разработчики знают принципы теоретически, даже могут применять их при конкретном запросе, но не делают этого в повседневной работе. Как же развивать привычку применять принципы разработки? Встраивать правила в разные этапы разработки, применять чек-листы и стратегические картонки (ну или стикеры).
Когда пригодятся принципы?
Артефакты/Документы проекта. Это может быть расширения старых артефактов или создание новых. Вы можете встроить принципы разработки в документации.
Инструкции команде по применению принципов
Для команды разработки — в code-style проекта. Если уже есть такой файл как editorconfig, то наряду с ним можно разработать общий гайд, который упростить понимание принципов и их применение.
Для команды разработки — чек-лист до отправки на ревью. Этот интересный чек-лист позволит сократить количество задач на доработку от ревьюера. Кроме привычных вещей при разработке сюда надо включить требования принципов.
Тимлидам — чек-лист проверки, в том числе на соответствие принципам. Аналогичный чек-лист команды разработки, но уже для тимлида/техлида/старшего разработчика, которые благодаря утвержденному списку правил не пропустят изменения противоречащие принципам разработки. Конечно, этот и предыдущий чек-листы должны быть синхронизированы, иначе будет обратный эффект. Плюшка для тех, кто дочитал: прикрепляем пример такого чек-листа. Но в чистом виде его применять не стоит! Это слишком подробный список, поэтому предварительно сократите его, иначе процесс отправки на ревью и процесс самого ревью сильно затянется ?.
Процессы
PM+Тимлидам: налаживание процессов, проведение небольших встреч, транслирование культуры разработки с учетом принципов разработки. Например, для онбординга можно подготовить видеоинструкцию, в которой помимо всего прочего будет рассказано про чек-лист с требованиями принципов.
Помните самое главное — не стоит внедрять изменения в ваши устоявшиеся процессы резко и несогласованно. Обязательно нужно каждый этап обсуждать с командой. Возможно, придется найти единомышленников, организовать курс, провести ряд встреч, услышать разные мнения, скорректировать план действий и пр.
И не забываем про ресурсы, целесообразность для бизнеса и все такое.
Кстати про популярность принципов. Мы провели внутренний опрос в направлении Backend в SimbirSoft и получили такую статистику по применению указанных принципов в разработке:
То есть тройка самых популярных принципов: DRY, SOLID и KISS.
Конечно в ваших командах, тем более в других направлениях разработки, возможны другие результаты. Но кажется, что большинство использует плюс минус несколько основных принципов.
За GRASP, кстати, обидно. Принципы GRASP были до SOLID, содержат 9 важных принципов, которые так или иначе используются в SOLID и паттернах GoF.
Нюансики!
А вот если вы решили применять принципы по-академически, без всяких упрощений, в чистом виде, то будьте добры изучить матчасть. То есть надо знать всякие тонкости принципов.
Ладно, чтобы упростить вам задачу, расскажу, так и быть.
Обсудим тонкости популярных (по крайней мере, по нашей статистике) принципов.
Подсмотрено в SOLID
Стоит отметить несколько важных замечаний и наблюдений относительно принципов SOLID:
Под классами можно понимать объединение функций и данных, то есть SOLID применимы не только к ООП!
Средний уровень принципов SOLID означает, что они применяются на уровне, лежащем непосредственно над уровнем программного кода, и помогают определять программные структуры, используемые в модулях и компонентах. То есть не про отдельную функцию.
Одно из главных: Мартин в «Чистой Архитектуре» описал эволюцию определения принципа SRP и итоговым является «Модуль должен отвечать за одного и только за одного актора.» Согласитесь, это не то же самое, что многие говорят «модуль должен отвечать за что-то одно». За что-то одно отвечает функция, поэтому функции можем делить на более мелкие — это логично. Но это более низкий уровень, а SOLID — средний уровень. Тут важно, что мы разрабатываем системы для пользователей (акторов), и желательно, чтобы только один актор влиял на тот или иной модуль. И не доверяйте всем источникам в чистом виде, проверяйте первоисточник и мнение автора, которое, кстати говоря, эволюционирует. Таким образом, модуль или класс могут выполнять несколько обязанностей, если это влияет только на одного актора. Конечно может случиться, что думая о неправильном определении, вы разработаете модуль, соответствующий корректному определению. Что ж, вы — везунчик. Но в следующий раз может не повезти. Ну и как уже отмечали ранее, про единственную ответственность скорее принцип High Cohesion.
Про OCP — еще раз обратим внимание, что тут кроме приемов декомпозиции и абстрагирования есть приемы связанные с возможностями или синтаксическим сахаром конкретного языка программирования или платформы. Например, методы расширения в Kotlin, C#, Groovy или Scala, которые позволяют добавлять новый функционал без наследования. С некоторой натяжкой можно еще вспомнить частичные классы и частичные методы в C#. Они позволяют добавить функционал в некотором внешнем файле, не затрагивая код в исходном файле. Но при этом есть ряд тонкостей. Во-первых, класс в исходном файле заранее должен содержать ключевое слово partial, а во-вторых, частичные классы все равно компилируются в один итоговый класс. И тут можно открывать холивар на тему расширения функционала посредством частичных классов и частичных методов в контексте OCP. Ну, а мы пойдем дальше ?
LSP — это не просто про то, как следует наследовать классы. Еще он про нарушение совместимости интерфейсов служб и проектирование согласно контракту. Тут необходимо вспомнить про 3 основных утверждения: предусловия, постусловия и инварианты. Также в контракт можно включить возможные типы входных данных и их значение, типы возвращаемых данных и их значение, условия возникновения исключений, их типы и значения, присутствие побочного эффекта метода и гарантии производительности.
Нетривиальный DRY
Вот тут очень интересно.
Начнем с того, что многие понимают, что с увеличением сложности программы дорожает и ее поддержка. А сложность возникает, в том числе, по мере роста степени связанности объектов, то есть в результате нарушения принципа Low Coupling.
Например, вам требуется изменить или полностью переписать объект, который связан различными отношениями с 10 другими объектами, в том числе потому что кто-то решил переиспользовать ваш объект по максимуму (ну чтобы не дублировать уже написанный код).
И теперь это ваша головная боль — изучить 10 других объектов, понять как повлияет изменения вашего объекта на эти 10. А если бы такой зависимости не было или, скажем, зависимость была бы от 1-2 классов, то проблем было бы гораздо меньше.
Так вот, DRY в контексте DDD и микросервисов вообще не особо работает. Возможно в то время не думали об ограниченных контекстах из DDD, где одна и та же сущность может по-разному называться и гулять по разным контекстам с разным набором полей.
Сущности в DDD создаются с учетом паттерна Bounded Context. Тут основная задача — подобрать поддомены и границы между ними так, чтобы они были максимально независимы друг от друга. Это позволяет вносить изменения в код не опасаясь, что где-то в другом месте, что-то сломается, а тимлидам легче распараллелить работу команд.
Например, если у вас 10 вариантов пользователей (User, Manager, Consumer и т.д.) и в одном случае нужны одни поля, а в другом — другие, а id у них общий, то они должны быть представлены разными сущностями в разных контекстах. И нет необходимости создавать большой объект, который придется пересылать по приложению и не дай бог по сети между сервисами.
Про DRY лучше сказать следующее: просто переиспользуйте код в рамках одного класса, а если это возможно, то и в рамках одной сборки.
И не надо разработчика заставлять все на свете объединить и переиспользовать.
Кстати, в этой статье мы уже делали обзор механизмов повторного использования кода.
Ну и немного про то, когда применять DRY, если уж собрались его применять? Тут логично:
не стоит переиспользовать то, что не потребуется в будущем;
не стоит переиспользовать что-то сложное, если его можно упростить.
Поэтому, мне нравится такая иллюстрация, в которой DRY применяем в конце, после YAGNI и KISS:
Не такой уж Big Design
Принцип BDUF, в соответствии с которым дизайн должен быть завершен до разработки программы, часто ассоциируется с водопадной моделью. В свою очередь, водопадная модель противоречит современным гибким методологиями, так как вернуться на предыдущий этап анализа уже нельзя, и тратится очень много времени на проектирование.
Но есть альтернатива: делать водопадики много раз ?, создавать наброски проекта, которые потом еще поправим 100 раз— это принцип RDUF (Rough Design Up Front — грубый/черновой дизайн прежде всего). То есть, как по скраму, мы не отбрасывает вообще проектирование, мы просто делаем проектирование на каждой итерации, инкрементно.
Summarize (Итоги, Резюме)
Вот и подошел к концу этот рассказ и я бы хотел закончить словами: «Принципы разработки — это не панацея от всех болезней. Но их грамотное встраивание в ваши процессы программной разработки на разных этапах позволит повысить качество программных продуктов, не допустить серьезных ошибок и пр.»
Но не все так красиво и просто.
С одной стороны, мы с вами узнали, что можно сгруппировать большинство (но не все) принципов в четыре группы: Не усложняй, Не оставляй лишнего, Упрости зависимости и Заложи гибкость. Также увидели, что такие группы принципов, как SOLID и GRASP, имеют много связей внутри и между собой, и их использование сводится до нескольких приемов. В итоге можно прокачивать навыки по применению намного меньшего числа принципов.
Основные полезные приемы: Декомпозиция и Абстрагирование.
Основные полезные знания: Модульность.
Основные полезные навыки: Умение выявлять точки возможных вариаций и умение распределять ответственность между классам/модулям/компонентами.
С другой стороны, попадаются принципы, которые достаточно серьезно выделяются на фоне остальных. Например, нельзя сказать, что LSP просто про абстрагирование, так как с ним связан целый ряд правил проектирования по контракту. Благо таких исключений немного.
Получается, что либо можно упростить все принципы до нескольких проблем и, исходя из большого опыта, бороться с этими проблемами, интуитивно применяя те или решения, либо использовать несколько основных принципов, но при этом изучить все тонкости и не упустить важных нюансов.
И еще немного дегтя для принципов. Мне кажется, что слишком много слов написано в каждом из принципов, но нет нормального понимания, а что же скрывается за «магией» этих правил? Может быть не так уж и глубока кроличья нора? Надеюсь, наш анализ немного раскрыл глаза на проблемы.
References
Дейкстра, Э. Почему программное обеспечение такое дорогое? EWD648. // Эдсгер Дейкстра: избранные статьи. Web: https://oberoncore.ru/_media/library/dijkstra.pdf
Larman, Craig. Applying UML and Patterns
Robert C. Martin Clean Architecture
Хорошим примером порядка применения принципов является цепочка
https://habr.com/ru/companies/itelma/articles/546372/
Связь coupling и cohesion
https://habr.com/ru/articles/568216/
List of software development philosophies
https://habr.com/ru/companies/tinkoff/articles/490738/
https://ru.wikipedia.org/wiki/Принцип_подстановки_Лисков
https://web.archive.org/web/20151128004108/http://www.objectmentor.com/resources/articles/lsp.pdf
https://ru.wikipedia.org/wiki/Контрактное_программирование
https://en.wikipedia.org/wiki/Dependency_inversion_principle
Спасибо за внимание!
Больше авторских материалов для backend-разработчиков от моих коллег читайте в соцсетях SimbirSoft – ВКонтакте и Telegram.
Комментарии (4)
ManGegenMann
18.04.2024 10:26+1Таким образом, мы как разработчики информационных систем часто сталкиваемся с требованиями, которые меняются на ходу
Я бы предпочел не работать там где требования меняются на ходу. Такого не бывает, экраные формы не требование. Когда заказчик постоянно меняет требования это повод собраться вместе с ним и провести аналитику, либо у него шизофрения если он сегодня хочет одно, а спустя месяц уже другое совершенно.
Я встречался с таким только когда требования ставил один человек, а приемку и дальнейшее проводил другой и у него другое мнение. В этом кстати состоит роль менеджера и аналитика, отделить проект от субъективных хотелок разных людей со стороны заказчика.
С изменением требований на ходу или с их "уточнением" очень помогает составление ТЗ. Я давече пол года туда обратно менял функционал и в итоге просто объявил забастовку пока мне не принесут чёткое ТЗ как это должно работать. Постоянно переделывать одно и тоже я просто отказался.
nv13
Очень крутой обзор) Добавлю ещё одну абревиатуру
BFI - Brut Force and Ignorence - метод грубой силы и невежества, заключающийся в экстенсивном решении потенциальных и актуальных проблем. Редко применяется программистами по собственной инициативе, преимущественно крутыми менеджерами с большими ресурсами. Позволяет исправить ошибки применения или неприменения других принципов программирования и методологиям Аджайл))
ivvi
Вы когда пишете что-то на иностранном языке и не уверены в своих силах, проверяйте хотя бы по словарю, а то у вас в обоих словах по ошибке:
"brute" пишется с "e" на конце
"ignorance" пишется через "a" в середине слова