- Принципы SOLID в картинках
- ООП, «святая троица» и SOLID: некоторый минимум знаний о них
- Чистая архитектура: руководство для начинающих
- Кто подставил Барбару Лисков, или кто кого SOLID
Началось с Дейкстры, который доказал, что любой алгоритм можно выразить через три способа выбора следующей команды: линейное выполнение (по порядку), ветвление по условию, выполнение цикла пока выполняется условие. С помощью этих трёх соединений можно сконструировать любой алгоритм. Более того, было рекомендовано писать программы, ограничиваясь линейным расположением команд друг за другом, ветвлением и циклами. Это было названо "структурным программированием".
Структурное программирование в нем нет классов, объектов, вместо классов используются структуры, функции. В ООП есть классы, объекты, методы, конструкторы, деструкторы. Но также можно использовать структуры, и функции.
ООП С++ выросло на базе хорошего структурированого программирования на С. Как ответ на возрастающую сложность разработки ПО. Да, структуры предшествовали классам - у них нет наследования, инкапсуляции и полиморфизма, даже методов нет.
Принципы SOLID подходят для проектов, разрабатываемых по гибким методологиям, ведь Роберт Мартин — один из авторов Agile Manifesto.
Источником принципов S.O.L.I.D принято считать книгу Роберта Мартина (Uncle Bob) rus: Чистая архитектура | eng: The Clean Architecture 2012, где собрал пять принципов проектирования ООП в красивую абревиатуру.
P.S.: Видео сайта cleancoders.com, где дядюшка Боб в шутливой форме на пальцах рассказывает, что же именно означают принципы и как их применять.
“ЗАДАЧА АРХИТЕКТУРЫ ПО — МИНИМИЗАЦИЯ ЧЕЛОВЕЧЕСКИХ РЕСУРСОВ ПРИ РАЗРАБОТКЕ И ПОСЛЕДУЮЩЕМ СОПРОВОЖДЕНИИ СИСТЕМЫ”, — РОБЕРТ С. МАРТИН.
Иллюстрация чистой архитектуры, предложенная Робертом Мартином:
Схематично стек архитектуры представлен четырьмя уровнями: синий, зеленый, красный и желтый.
Каждая окружность соответствует различным составляющим ПО. Внешний уровень ПО является самым низким. К центру уровень повышается. В целом, чем ближе слой к центру, тем он менее подвержен изменениям.
SOLID и ООП неразрывно связаны. Так получилось, что именно в ООП языках появилась удобная и безопасная поддержка динамического полиморфизма. Фактически, в контексте SOLID под ООП понимается именно динамический полиморфизм.
Полиморфизм дает возможность для разных типов использовать один код. Полиморфизм можно грубо разделить на динамический и статический:
-
Динамический полиморфизм — это про абстрактные классы, интерфейсы, утиную типизацию, т.е. только в рантайме будет понятно, с каким типом будет работать наш код.
Утиная типизация (в swift): Если что-то выглядит как утка, плавает как утка и крякает как утка, то, скорее всего, это утка
Пример: Строка и массив — это разные типы данных, но они ведут себя одинаково. А если строка выводится как массив, заполняется как массив и меняется как массив, то с практической точки зрения это и есть массив и с ним можно работать как с массивом. Это и есть утиная типизация, когда нам неважно, что там на самом деле — важно, как оно себя ведёт и как с ним работать.
-
Статический полиморфизм — это в основном про шаблоны (genererics). Когда уже на этапе компиляции из одного шаблонного кода генерируется код специфичный для каждого используемого типа.
Open
Принцип единственной ответственности можно считать этакой инструкцией к инкапсуляции. Давайте на секундочку вернёмся к её определению.
Упаковка данных и функций в единый компонент
Каждый компонент должен быть занят чем-то одним. Выполнять одну задачу. Но тогда встаёт в полный рост вопрос «что считать одной задачей?». Поэтому на практике появился иной подход (согласно Роберту Мартину).
У программной сущности должна быть только одна причина для изменения
На заре развития веба разметка HTML определяла структуру документа и его внешний вид. Возможно, у вас тут при виде союза «и» уже задёргался глаз. К сожалению, для разработчиков HTML в начале 90-х это было не очевидно. Тогда вообще мало кто представлял, во что это всё выльется. В какой-то момент стало ясно, что изменения структуры документа и изменения внешнего вида документа очень разные причины для изменения. Настолько разные, что для задания внешнего вида HTML-документов создали отдельный язык. И назвали его CSS.
Разделение ответственности в самой радикальной форме.
Open
Принцип открытости/закрытости отличается от остальной пятёрки тем, что не объясняет, как использовать инкапсуляцию и полиморфизм отдельно. Он накрывает все три кита сразу. Смотрите сами:
Программные сущности должны быть открыты для расширения и закрыты для модификации
Если хотите, чтобы класс выполнял больше операций, то идеальный вариант – не заменять старые на новые, а добавлять новые к уже существующим.
Open
Формулировка Роберта Мартина: "Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом"
❗ Механизмы, помогающие соблюдать принцип LSP: протоколы, наследование и строгая система типов.
Если кто-то ведет себя как утка, то это, безусловно, птица» — так объясняет принцип Лисков один из пользователей форума StackOverflow. Есть похожий программистский мем: «Если кто-то выглядит как утка и плавает как утка, но работает на батарейках, значит, у вас плохо с абстракцией данных».
Без соответствия этому принципу наследование становится хуже, чем бесполезным, оно наносит вред. Зачем мы используем наследование?
- Переиспользование кода
- Мощь полиморфизма
- Чтобы нашу программу было проще понимать и читать
Если у вас имеется класс и вы создаете на его базе другой класс, исходный класс становится родителем, а новый – его потомком. Класс-потомок должен производить такие же операции, как и класс-родитель. Это называется наследственностью.
Необходимо, чтобы класс-потомок был способен обрабатывать те же запросы, что и родитель, и выдавать тот же результат. Или же результат может отличаться, но при этом относиться к тому же типу. На картинке это показано так: класс-родитель подаёт кофе (в любых видах), значит, для класса-потомка приемлемо подавать капучино (разновидность кофе), но неприемлемо подавать воду.
Open
Класс должен производить только те операции, которые необходимы для осуществления его функций. Все другие действия следует либо удалить совсем, либо переместить, если есть вероятность, что они понадобятся другому классу в будущем.
Программные сущности не должны зависеть от частей интерфейса, которые они не используют (и знать о них тоже не должны).
Грубо говоря, давайте тем, кто пользуется вашей программной сущностью всё необходимое, но не более того.
Dependency Inversion Principle — принцип инверсии зависимости
Open
Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.
Для начала объясню термины, которые здесь применяются, простыми словами.
Чем ближе модуль к вводу/выводу, тем ниже уровень модуля. Т.е. модули, работающие с BD, интерфейсом пользователя, низкого уровня. А модули, реализующие бизнес-логику — высокого уровня.
- Модули (или классы) верхнего уровня = классы, которые выполняют операцию при помощи инструмента
- Модули (или классы) нижнего уровня = инструменты, которые нужны для выполнения операций (Н/р: mapper)
- Абстракции – представляют интерфейс, соединяющий два класса
- Детали = специфические характеристики работы инструмента
Юнит-тесты. Вы просто пишите код, который убеждается, что ваш кусочек кода работает правильно. Это лучше, чем убеждаться в этом в процессе пошаговой отладки, гадая, правильно ли работает этот класс, или эту противную багу вызывает кто-то другой. Представьте, что у вас есть проект, где юнит-тестов до этого не было. В один прекрасный день вы решили, что больше так жить нельзя, и что с этим надо что-то делать.
«Вот! Вот с этого класса начну.» — решаете вы и начинаете радостно продумывать тест-кейсы для вашего ImportantClass. Когда приходит пора писать тесты, выясняется, что вам для тестирования класса и его методов нужен рабочий объект этого класса. Дальше выясняется, что классу ImportantClass требуется для работы экземпляр класса VeryImportantClass, которому требуется ещё с пять объектов разных классов, из разных частей приложения, и экземпляр класса EvenMoreImportantClass, которому для работы вообще необходимо соединение с базой данных, доступ к файлам конфигурации и жабья лапка с кровью девственницы. Быстро становится ясно, что следом за тем, что мы хотим протестировать поднимается очень много вещей, которые мы тестировать не хотим. По крайней мере, не одним тестом. И процедура начинает казаться столь противной, что мы тяжко вздыхаем и возвращаемся к прошлой жизни.
Проблема тут в зависимости ImportantClass от VeryImportantClass и EvenMoreImportantClass. И решить нашу проблему с нетестируемостью ImportantClass можно через инверсию этой зависимости. Это делается с помощью абстракций, которые вклиниваются между вещами, которые надо разделить. Например, можно создать интерфейсы IVeryImportantClass и IEvenMoreImportantClass, где будут только методы необходимые ImportantClass.
Таким образом зависимость между ImportantClass и VeryImportantClass исчезает. И мы теперь можем тестировать наш ImportantClass в «сферическом ваакуме», дав ему вместо полноценных реализаций IVeryImportantClass тестовые заглушки.
Согласно данному принципу, класс не должен соединяться с инструментом, который применяет для выполнения операции (Н/р: mapper). Вместо этого он должен быть соединён с интерфейсом, который поможет установить связь между инструментом и классом.
2.4.4.4 YAGNI Theme | Back To iOSWiki Contents | 2.4.5 Tests Theme Folder