SOLID principles

SOLID — 5 правил разработки ПО задают траекторию, по которой нужно следовать, когда пишешь программы, чтобы их проще было масштабировать и поддерживать.

SOLID подробнее: https://solidbook.vercel.app/

S – Single Responsibility Principle (SRP)

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

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

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

Назначение

Служит для разделения поведения разных классов, так чтобы разное поведение не пересекалось.

Пример кода

Давайте рассмотрим пример на python с классом User, который нарушает принцип SRP:

class User:
def __init__(self, username, email):
	self.username = username
	self.email = email

def save(self):
	# Логика сохранения пользователя в базе данных
	pass

def send_email(self, message):
	# Логика отправки электронной почты пользователю
	pass

def generate_report(self):
	# Логика генерации отчета пользователя
	pass

Тут класс User отвечает за сохранение пользователя в базе данных, отправку электронной почты и генерацию отчета.

Давайте применим SRP принцип и разделим класс User на несколько отдельных классов с единой ответственностью:

class User:
	def __init__(self, username, email):
		self.username = username
		self.email = email

	def save(self):
		# Логика сохранения пользователя в базе данных
		pass

class EmailSender:
	def send_email(self, user, message):
		# Логика отправки электронной почты пользователю
		pass

class ReportGenerator:
	def generate_report(self, user):
		# Логика генерации отчета пользователя
		pass

Видео

O — Open-Closed Principle (OCP)

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

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

Назначение

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

Пример кода

Правильная реализация OCP на PHP:

interface Shape {
	public function area(): float;
}

class Rectangle implements Shape {
	private $width;
	private $height;

	public function __construct($width, $height) {
		$this->width = $width;
		$this->height = $height;
	}

	public function area(): float {
		return $this->width * $this->height;
	}
}

class Circle implements Shape {
	private $radius;

	public function __construct($radius) {
		$this->radius = $radius;
	}

	public function area(): float {
		return pi() * pow($this->radius, 2);
	}
}

Здесь мы создаем интерфейс Shape, который определяет контракт для вычисления площади различных фигур. Классы Rectangle и Circle реализуют этот интерфейс и предоставляют свои специфические реализации метода area().

Придерживаясь принципа OCP, мы можем вводить новые фигуры без модификации существующего кода. Добавим треугольник:

class Triangle implements Shape {
	private $base;
	private $height;

	public function __construct($base, $height) {
		$this->base = $base;
		$this->height = $height;
	}

	public function area(): float {
		return 0.5 * $this->base * $this->height;
	}
}

Тут мы добавляем новый класс Triangle, который реализует интерфейс Shape со своей реализацией метода area(). Нам не потребовалось модифицировать существующие классы Rectangle или Circle для размещения новой фигуры.

Создавая классы и интерфейсы, открытые для расширения (добавления новых фигур) и закрытые для модификации (избегая изменения существующей логики вычисления фигур), мы следуем принципу OCP.

Помимо интерфейсов и наследования, для реализации принципа OCP могут использоваться и другие приемы, такие как:

  • композиция.
  • паттерн стратегия.
  • инъекция зависимостей.

Видео

TODO: video_here

L — Liskov Substitution Principle (LSP)

Объекты (классы) можно заменить на под-объекты (классы созданные на базе первого класса) при этом программа не должна сломаться.

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

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

Если вы создаете новый класс на базе другого, исходный класс становится родителем, а новый – потомком. Необходимо, чтобы потомок умел делать все точно так же как и родитель. Например, при перегрузке метода, новый метод должен уметь все тоже самое что и исходный метод (получать такие же параметры, возвращать такие же данные/типы).

Если потомок не удовлетворяет этим требованиям, значит, он слишком сильно отличается от родителя и нарушает принцип LSP.

Назначение

Служит для обеспечения постоянства: потомок может быть использован вместо родителя без нарушения работы программы.

Видео

I — Interface Segregation Principle (ISP)

