Skip to content
Description of the principles of SOLID
PHP
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
images Init Sep 9, 2019
with-solid Init Sep 9, 2019
without-solid Init Sep 9, 2019
README.md Update README.md Oct 30, 2019

README.md

SOLID Principles

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

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

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

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

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

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

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 (Принцип открытости/закрытости)

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

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 (Принцип подстановки Барбары Лисков)

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

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 (Принцип разделения интерфейса)

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

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 (Принцип инверсии зависимостей)

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

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
You can’t perform that action at this time.