SOLID — это акроним пяти принципов объектно-ориентированного программирования для создания понятного, масштабируемого и поддерживаемого кода. Произносится как «принципы солид».
Одна буква — один принцип. Соответственно, принципов столько же, сколько и букв— пять. Расшифровка SOLID такая:
В этой статье мы разберемся, что такое SOLID и что утверждает каждый из его пяти принципов.
Все показанные примеры кода выполнялись интерпретатором Python версии 3.10.12 на облачном сервере Timeweb Cloud под управлением операционной системы Ubuntu 22.04.
SRP (Single Responsibility Principle) — принцип единственной ответственности, утверждающий, что каждый отдельный класс должен специализироваться только на решении одной узкой задачи. Иными словами, класс несет ответственность только за один компонент приложения, реализуя его логику.
По сути, это форма «разделения труда» на уровне программного кода. В строительстве дома прораб управляет командой, дровосек рубит деревья, грузчик носит бревна, маляр красит стены, сантехник прокладывает трубы, дизайнер создает интерьер и т.д. Каждый занят своим делом и работает только в рамках своих компетенций.
В SRP все точно также. Например, RequestHandler
обрабатывает HTTP-запросы, FileStorage
управляет локальными файлами, Logger
записывает информацию, а AuthManager
проверяет права доступа.
Как говорится, «мухи отдельно, котлеты отдельно». Если у класса несколько обязанностей — их нужно разделить.
Разумеется, SRP напрямую влияет на связность (coupling) и связанность (cohesion) кода. Оба свойства похожи по звучанию, но отличаются по значению:
Связность (Coupling). Положительная характеристика, означающая логическую целостность классов относительно друг друга. Чем выше связность, тем у́же функциональность класса.
Связанность (Cohesion). Отрицательная характеристика, означающая логическую зависимость классов друг от друга. Чем выше связанность, тем сильнее функциональность одного класса переплетена с функциональностью другого класса.
SRP стремится увеличить связность, но уменьшить связанность классов. Каждый класс решает свою узкую задачу, оставаясь как можно более независимым от внешней среды — других классов. Однако все классы по прежнему могут (и должны) взаимодействовать друг с другом через интерфейсы.
vds
Объект класса, способный выполнять множество разноплановых функций, иногда называют божественным объектом — экземпляром класса, который берет на себя слишком много обязанностей, выполняя множество логически несвязанных функций, например, управление бизнес-логикой, хранение данных, работу с БД, отправку уведомлений и т.п.
Пример кода на языке Python, где нарушается SRP:
# реализация класса божественного объекта
class DataProcessorGod:
# метод загрузки данных
def load(self, file_path):
with open(file_path, 'r') as file:
return file.readlines()
# метод обработки данных
def transform(self, data):
return [line.strip().upper() for line in data]
# метод сохранения данных
def save(self, file_path, data):
with open(file_path, 'w') as file:
file.writelines("\n".join(data))
# cоздание божественного объекта
justGod = DataProcessorGod()
# обработка данных
data = justGod.load("input.txt")
processed_data = justGod.transform(data)
justGod.save("output.txt", processed_data)
Функциональность программы из этого примера можно поделить на два типа:
Соответственно, для создания более оптимального уровня абстракций, позволяющего в дальнейшем легко масштабировать программу, необходимо выделить каждой функциональности свой собственный класс.
Показанную программу лучше всего представить в виде двух специализированных классов, не знающих друг о друге:
DataManager
. Для работы с файлами.DataTransformer
. Для преобразования данных.Пример кода на языке Python, где используется SRP:
class DataManager:
def load(self, file_path):
with open(file_path, 'r') as file:
return file.readlines()
def save(self, file_path, data):
with open(file_path, 'w') as file:
file.writelines("\n".join(data))
class DataTransformer:
def transform(self, data):
return [line.strip().upper() for line in data.text]
# создание специализированных объектов
manager = DataManager()
transformer = DataTransformer()
# обработка данных
data = manager.load("input.txt")
processed_data = transformer.transform(data)
manager.save("output.txt", processed_data)
В данном случае DataManager
и DataTransformer
взаимодействуют друг с другом с помощью строк, которые передаются в качестве аргументов их методов.
В более сложной реализации мог бы существовать дополнительный класс Data
, используемый для передачи данных между разными компонентами программы:
class Data:
def __init__(self):
self.text = ""
class DataManager:
def load(self, file_path, data):
with open(file_path, 'r') as file:
data.text = file.readlines()
def save(self, file_path, data):
with open(file_path, 'w') as file:
file.writelines("\n".join(data.text))
class DataTransformer:
def transform(self, data):
data.text = [line.strip().upper() for line in data.text]
# создание специализированных объектов
manager = DataManager()
transformer = DataTransformer()
# обработка данных
data = Data()
manager.load("input.txt", data)
transformer.transform(data)
manager.save("output.txt", data)
В этом случае низкоуровневая работа с данными обернута в пользовательские классы. Такую реализацию легко масштабировать.
Например, можно добавить множество методов для работы с файлами (DataManager
) и данными (DataTransformer
), а также усложнить внутреннее представление хранимой информации (Data
).
Однозначно, SRP упрощает поддержку приложения, делает код читаемым и уменьшает зависимость между частями программы:
Повышение масштабируемости. Добавление новых функций в программу не запутывает ее логику. Класс, решающий только одну задачу, проще менять без риска сломать другие части системы.
Повторное использование. Логически целостные компоненты, реализующие логику программы, можно переиспользовать для создания нового поведения.
Упрощение тестирования. Классы с одной обязанностью легче покрывать юнит-тестами, ведь они не содержат лишней логики внутри.
Улучшение читаемости. Логически связанные функции, обернутые в один класс, выглядят понятнее. В них проще разбираться, вносить изменения и искать ошибки.
Совместная разработка. Логически разделенный код могут писать сразу несколько программистов. В этом случае каждый работает над отдельным компонентом.
Иными словами, класс должен отвечать только за одну задачу. Если в классе сосредоточено несколько обязанностей, его сложнее поддерживать без побочных эффектов для всей программы.
OCP (Open/Closed Principle) — принцип открытости/закрытости, утверждающий, что код должен быть открыт для расширения, но закрыт для модификации. Иными словами, модификация поведения программы осуществляется только добавлением новых компонентов. Новый функционал как бы наслаивается на старый.
На практике OCP реализуется через наследование, интерфейсы, абстракции и полиморфизм. Вместо изменения существующего кода добавляются новые классы и функции.
Например, вместо реализации единого класса, обрабатывающего все HTTP-запросы (RequestHandler
), можно создать один класс менеджера подключений (HTTPManager
) и несколько классов для обработки разных методов HTTP-запросов: RequestGet
, RequestPost
, RequestDelete
. При этом классы обработки запросов наследуют от базового класса обработчика — Request
.
Соответственно, для реализация новых методов обработки запросов потребуется не модификация уже имеющихся классов, а добавление новых. Например, RequestHead
, RequestPut
, RequestConnect
, RequestOptions
, RequestTrace
, RequestPatch
.
Без OCP любое изменение логики работы программы (ее поведения) потребует модификации ее компонентов.
Пример кода на языке Python, где нарушается OCP:
# единый класс обработки запросов
class RequestHandler:
def handle_request(self, method):
if method == "GET":
return "Обработка GET-запроса"
elif method == "POST":
return "Обработка POST-запроса"
elif method == "DELETE":
return "Обработка DELETE-запроса"
elif method == "PUT":
return "Обработка PUT-запроса"
else:
return "Метод не поддерживается"
# обработка запросов
handler = RequestHandler()
print(handler.handle_request("GET")) # Обработка GET-запроса
print(handler.handle_request("POST")) # Обработка POST-запроса
print(handler.handle_request("PATCH")) # Метод не поддерживается
Такая реализация нарушает OCP. При добавлении новых методов придется модифицировать класс RequestHandler
, добавляя новые условия обработки elif
. Чем сложнее будет становиться программа с такой архитектурой, тем тяжелее ее будет поддерживать и масштабировать.
Обработчик запросов из примера выше можно разделить на несколько классов таким образом, чтобы последующее изменение поведения программы не требовало модификации уже созданных классов.
Абстрактный пример кода на языке Python, где используется OCP:
from abc import ABC, abstractmethod
# базовый класс обработчика запросов
class Request(ABC):
@abstractmethod
def handle(self):
pass
# классы для обработки разных HTTP-методов
class RequestGet(Request):
def handle(self):
return "Обработка GET-запроса"
class RequestPost(Request):
def handle(self):
return "Обработка POST-запроса"
class RequestDelete(Request):
def handle(self):
return "Обработка DELETE-запроса"
class RequestHead(Request):
def handle(self):
return "Обработка HEAD-запроса"
class RequestPut(Request):
def handle(self):
return "Обработка PUT-запроса"
class RequestConnect(Request):
def handle(self):
return "Обработка CONNECT-запроса"
class RequestOptions(Request):
def handle(self):
return "Обработка OPTIONS-запроса"
class RequestTrace(Request):
def handle(self):
return "Обработка TRACE-запроса"
class RequestPatch(Request):
def handle(self):
return "Обработка PATCH-запроса"
# класс менеджера подключений
class HTTPManager:
def __init__(self):
self.handlers = {}
def register_handler(self, method: str, handler: Request):
self.handlers[method.upper()] = handler
def handle_request(self, method: str):
handler = self.handlers.get(method.upper())
if handler:
return handler.handle()
return "Метод не поддерживается"
# регистрация обработчиков в менеджере
http_manager = HTTPManager()
http_manager.register_handler("GET", RequestGet())
http_manager.register_handler("POST", RequestPost())
http_manager.register_handler("DELETE", RequestDelete())
http_manager.register_handler("PUT", RequestPut())
# обработка запросов
print(http_manager.handle_request("GET"))
print(http_manager.handle_request("POST"))
print(http_manager.handle_request("PUT"))
print(http_manager.handle_request("TRACE"))
В данном случае базовый класс Request
реализуется с помощью ABC
и @abstractmethod
:
ABC
(Abstract Base Class). Это базовый класс в Python, от которого нельзя создать экземпляр напрямую. Он нужен исключительно для определения подклассов.@abstractmethod
. Декоратор, обозначающий метод как абстрактный. То есть каждый подкласс обязан реализовать этот метод, в противном случае создать его экземпляр будет невозможно.Несмотря на то, что код программы стал длиннее и сложнее, его поддержка значительно упростилась. Реализация обработчика теперь выглядит структурнее и понятнее.
Следование OCP наделяет процесс разработки приложения некоторыми достоинствами:
Понятная расширяемость. Логика программы может быть легко дополнена новым функционалом. При этом уже реализованные компоненты остаются неизменными.
Уменьшение ошибок. Добавление новых компонентов безопаснее изменения уже существующих. Риск сломать уже работающую программу невелик, а ошибки после дополнения вероятно исходят от новых компонентов.
На самом деле OCP можно сравнить с SRP по способности изолировать реализацию отдельных классов друг от друга. Разница лишь в том, что SRP работает по горизонтали, OCP — по вертикали.
Например, в случае с SRP, класс Request
логически отделен от класса Handler
по горизонтали. Это SRP. В тоже время классы RequestGet
и RequestPost
, конкретизирующие метод запроса, логически отделены от класса Request
по вертикали, хотя и являются его наследниками. Это OCP.
Все три класса (Request
, RequestGet
, RequestPost
) полностью субъектны и автономны — они могут использоваться по отдельности. Ровно как и Handler
. Хотя, конечно, это вопрос теоретических интерпретаций.
Таким образом, благодаря OCP можно создавать новые компоненты программы на основе старых, оставляя и те и другие полностью самостоятельными сущностями.
LSP (Liskov Substitution Principle) — принцип подстановки Барбары Лисков, утверждающий, что объекты в программе должны быть заменяемы их наследниками без изменения корректности программы. Иными словами, классы-наследники должны полностью сохранять поведение своих родителей.
Барбара Лисков — это американская ученая в области информатики, специализирующаяся на абстракциях данных.
Барбара Лисков (источник: Wikipedia)
Например, есть класс Vehicle
. От него наследуется класс Car
и Helicopter
. От Car
наследуется Tesla
, а от Helicopter
— Apache. Таким образом, каждый последующий класс (наследник) добавляет новые свойства предыдущему (родитель).
Транспортные средства могут заводить и глушить двигатель. Машины способны ездить. Вертолеты — летать. При этом модель машины Tesla
способна использовать автопилот, а Apache
— радиовещать.
Получается своего рода иерархия способностей:
Чем конкретнее класс транспортного средства, тем большим количеством умений он обладает. Но базовые способности тоже сохраняются.
Пример кода на языке Python, где нарушается LSP:
class Vehicle:
def __init__(self):
self.x = 0
self.y = 0
self.z = 0
self.engine = False
def on(self):
if self.engine:
self.engine = True
return "Двигатель заведен"
else:
return "Двигатель уже заведен"
def off(self):
if self.engine:
self.engine = False
return "Двигатель заглушен"
else:
return "Двигатель уже заглушен"
def move(self):
if self.engine:
self.x += 10
self.y += 10
self.x += 10
return "Техника перемещена"
else:
return "Двигатель не заведен"
# классы различной техники
class Car(Vehicle):
def move(self):
if self.engine:
self.x -= 1
self.y -= 1
return "Машина проехала"
else:
return "Двигатель не заведен"
class Helicopter(Vehicle):
def move(self):
if self.engine:
self.x += 1
self.y += 1
self.z += 1
return "Вертолет пролетел"
else:
return "Двигатель не заведен"
def radio(self):
return "Пшш...пшш...пшш..."
В данном случае родительский класс Vehicle
имеет метод move()
, обозначающий перемещение техники. Наследующие классы переопределяют базовое поведение Vehicle
, задавая собственный способ перемещения.
Следуя LSP, логично предположить, что Car
и Helicopter
должны сохранять способность перемещения, добавляя к ней уникальные типы движения своим ходом — поездку и полет.
Пример кода на языке Python, где используется LSP:
# базовый класс техники
class Vehicle:
def __init__(self):
self.x = 0
self.y = 0
self.z = 0
self.engine = False
def on(self):
if self.engine:
self.engine = True
return "Двигатель заведен"
else:
return "Двигатель уже заведен"
def off(self):
if self.engine:
self.engine = False
return "Двигатель заглушен"
else:
return "Двигатель уже заглушен"
def move(self):
if self.engine:
self.x += 10
self.y += 10
self.x += 10
return "Техника перемещена"
else:
return "Двигатель не заведен"
# классы различной техники
class Car(Vehicle):
def ride(self):
if self.engine:
self.x += 1
self.y += 1
return "Машина проехала"
else:
return "Двигатель не заведен"
class Helicopter(Vehicle):
def fly(self):
if self.engine:
self.x += 1
self.y += 1
self.z += 1
return "Вертолет пролетел"
else:
return "Двигатель не заведен"
def radio(self):
return "Пшш...пшш...пшш..."
class Tesla(Car):
def __init__(self):
super().__init__()
self.autopilot = False
def switch(self):
if self.autopilot:
self.autopilot = False
return "Автопилот выключен"
else:
self.autopilot = True
return "Автопилот включен"
class Apache(Helicopter):
def __init__(self):
super().__init__()
self.frequency = 103.4
def radio(self):
if self.frequency != 0:
return "Пшш...пшш...Прием, как слышно? [" + str(self.frequency) +" GHz]"
else:
return "Кажется, радио не работает..."
В данном случае Car
и Helicopter
, ровно как и производные от них Tesla
и Apache
, сохранят оригинальное поведение Vehicle
. То есть каждый наследник добавляет новое поведение к родительскому классу, но при этом сохраняет его собственное.
Код, следующий LSP, работает с родительскими классами так же, как и с их наследниками. Таким образом можно реализовывать интерфейсы, способные взаимодействовать с объектами разных типов, но с общими свойствами.
ISP (Interface Segregation Principle) — принцип разделения интерфейса, утверждающий, что классы программы не должны зависеть от тех методов, которые он не используют.
Это означает, что если каждый класс должен содержать только необходимые ему методы. Он не должен «тащить» с собой лишний «груз». Поэтому вместо одного большого интерфейса лучше создать несколько маленьких специализированных интерфейсов.
Во многом ISP имеет черты SRP и LSP, однако отличается от них.
Пример кода на языке Python, который игнорирует ISP:
# базовое транспортное средство
class Vehicle:
def __init__(self):
self.hp = 100
self.power = 0
self.wheels = 0
self.frequency = 103.4
def ride(self):
if self.power > 0 and self.wheels > 0:
return "Едем"
else:
return "Стоим"
# транспортные средства
class Car(Vehicle):
def __init__(self):
super().__init__()
self.hp = 80
self.power = 250
self.wheels = 4
class Bike(Vehicle):
def __init__(self):
super().__init__()
self.hp = 60
self.power = 150
self.wheels = 2
class Helicopter(Vehicle):
def __init__(self):
super().__init__()
self.hp = 120
self.power = 800
def fly(self):
if self.power > 0 and self.propellers > 0:
return "Летим"
else:
return "Стоим"
def radio(self):
if self.frequency != 0:
return "Пшш...пшш...Прием, как слышно? [" + str(self.frequency) +" GHz]"
else:
return "Кажется, радио не работает..."
# создание транспортных средств
bmw = Car()
ducati = Bike()
apache = Helicopter()
# эксплуатация транспортных средств
print(bmw.ride()) # ВЫВОД: Едем
print(ducati.ride()) # ВЫВОД: Едем
print(apache.ride()) # ВЫВОД: Стоим (избыточный метод)
print(apache.radio()) # ВЫВОД: Пшш...пшш...Прием, как слышно? [103.4 GHz]
В данном случае базовый класс транспортного средства реализует свойства и методы, которые избыточны для некоторых его наследников.
Пример кода на языке Python, который следует ISP:
# простые составные части транспортных средств
class Body:
def __init__(self):
self.hp = 100
class Engine:
def __init__(self):
self.power = 0
class Radio:
def __init__(self):
self.frequency = 103.4
def communicate(self):
if self.frequency != 0:
return "Пшш...пшш...Прием, как слышно? [" + str(self.frequency) +" GHz]"
else:
return "Кажется, радио не работает..."
# сложные составные части транспортных средств
class Suspension(Engine):
def __init__(self):
self.wheels = 0
def ride(self):
if self.power > 0 and self.wheels > 0:
return "Едем"
else:
return "Стоим"
class Frame(Engine):
def __init__(self):
self.propellers = 0
def fly(self):
if self.power > 0 and self.propellers > 0:
return "Летим"
else:
return "Стоим"
# транспортные средства
class Car(Body, Suspension):
def __init__(self):
super().__init__()
self.hp = 80
self.power = 250
self.wheels = 4
class Bike(Body, Suspension):
def __init__(self):
super().__init__()
self.hp = 60
self.power = 150
self.wheels = 2
class Helicopter(Body, Frame, Radio):
def __init__(self):
super().__init__()
self.hp = 120
self.power = 800
self.propellers = 2
self.frequency = 107.6
class Plane(Body, Frame):
def __init__(self):
super().__init__()
self.hp = 200
self.power = 1200
self.propellers = 4
# создание транспортных средств
bmw = Car()
ducati = Bike()
apache = Helicopter()
boeing = Plane()
# эксплуатация транспортных средств
print(bmw.ride()) # ВЫВОД: Едем
print(ducati.ride()) # ВЫВОД: Едем
print(apache.fly()) # ВЫВОД: Летим
print(apache.communicate()) # ВЫВОД: Пшш...пшш...Прием, как слышно? [107.6 GHz]
print(boeing.fly()) # ВЫВОД: Летим
Таким образом, все транспортные средства представляют собой набор составных частей со своими свойствами и методами. Ни один класс готовой техники не несет «на борту» лишний элемент или способность.
Благодаря ISP классы содержат в себе только нужны переменные и методы. Более того, разделение больших интерфейсов на маленькие позволяет специализировать логику в духе SRP.
Таким образом интерфейсы строятся из небольших блоков, словно конструктор, каждый из которых реализует только свою зону ответственности.
DIP (Dependency Inversion Principle) — принцип инверсии зависимостей, утверждающий, что компоненты верхнего уровня не должны зависеть от компонентов нижнего уровня.
Иными словами, абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.
Подобная архитектура достигается с помощью общих интерфейсов, скрывающих реализацию нижележащих объектов.
Пример кода на языке Python, который не следует DIP:
# проектор
class Light():
def __init__(self, wavelength):
self.wavelength = wavelength
def use(self):
return "Светим [" + str(self.wavelength) + " nm]"
# вертолет
class Helicopter:
def __init__(self, color = "белый"):
if color == "белый":
self.light = Light(600)
elif color == "голубой":
self.light = Light(450)
elif color == "красный":
self.light = Light(650)
def project(self):
return self.light.use()
# создаем технику
helicopterWhite = Helicopter("белый")
helicopterRed = Helicopter("красный")
# эксплуатируем технику
print(helicopterWhite.project()) # ВЫВОД: Светим [600 nm]
print(helicopterRed.project()) # ВЫВОД: Светим [650 nm]
В данном случае реализация Helicopter
зависит от реализации Light
. Вертолет должен учитывать принцип настройки проектора, передавая определенные параметры в его объект.
Более того, скрипт аналогично выполняет настройку Helicopter
с помощью булевой переменной. Если реализация проектора или вертолета изменится, параметры настройки могут перестать работать, что потребует модификации классов вышележащих объектов.
Реализация проектора должна быть полностью изолирована от реализации вертолета. Вертикальное взаимодействие между обеими сущностями необходимо выполнять через специальный интерфейс.
Пример кода на языке Python, который учитывает DIP:
from abc import ABC, abstractmethod
# базовый класс проектора
class Light(ABC):
@abstractmethod
def use(self):
pass
# белый проектор
class NormalLight(Light):
def use(self):
return "Светим ярким белым светом"
# красный проектор
class SpecialLight(Light):
def use(self):
return "Светим тусклым красным светом"
# вертолет
class Helicopter:
def __init__(self, light):
self.light = light
def project(self):
return self.light.use()
# создаем технику
helicopterWhite = Helicopter(NormalLight())
helicopterRed = Helicopter(SpecialLight())
# эксплуатируем технику
print(helicopterWhite.project()) # ВЫВОД: Светим ярким белым светом
print(helicopterRed.project()) # ВЫВОД: Светим тусклым красным светом
В такой архитектуре реализация конкретного проектора, будь то NormalLight
или SpecialLight
, не влияет на устройство вертолета Helicopter
. Наоборот, класс Helicopter
выставляет ряд требований к наличию определенных методов у класса Light
и его наследников.
Следование DIP уменьшает связанность программы — код верхнего уровня не зависит от деталей реализации, что упрощает модификацию или замену компонентов.
Благодаря активному использованию интерфейсов в программу можно добавлять новые реализации (наследуемые от базовых классов), которые можно использовать с уже имеющимися компонентами. В этом DIP перекликается с LSP.
В дополнение к этому во время тестирования вместо реальных зависимостей нижнего уровня можно подставлять пустые заглушки, имитирующие функции реальных компонентов.
Например, вместо совершения запроса к удаленному серверу можно имитировать задержку с помощью функции наподобие time.sleep()
.
Да и в целом DIP существенно повышает модульность программы, вертикально инкапсулируя логику компонентов.
Принципы SOLID помогают писать гибкий, поддерживаемый и масштабируемый код. Они особенно актуальны при разработке бэкенда высоконагруженных приложений, работе с микросервисной архитектурой и использовании объектно-ориентированного программирования.
По сути, SOLID направлен на локализацию (увеличение связанности) и инкапсуляцию (уменьшение связности) логики компонентов приложения как по горизонтали, так и вертикали.
Какими бы синтаксическим конструкциями не обладал язык (возможно, он слабо поддерживает ООП), он в той или иной степени позволяет следовать принципам SOLID.
Как правило каждая итерация программного продукта либо добавляет новое поведение, либо изменяет уже имеющееся, тем самым увеличивая сложность системы.
Однако рост сложности часто приводит к беспорядку. Поэтому принципы SOLID задают некие архитектурные рамки, в пределах которых проект остается понятным и структурированным. SOLID не позволяет хаосу нарастать.
В реальных проектах SOLID выполняет несколько важных функций:
По сути, SOLID является обобщенным сводом правил, на основе которых формируются программные абстракции и взаимодействия между разными компонентами приложения.
Принципы SOLID и архитектурные паттерны — это два разных, но взаимосвязанных уровня проектирования ПО.
Принципы SOLID существуют на более низком уровне реализации, а архитектурные паттерны — на более высоком.
То есть SOLID может применяться в рамках произвольного архитектурного паттерна, будь то MVC, MVVM, Layered Architecture, Hexagonal Architecture.
Например, в веб-приложении, построенном на MVC, один контроллер может отвечать за обработку HTTP-запросов, а другой — за выполнение бизнес-логики. Таким образом, реализация будет следовать SRP.
Более того, в рамках MVC все зависимости могут передаваться через интерфейсы, а не создаваться внутри классов. Это, в свою очередь, будет уже следованием DIP.
Главное достоинство SOLID — увеличение модульности кода. Модульность — крайне полезное свойство для юнит-тестирования. Ведь классы, выполняющие только одну задачу, тестировать проще, чем классы, состоящие из логической «сборной солянки».
В какой-то степени само тестирование начинает следовать SRP, выполняя вместо одного разрозненного теста множество мелких и специализированных.
Более того, благодаря OCP, добавление нового функционала не ломает существующие тесты, а оставляет их по-прежнему актуальными, несмотря на то, что общее поведение программы могло измениться.
На самом деле, тесты можно считать своего рода слепком программы. Исключительно в том смысле, что они обрамляют логику приложения и тестируют его реализацию. Поэтому нет ничего удивительного в том, что тесты следуют тем же принципам и архитектурным паттернам, что и само приложение.
Чрезмерное следование SOLID может привести к раздробленному коду со множеством мелких классов и интерфейсов. В небольших проектах строгие разделения могут быть излишними.
Принципы SOLID актуальны в любых проектах. Придерживаться их — хорошая практика.
Однако сложные абстракции и интерфейсы SOLID могут быть избыточными для простых проектов. Напротив, в сложных проектах SOLID способен упростить понимание кода и помочь в масштабировании реализации.
Иными словами, если проект небольшой, дробление кода на множество классов и интерфейсов излишне. Например, разделение логики на кучу классов в простом Telegram-боте только усложнит поддержку.
То же самое касается кода для одноразового использования (например, разовая автоматизация задач) — строгое следование SOLID в этом случае будет пустой тратой времени.
Надо понимать, что SOLID — не догма, а инструмент. Он должен применяться там, где необходимо повысить качество кода, а не усложнить его без необходимости.
Иногда проще написать простой и монолитный код, чем раздробленный и переусложненный.
Помимо SOLID существуют и другие принципы, подходы и шаблоны проектирования ПО, которые возможно использовать как по отдельности, так и в качестве дополнения к SOLID:
GRASP (General Responsibility Assignment Software Patterns). Набор шаблонов распределения ответственности, описывающий взаимодействия классов друг с другом.
YAGNI (You Ain’t Gonna Need It). Принцип отказа от избыточной функциональности, в которой нет непосредственной надобности.
KISS (Keep It Simple, Stupid). Принцип программирования, декларирующий простоту в качестве основной ценности ПО.
DRY (Don’t Repeat Yourself). Принцип разработки ПО, минимизирующий дублирование кода.
CQS (Command-Query Separation). Шаблон проектирования, разделяющий операции на две категории — команды, изменяющие состояние системы, и запросы, получающие данные из системы.
DDD (Domain-Driven Design). Подход к разработке ПО, структурирующий код вокруг предметной области предприятия.
Тем не менее, сколько бы подходов ни было, главное — применять их осмысленно, а не слепо следовать им. SOLID – это полезный инструмент, но применять его нужно осознанно.
Надежные VDS/VPS для ваших проектов