SOLID — мнемонический акроним, введённый Майклом Фэзерсом (Michael Feathers) для первых пяти принципов, названных Робертом Мартином в начале 2000-х, которые означали пять основных принципов объектно-ориентированного программирования и проектирования.
Принципы SOLID — это набор правил, которые необходимо применять во время работы над программным обеспечением(ПО) для его улучшения.
Удивительно то, что принципы были сформулированы несколько десятков лет назад и до сих пор актуальны. Это может говорить только о их еффективности.
Каждый класс выполняет лишь одну задачу.
Очень простой, но в тоже время очень и очень важный принцип. Необходимо следить в вашем ПО, чтобы каждый класс или интерфейс не был перегружен лишней логикой и выполнял только одну задачу.
Нужно понимать, что каждое изменение в логике работы класса влечет за собой изменения в коде. Если ваш класс имеет более одной ответственности, то изменения его при изменении бизнес-логики будут происходить чаще. Так же класс взаимодействует с большим количеством других классов, что сильнее их связывает между собой и становится сложнее в поддержке.
Часто при правке багов и хотфиксов нарушается этот принцип. В случае хотфиксов нужно занести это в технический долг и выполнить его в ближайшее время.
Есть класс 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 ) {/*...*/}
}
Классы должны быть открыты для расширения и закрыты для модификации.
В идеальном мире это для добавление нового функционала нужно добавлять новый код, а не изменять старый.
Багфикс, рефакторинг и улучшение производительности это не нарушение этого принципа. Принцип гласит именно про изменение логики работы ПО.
У нас есть класс 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, то можно поступить следующим образом:
- Создать интерфейс IOrderSource
- Сделать 2 класса MySQLOrderSource и ApiOrderSource, которые выполняют данный интерфейс
- И передавать в конструктор класса 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 ) {/*...*/}
}
Наследники должны повторять поведение родительского класса и должны вести себя без сюрпризов.
У нас есть класс 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();
}
}
Много мелких интерфейсов лучше, чем один большой.
У нас есть интерфейс 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() {/*...*/}
}
Зависимость на абстракциях, нет зависимостей на что-то конкретное.
Самое простое решение начать применять этот принцип это писать тесты т.к. при их написании тестов необходимо мокать какие-то данные и как итог: проще переписать класс, чем написать на него тест.
У нас есть класс 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() {/*...*/}
}
- Оператор switch
- Большое кол-во констант
- new внутри методов класса
- instanceof
Найболее полулярные паттерны в PHP:
- Strategy
- State
- Chain of Responsibility
- Visitor
- Decorator
- Composition
- Factory