февраль 20, 2021 · программирование принципы SOLID проектирование паттерны

Принципы SOLID для желторотиков

Рано или поздно, все мальчики и девочки, которые пишут программки (или даже программы), задумываются об их архитектуре. И все они, так или иначе находят в поисковике загадочную фразу "паттерны проектирования", которые как раз неразрывно связаны с проектированием архитектуры программ. Вот мне бы и хотелось как-то структурировать и описать пять принципов проектирования, изложенных в книге Р. Мартина "Agile Software Development PNIE".

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

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

Итак, знакомимся. SOLID.

S - Single Responcibility Principle

Принцип единой ответственности

У класса должен быть только один мотив для изменения.

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

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

Пример. Пусть есть класс "Фирма", предназначенный для управления фирмой :) Он имеет, например, такую структуру.

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

Так гораздо удобнее и логичнее.

O - Open/Closed Principle

Принцип открытости/закрытости

Расширяйте классы, но не изменяйте их код.

Классы должны быть открыты для расширения, но закрыты для изменения.

Класс открыт, если возможно расширить его набор операции или полей, создав его подкласс.
Класс закрыт, если он доступен для использования другими классами. Т.е. его интерфейс УЖЕ определен окончательно и не изменится в будущем.

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

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

L - Liskov Substitution Principle

Принцип подстановки Лисков

Подклассы должны дополнять, а не замещать поведение базового класса.

Принцип назван в честь Барбары Лисков, американского ученого, которая впервые его сформулировала.

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

Принцип включает в себя целый рад требований к переопределенным методам подклассов.

Типы параметров метода подкласса должны совпадать или быть более абстрактными, чем типы параметров базового метода.

Тут все логично и очевидно. Есть класс, содержащий метод drive(Car car), который заставляет ехать машинку. Создали подкласс и переопределили этот метод, который умеет вообще запускать любое транспортное средство drive(Vehicle v). Если использовать этот подкласс, ничего не сломается.

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

Все тоже самое, как в предыдущем, только наоборот. Базовый метод getVehicle() : Vehicle. Переопределяем его в подклассе как getVehicle(): Bus и все отлично работает.

Метод не должен выбрасывать исключения, которые не свойственны базовому методу.

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

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

Метод не должен ужесточать предусловия.

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

Метод не должен ослаблять постусловия

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

Инварианты класса должны остаться без изменений.

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

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

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

I - Interface Segregation Principle

Принцип разделения интерфейса

Клиенты не должны зависеть от методов, которые они не используют.

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

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

D - Dependency Inversion Principle

Принцип инверсии зависимостей

Классы верхних уровней не должны зависеть от классов нижних уровней. Оба должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

При проектировании обычно получается выделить два типа классов:

Данный принцип подразумевает такое направление проектирования:

  1. Необходим интерфейс низкоуровневых операций.
  2. Проектируется бизнес-логика, применяющая спроектированный низкоуровневый интерфейс.
  3. Реализуются низкоуровневые классы.

Тоже достаточно очевидный принцип, не требующий каких-то особых пояснений.

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