SOLID принципы: залог надёжного и поддерживаемого кода
В постоянно меняющемся мире разработки программного обеспечения написание надёжного, поддерживаемого и масштабируемого кода – это не просто лучшая практика, а необходимость. Одним из наиболее эффективных способов достижения этой цели является соблюдение принципов SOLID, набора из пяти фундаментальных принципов проектирования, предложенных Робертом К. Мартином, также известным как «дядя Боб». Эти принципы являются краеугольным камнем объектно-ориентированного дизайна и десятилетиями направляют разработчиков.
Принцип единственной ответственности (SRP)
Принцип единственной ответственности является первым и, возможно, самым простым из принципов SOLID. Он гласит, что класс должен иметь только одну причину для изменения, или, проще говоря, он должен выполнять одну чётко определённую задачу.
Пример нарушения SRP:
Рассмотрим класс Java, который нарушает принцип единственной ответственности:
public class Employee {
private String name;
private double salary;
public void calculateSalary() {
// Код для расчёта зарплаты
}
public void generatePayrollReport() {
// Код для создания отчёта о заработной плате
}
}
Здесь класс Employee
имеет две отдельные обязанности: расчёт зарплаты и создание отчёта о зарплате. Это явное нарушение принципа SRP, поскольку класс имеет более одной причины для изменения.
Исправление нарушения:
Чтобы соблюсти принцип SRP, необходимо разделить эти обязанности на разные классы:
public class Employee {
private String name;
private double salary;
public void calculateSalary() {
// Код для расчета зарплаты
}
}
public class PayrollReportGenerator {
public void generatePayrollReport(Employee employee) {
// Код для генерации отчёта о заработной плате
}
}
Теперь каждый класс выполняет одну функцию, делая код более удобным в обслуживании и простым для расширения.
Открытый/закрытый принцип (OCP)
Открытый/закрытый принцип гласит, что программные сущности (классы, модули, функции и т. д.) должны быть открыты для расширения, но закрыты для модификации. Это означает, что вы должны иметь возможность добавлять новые функциональные возможности без изменения существующего кода.
Применение OCP:
Рассмотрим пример PHP для иллюстрации этого принципа. Предположим, у нас есть класс, который вычисляет площадь различных фигур:
$shapes = [
new Circle(2),
new Square(5),
new Square(6),
];
$areas = new AreaCalculator($shapes);
$output = new SumCalculatorOutputter($areas);
echo $output->JSON();
echo $output->HTML();
Для расширения без изменения существующего класса AreaCalculator
можно создать новые производные классы и использовать полиморфизм:
class Shape {
public function area() {
// Абстрактный метод
}
}
class Circle extends Shape {
private $radius;
public function __construct($radius) {
$this->radius = $radius;
}
public function area() {
return pi() * pow($this->radius, 2);
}
}
class Square extends Shape {
private $side;
public function __construct($side) {
$this->side = $side;
}
public function area() {
return pow($this->side, 2);
}
}
class AreaCalculator {
private $shapes;
public function __construct(array $shapes) {
$this->shapes = $shapes;
}
public function totalArea() {
$total = 0;
foreach ($this->shapes as $shape) {
$total += $shape->area();
}
return $total;
}
}
Таким образом, мы расширили функциональность, добавив новые классы фигур без изменения класса AreaCalculator
.
Принцип подстановки Лисков (LSP)
Согласно принципу подстановки Лискова, подтипы должны быть заменяемыми для своих базовых типов. Другими словами, любой код, использующий базовый тип, должен работать с подтипом, не зная разницы.
Нарушение LSP:
Рассмотрим простой пример, где у нас есть класс Bird
и его подклассы Duck
и Penguin
:
class Bird {
public function fly() {
// Код, чтобы летать
}
}
class Duck extends Bird {
public function fly() {
// Код, чтобы лететь
}
}
class Penguin extends Bird {
public function fly() {
throw new Exception("Пингвины не умеют летать");
}
}
Класс Penguin
нарушает принцип подстановки Лискова, так как он не может летать, несмотря на то, что является подклассом Bird
.
Устранение нарушения:
Необходимо убедиться, что класс Penguin
не наследует метод fly
от Bird
. Вместо этого можно создать интерфейс или абстрактный класс, определяющий поведение полёта, и реализовать его только в классе Duck
:
interface Flyable {
public function fly();
}
class Bird {
// Общее поведение птиц
}
class Duck extends Bird implements Flyable {
public function fly() {
// Летательный код
}
}
class Penguin extends Bird {
// Конкретное поведение пингвинов
}
Так мы гарантируем, что только птицы, способные летать, реализуют интерфейс Flyable
.
Сегрегация интерфейса (ISP)
Сегрегация интерфейсов гласит, что клиенты не должны реализовывать интерфейсы, которые они не используют. Вместо одного большого интерфейса лучше иметь несколько меньших интерфейсов.
Нарушение ISP:
Представьте себе единый большой интерфейс для принтера, включающий методы печати, сканирования и факса:
interface Printer {
public function print();
public function scan();
public function fax();
}
class BasicPrinter implements Printer {
public function print() {
// Печатный код
}
public function scan() {
throw new Exception("Базовый принтер не умеет сканировать");
}
public function fax() {
throw new Exception("Базовый принтер не может отправлять факс");
}
}
Класс BasicPrinter
вынужден реализовывать методы, которые он не использует.
Устранение нарушения:
Чтобы следовать принципу сегрегации интерфейса, можно разбить большой интерфейс на более конкретные интерфейсы:
interface Printable {
public function print();
}
interface Scannable {
public function scan();
}
interface Faxable {
public function fax();
}
class BasicPrinter implements Printable {
public function print() {
// Напечатанный код
}
}
class AdvancedPrinter implements Printable, Scannable, Faxable {
public function print() {
// Напечатанный код
}
public function scan() {
// Сканированный код
}
public function fax() {
// Отправляемый факсом код
}
}
Каждый класс реализует только те интерфейсы, которые соответствуют его функциональности.
Инверсия зависимостей (DIP)
Инверсия зависимостей гласит, что высокоуровневые модули не должны зависеть от низкоуровневых модулей, а оба должны зависеть от абстракций. Абстракции не должны зависеть от деталей; детали должны зависеть от абстракций.
Нарушение DIP:
Представим класс PasswordReminder
, зависящий напрямую от класса MySQLConnection
:
class MySQLConnection {
public function connect() {
// Подключение к MySQL
}
}
class PasswordReminder {
private $dbConnection;
public function __construct(MySQLConnection $dbConnection) {
$this->dbConnection = $dbConnection;
}
public function remind() {
// Кодирование напоминания пароля с использованием соединения MySQL
}
}
Класс PasswordReminder
тесно связан с классом MySQLConnection
.
Устранение нарушения:
Следуя принципу инверсии зависимостей, можно ввести уровень абстракции, используя интерфейс:
interface DBConnectionInterface {
public function connect();
}
class MySQLConnection implements DBConnectionInterface {
public function connect() {
// Соединение с MySQL
}
}
class PasswordReminder {
private $dbConnection;
public function __construct(DBConnectionInterface $dbConnection) {
$this->dbConnection = $dbConnection;
}
public function remind() {
// Использование абстракции для напоминания пароля
}
}
Благодаря этому класс PasswordReminder
зависит от абстракции (DBConnectionInterface
), а не от конкретной реализации (MySQLConnection
).
Визуальное представление с помощью Mermaid
Ниже представлена диаграмма классов Mermaid, иллюстрирующая исправленную структуру класса PasswordReminder
: