Перечитывая «Чистую архитектуру» (3): принципы SOLID
При построении программных модулей преследуют три основные цели:
- Сделать ПО устойчивым к изменениям.
- Сделать ПО более понятным.
- Создавать компоненты, пригодные для повторного использования в разных программных системах.
Принципы SOLID прежде всего объясняют, как объединять данные и функции в классы, а затем связывать классы в программу. Они направляют проектирование модулей; на уровне архитектуры действуют другие принципы.
SOLID включает принцип единственной ответственности SRP, принцип открытости/закрытости OCP, принцип подстановки Лисков LSP, принцип разделения интерфейсов ISP и принцип инверсии зависимостей DIP.
Принцип единственной ответственности — SRP
SRP определяет для каждого модуля одну ответственность: должна существовать только одна причина для его изменения. Каждый программный модуль должен отвечать только перед одной категорией действующих лиц.
На уровне компонентов SRP называется принципом общей закрытости — CCP.
Что произойдёт, если проектировать модуль без соблюдения SRP? Рассмотрим пример.
Пусть финансовый отдел, кадровая служба и отдел разработки совместно зависят от программы расчёта зарплаты и рабочего времени. В программе три функции:
- CalculateSalary: расчёт зарплаты.
- CalculateTime: расчёт рабочего времени.
- Save: сохранение информации.
Три отдела спокойно пользуются программой. Однажды у финансового отдела меняется порядок расчёта зарплаты, и его специалисты изменяют функцию CalculateSalary. После этого происходит неожиданное: одновременно меняется зарплата у кадровой службы и отдела разработки…
Принцип открытости/закрытости — OCP
«Приветствовать добавление, сопротивляться изменению!»
OCP утверждает: чтобы программную систему было легче менять, её устройство должно позволять изменять поведение добавлением нового кода, а не только правкой существующего. Хорошо спроектированное ПО легко расширяется и сопротивляется изменению.
OCP относится не только к классам и модулям, но и к компонентам. Он вырос из идеи независимости от устройств. Например, инверсия зависимостей позволяет оформить устройства IO как плагины, чтобы легко добавлять новые, не изменяя прежние.
OCP реализуют, разделяя систему на компоненты и организуя их зависимости в иерархию, при которой изменения низкоуровневых компонентов не затрагивают высокоуровневые.
Принцип подстановки Лисков — LSP
LSP утверждает: чтобы строить систему из взаимозаменяемых компонентов, все эти компоненты должны соблюдать один и тот же контракт.
Суть LSP — взаимозаменяемость: если для каждого объекта o1 типа S существует объект o2 типа T, причём поведение программы P, работающей с T, не меняется при замене o2 на o1, то S можно назвать подтипом T.
LSP можно и нужно применять на архитектурном уровне: нарушение взаимозаменяемости заставляет добавлять множество сложных компенсирующих механизмов.
Принцип разделения интерфейсов — ISP
ISP предписывает избегать ненужных зависимостей. На любом уровне проектирования зависимость от того, что на самом деле не требуется, может привести к неожиданным проблемам.
Инверсия зависимостей — DIP
DIP утверждает: код высокоуровневых правил не должен зависеть от кода низкоуровневых деталей. Напротив, низкоуровневая реализация должна зависеть от высокоуровневых правил.
Для простого объяснения инверсии зависимостей нужно различать поток управления и зависимости исходного кода. Пусть управление идёт от компонента A к B. В таком устройстве B должен знать реализацию A, а A должен подключать модуль B, поэтому зависимость кода тоже направлена от A к B. Поведение системы определяет поток управления, а он — зависимости кода, и у архитектуры не остаётся выбора. После введения интерфейса B лишь реализует его; связь с B проходит через интерфейс, а A подключает интерфейс. A больше не нужно подключать модуль B. Направления потока управления и зависимости исходного кода становятся противоположными — это и есть инверсия зависимостей.
Инверсия зависимостей даёт свободу архитектурному проектированию. Для гибкой системы зависимости исходного кода должны чаще ссылаться на абстракции — интерфейсы, абстрактные классы и подобные типы, — а не на конкретные реализации.
Из DIP следуют несколько правил написания кода:
- Чаще используйте абстрактные интерфейсы и по возможности избегайте изменчивых конкретных классов реализации.
- Не создавайте производные классы от конкретных классов реализации.
- Не переопределяйте (override) функции, содержащие конкретную реализацию.
- Избегайте в коде имён, привязанных к конкретной реализации, и других легко меняющихся сущностей.
(Конец третьей статьи)