Обложка к записи Принципы SOLID
Время для прочтения: 5 мин. 53 сек.

Принципы SOLID

SOLID — мнемонический акроним, введённый Майклом Фэзерсом (Michael Feathers) для первых пяти принципов, названных Робертом Мартином в начале 2000-х, которые означали пять основных принципов объектно-ориентированного программирования и проектирования.

Для чего нужны принципы SOLID?

Принципы SOLID — это набор правил, которые необходимо применять во время работы над программным обеспечением(ПО) для его улучшения.

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

S – The Single Responsibility Principle (Принцип единственной ответственности)

Каждый класс выполняет лишь одну задачу.

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

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

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

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

Пример

Есть класс Order в котором описана логика работы с заказом.

class Order {

	public function calculateTotalSum() {/*...*/}

	public function getItems() {/*...*/}

	public function getItemCount() {/*...*/}

	public function addItem( $item ) {/*...*/}

	public function deleteItem( $item ) {/*...*/}

	public function printOrder() {/*...*/}

	public function showOrder() {/*...*/}

	public function load() {/*...*/}

	public function save() {/*...*/}

	public function update() {/*...*/}

	public function delete() {/*...*/}
	
}

Необходимо разделить его на несколько классов и выделить логику работы с базой и отображением в отдельные классы.

class Order {

	public function calculateTotalSum() {/*...*/}

	public function getItems() {/*...*/}

	public function getItemCount() {/*...*/}

	public function addItem( $item ) {/*...*/}

	public function deleteItem( $item ) {/*...*/}
	
}

class OrderRepository {

	public function load( $orderID ) {/*...*/}

	public function save( $order ) {/*...*/}

	public function update( $order ) {/*...*/}

	public function delete( $order ) {/*...*/}
	
}

class OrderViewer {

	public function printOrder( $order ) {/*...*/}

	public function showOrder( $order ) {/*...*/}
	
}

O – The Open Closed Principle (Принцип открытости/закрытости)

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

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

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

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

Пример

У нас есть класс OrderRepository. В его методе load описана работа получения заказа из БД.

class OrderRepository {

	public function load( $orderID ) {
		$pdo       = new PDO(
			$this->config->getDsn(),
			$this->config->getDBUser(),
			$this->config->getDBPassword()
		);
		$statement = $pdo->prepare( "SELECT * FROM `orders` WHERE id=:id" );
		$statement->execute( array( ":id" => $orderID ) );

		return $query->fetchObject( "Order" );
	}

	public function save( $order ) {/*...*/}

	public function update( $order ) {/*...*/}

	public function delete( $order ) {/*...*/}
	
}

Когда появляется необходимость получать заказы не только с базы, а например с API, то можно поступить следующим образом:

  1. Создать интерфейс IOrderSource
  2. Сделать 2 класса MySQLOrderSource и ApiOrderSource, которые выполняют данный интерфейс
  3. И передавать в конструктор класса OrderRepository инстанс, который реализует интерфейс IOrderSource.

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

interface IOrderSource {

	public function load( $orderID );

	public function save( $order );

	public function update( $order );

	public function delete( $order );
	
}

class MySQLOrderSource implements IOrderSource {

	public function load( $orderID ) {/*...*/}

	public function save( $order ) {/*...*/}

	public function update( $order ) {/*...*/}

	public function delete( $order ) {/*...*/}
	
}

class ApiOrderSource implements IOrderSource {

	public function load( $orderID ) {/*...*/}

	public function save( $order ) {/*...*/}

	public function update( $order ) {/*...*/}

	public function delete( $order ) {/*...*/}
	
}

class OrderRepository {
	
	private $source;

	public function __constructor( IOrderSource $source ) {
		$this->source = $source;
	}

	public function load( $orderID ) {
		return $this->source->load( $orderID );
	}

	public function save( $order ) {/*...*/}

	public function update( $order ) {/*...*/}
	
}

L – The Liskov Substitution Principle (Принцип подстановки Барбары Лисков)

Наследники должны повторять поведение родительского класса и должны вести себя без сюрпризов.

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

Пример

У нас есть класс LessonRepository, который в методе getAll возвращает массив всех уроков из файла. Появилась необходимость получать уроки из БД. Создаем класс DatabaseLessonRepository, наследуем его от LessonRepository и переписываем метод getAll.

class LessonRepository {
	
    //return array of lesson through file system.
	public function getAll() {
		return $files;
	}
	
}

class DatabaseLessonRepository extends LessonRepository {
	
    //return a Collection type instead of array
	public function getAll() {
		return Lesson::all();
	}
	
}

В методе getAll у класса DatabaseLessonRepository вместо коллекции мы должны вернуть массив.

interface LessonRepositoryInterface {

	public function getAll(): array;
	
}

class FilesystemLessonRepository implements LessonRepositoryInterface {
	
	public function getAll(): array {
		return $files;
	}
	
}


class DatabaseLessonRepository implements LessonRepositoryInterface {
	
	public function getAll(): array {
		return Lesson::all()->toArray();
	}
	
}

I – The Interface Segregation Principle (Принцип разделения интерфейса)

Много мелких интерфейсов лучше, чем один большой.

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

Пример

У нас есть интерфейс Bird, который имеет методы eat и fly. Когда в коде появляется Penguin, который не умеет летать нужно бросить Exception.

interface Bird {
	
	public function eat();

	public function fly();
	
}

class Duck implements Bird {

	public function eat() {/*...*/}

	public function fly() {/*...*/}
	
}

class Penguin implements Bird {
	
	public function eat() {/*...*/}

	public function fly() {/* exception */}
	
}

Вместо Exception лучше разделить интерфейсы для птицы на Bird, FlyingBird и RunningBird.

interface Bird {
	
	public function eat();
	
}

interface FlyingBird {
	
	public function fly();
	
}

interface RunningBird {
	
	public function run();
	
}

class Duck implements Bird, FlyingBird {
	
	public function eat() {/*...*/}

	public function fly() {/*...*/}
	
}

class Penguin implements Bird, RunningBird {
	
	public function eat() {/*...*/}
	public function run() {/*...*/}
	
}

D – The Dependency Inversion Principle (Принцип инверсии зависимостей)

Зависимость на абстракциях, нет зависимостей на что-то конкретное.

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

Самое простое решение начать применять этот принцип это писать тесты т.к. при их написании тестов необходимо мокать какие-то данные и как итог: проще переписать класс, чем написать на него тест.

Пример

У нас есть класс EBookReader, который принимает в коструктор объект класса PDFBook и в методе read — читает его. Когда появляется необходимость читать не только из PDF-файла класс необходимо изменять.

class EBookReader {

	private $book;

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

	public function read() {
		return $this->book->read();
	}

}

class PDFBook {

	public function read() { /*...*/
	}
	
}

Лучше сделать интерфейс EBook с методом read. И тогда в EBookReader мы сможем передавать любые объекты, которые реализовывают интерфейс EBook.

interface EBook {

	public function read();
	
}

class EBookReader {

	private $book;

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

	public function read() {
		return $this->book->read();
	}

}

class PDFBook implements EBook {

	public function read() {/*...*/}

}

class MobiBook implements EBook {

	public function read() {/*...*/}

}

Основные триггеры того, что принципы SOLID нарушены:

  • Оператор switch
  • Большое кол-во констант
  • new внутри методов класса
  • instanceof

Часть проблем решает применение паттернов проектирования:

Найболее полулярные паттерны в PHP:

  • Strategy
  • State
  • Chain of Responsibility
  • Visitor
  • Decorator
  • Composition
  • Factory
ВКонтакте
Telegram
WhatsApp

Комментарии