Клиент должен знать о методах класса, которые нужны ему и не знать ничего о каких-либо других методах.

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

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

Назначение

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

Пример кода

Давайте рассмотрим пример на Python с интерфейсами для различных устройств вывода ввода:

class InputDevice(ABC):
	@abstractmethod
	def read_input(self):
		pass

class OutputDevice(ABC):
	@abstractmethod
	def write_output(self, data):
		pass

class Keyboard(InputDevice):
	def read_input(self):
		# Логика чтения ввода с клавиатуры
		pass

class Mouse(InputDevice):
	def read_input(self):
		# Логика чтения ввода с мыши
		pass

class Monitor(OutputDevice):
	def write_output(self, data):
		# Логика вывода данных на монитор
		pass

class Printer(OutputDevice):
	def write_output(self, data):
		# Логика вывода данных на принтер
		pass

В этом примере у нас есть абстрактные классы InputDevice и OutputDevice, представляющие интерфейсы для устройств ввода и вывода соответственно. Затем мы определяем реализации этих абстракций в виде классов: Keyboard, Mouse, Monitor и Printer.

Клиенты будут зависить от интерфейсов, а не от реализаций. Например, если клиенту нужен только ввод с клавиатуры, он может зависеть только от интерфейса InputDevice и использовать класс Keyboard:

def process_input(device):
	data = device.read_input()
	# Логика обработки ввода

keyboard = Keyboard()
process_input(keyboard)

В этом примере функция process_input() принимает объект, реализующий интерфейс InputDevice, и обрабатывает его ввод. Здесь мы передаем объект Keyboard, который соответствует интерфейсу InputDevice. Таким образом, клиент зависит только от необходимого интерфейса и не зависит от лишних методов или классов.

Видео

TODO: video here

D — Dependency Inversion Principle (DIP)

Модули верхнего уровня не должны зависеть от модулей нижнего уровня. И те, и другие должны зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

  • Модули (классы) верхнего уровня = классы, которые выполняют операцию при помощи инструмента.
  • Модули (классы) нижнего уровня = инструменты, которые нужны для выполнения операций.
  • Абстракция = интерфейс, соединяющий два класса.
  • Детали = специфический код внутри инструмента - реализация конкретной задачи в коде.

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

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

Назначение

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

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

Пример кода

Давайте рассмотрим пример на python с классами Notification и EmailSender. Данный код нарушает принцип DIP:

class Notification(ABC):
	@abstractmethod
	def send_notification(self, message):
		pass

class EmailSender(Notification):
	def send_notification(self, message):
		# Логика отправки уведомления по электронной почте
		pass

class SMSNotification(Notification):
	def send_notification(self, message):
		# Логика отправки уведомления по SMS
		pass

class User:
	def __init__(self, username, email):
		self.username = username
		self.email = email
		self.notification_service = EmailSender() # !!!!!!!

	def send_notification(self, message):
		self.notification_service.send_notification(message)

В этом примере класс User напрямую зависит от конкретной реализации EmailSender в качестве сервиса уведомлений.

Чтобы применить SOLID принцип DIP, изменяем User, чтобы он зависел от абстракции Notification, а не от конкретной реализации:

class User:
def __init__(self, username, email, Notification):
	self.username = username
	self.email = email
	self.notification_service = Notification

def send_notification(self, message):
	self.notification_service.send_notification(message)

Теперь User принимает объект notification_service, реализующий интерфейс Notification. Это позволяет передавать различные реализации уведомлений, например EmailSender или SMSNotification, без изменения самого User:

email_sender = EmailSender()
user = User("John", "john@example.com", email_sender)
user.send_notification("Hello!")

sms_notification = SMSNotification()
user = User("Jane", "jane@example.com", sms_notification)
user.send_notification("Hi there!")

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

Видео

--

Фото были взяты здесь: https://habr.com/ru/company/productivity_inside/blog/505430/