Принципы SOLID — это стандарт программирования, который все разработчики должны хорошо понимать, чтобы избегать создания плохой архитектуры. Этот стандарт широко используется в ООП. Если применять его правильно, он делает код более расширяемым, логичным и читабельным. Когда разработчик создаёт приложение, руководствуясь плохой архитектурой, код получается негибким, даже небольшие изменения в нём могут привести к багам. Поэтому нужно следовать принципам SOLID.
На их освоение потребуется какое-то время, но если вы будете писать код в соответствии с этими принципами, то его качество повысится, а вы освоите создание хорошей архитектуры ПО.
Чтобы понять принципы SOLID, нужно чётко понимать, как использовать интерфейсы. Если у вас такого понимания нет, то сначала почитайте документацию.
Я буду объяснять SOLID самым простым способом, так что новичкам легче будет разобраться. Будем рассматривать принципы один за другим.
Принцип единственной ответственности (Single Responsibility Principle)
Существует лишь одна причина, приводящая к изменению класса.Один класс должен решать только какую-то одну задачу. Он может иметь несколько методов, но они должны использоваться лишь для решения общей задачи. Все методы и свойства должны служить одной цели. Если класс имеет несколько назначений, его нужно разделить на отдельные классы.
Рассмотрим пример:
<?php
namespace Demo;
use DB;
class OrdersReport
{
public function getOrdersInfo($startDate, $endDate)
{
$orders = $this->queryDBForOrders($startDate, $endDate);
return $this->format($orders);
}
protected function queryDBForOrders($startDate, $endDate)
{ // If we would update our persistence layer in the future,
// we would have to do changes here too. <=> reason to change!
return DB::table('orders')->whereBetween('created_at', [$startDate, $endDate])->get();
}
protected function format($orders)
{ // If we changed the way we want to format the output,
// we would have to make changes here. <=> reason to change!
return '<h1>Orders: ' . $orders . '</h1>';
}
}
Приведённый здесь класс нарушает принцип единственной ответственности. Почему он должен извлекать данные из базы? Это задача для уровня хранения данных, на котором данные сохраняются и извлекаются из хранилища (например, базы данных). Это ответственность не этого класса.
Также данный класс не должен отвечать за формат следующего метода, потому что нам могут понадобиться данные другого формата, например, XML, JSON, HTML и т.д.
Код после рефакторинга будет выглядеть так:
<?php
namespace Report;
use Report\Repositories\OrdersRepository;
class OrdersReport
{
protected $repo;
protected $formatter;
public function __construct(OrdersRepository $repo, OrdersOutPutInterface $formatter)
{
$this->repo = $repo;
$this->formatter = $formatter;
}
public function getOrdersInfo($startDate, $endDate)
{
$orders = $this->repo->getOrdersWithDate($startDate, $endDate);
return $this->formatter->output($orders);
}
}
namespace Report;
interface OrdersOutPutInterface
{
public function output($orders);
}
namespace Report;
class HtmlOutput implements OrdersOutPutInterface
{
public function output($orders)
{
return '<h1>Orders: ' . $orders . '</h1>';
}
}
namespace Report\Repositories;
use DB;
class OrdersRepository
{
public function getOrdersWithDate($startDate, $endDate)
{
return DB::table('orders')->whereBetween('created_at', [$startDate, $endDate])->get();
}
}
Принцип открытости/закрытости (Open-closed Principle)
Программные сущности должны быть открыты для расширения, но закрыты для модификации.Программные сущности (классы, модули, функции и прочее) должны быть расширяемыми без изменения своего содержимого. Если строго соблюдать этот принцип, то можно регулировать поведение кода без изменения самого исходника.
Рассмотрим пример:
<?php
class Rectangle
{
public $width;
public $height;
public function __construct($width, $height)
{
$this->width = $width;
$this->height = $height;
}
}
class Circle
{
public $radius;
public function __construct($radius)
{
$this->radius = $radius;
}
}
class AreaCalculator
{
public function calculate($shape)
{
if ($shape instanceof Rectangle) {
$area = $shape->width * $shape->height;
} else {
$area = $shape->radius * $shape->radius * pi();
}
return $area;
}
}
$circle = new Circle(5);
$rect = new Rectangle(8,5);
$obj = new AreaCalculator();
echo $obj->calculate($circle);
Если нам нужно вычислить площадь квадрата, то нужно изменить метод вычисления в классе
AreaCalculator
. Но это нарушит принцип открытости/закрытости, согласно которому мы можем не изменять, а только расширять.Как же можно решить поставленную задачу? Смотрите:
<?php
interface AreaInterface
{
public function calculateArea();
}
class Rectangle implements AreaInterface
{
public $width;
public $height;
public function __construct($width, $height)
{
$this->width = $width;
$this->height = $height;
}
public function calculateArea(){
$area = $this->height * $this->width;
return $area;
}
}
class Circle implements AreaInterface
{
public $radius;
public function __construct($radius)
{
$this->radius = $radius;
}
public function calculateArea(){
$area = $this->radius * $this->radius * pi();
return $area;
}
}
class AreaCalculator
{
public function calculate($shape)
{
$area = 0;
$area = $shape->calculateArea();
return $area;
}
}
$circle = new Circle(5);
$obj = new AreaCalculator();
echo $obj->calculate($circle);
Теперь можно найти площадь круга, не меняя класс
AreaCalculator
.Принцип подстановки Барбары Лисков (Liskov Substitution Principle)
Этот принцип Барбара Лисков предложила в своём докладе “Data abstraction” в 1987-м. А в 1994-м идея была вкратце сформулирована Барбарой и Дженнет Уинг:
Пусть ?(x) — доказуемое свойство объекта x типа T. Тогда ?(y) должно быть верным для объектов y типа S, где S — подтип T.В удобочитаемой версии повторяется практически всё, что говорил Бертранд Майер, но здесь в качестве базиса взята система типов:
Роберт Мартин в 1996-м дал другое определение, более понятное:
- Предварительные условия не могут быть усилены в подтипе.
- Постусловия не могут быть ослаблены в подтипе.
- Инварианты супертипа могут быть сохранены в подтипе.
Функции, использующие указатели ссылок на базовые классы, должны уметь использовать объекты производных классов, даже не зная об этом.Попросту говоря: подкласс/производный класс должен быть взаимозаменяем с базовым/родительским классом.
Значит, любая реализация абстракции (интерфейса) должна быть взаимозаменяемой в любом месте, в котором принимается эта абстракция. По сути, когда мы используем в коде интерфейсы, то используем контракт не только по входным данным, принимаемым интерфейсом, но и по выходным данным, возвращаемым разными классами, реализующими этот интерфейс. В обоих случаях данные должны быть одного типа.
В этом коде нарушен обсуждаемый принцип и показан способ исправления:
<?php
interface LessonRepositoryInterface
{
/**
* Fetch all records.
*
* @return array
*/
public function getAll();
}
class FileLessonRepository implements LessonRepositoryInterface
{
public function getAll()
{
// return through file system
return [];
}
}
class DbLessonRepository implements LessonRepositoryInterface
{
public function getAll()
{
/*
Violates LSP because:
- the return type is different
- the consumer of this subclass and FileLessonRepository won't work identically
*/
// return Lesson::all();
// to fix this
return Lesson::all()->toArray();
}
}
Принцип разделения интерфейса (Interface Segregation Principle)
Нельзя заставлять клиента реализовать интерфейс, которым он не пользуется.Это означает, что нужно разбивать интерфейсы на более мелкие, лучше удовлетворяющие конкретным потребностям клиентов.
Как и в случае с принципом единственной ответственности, цель принципа разделения интерфейса заключается в минимизации побочных эффектов и повторов за счёт разделения ПО на независимые части.
Рассмотрим пример:
<?php
interface workerInterface
{
public function work();
public function sleep();
}
class HumanWorker implements workerInterface
{
public function work()
{
var_dump('works');
}
public function sleep()
{
var_dump('sleep');
}
}
class RobotWorker implements workerInterface
{
public function work()
{
var_dump('works');
}
public function sleep()
{
// No need
}
}
RobotWorker
’у не нужно спать, но класс должен реализовать метод sleep
, потому что все методы в интерфейсе абстрактны. Это нарушает принцип разделения. Вот как это можно исправить:<?php
interface WorkAbleInterface
{
public function work();
}
interface SleepAbleInterface
{
public function sleep();
}
class HumanWorker implements WorkAbleInterface, SleepAbleInterface
{
public function work()
{
var_dump('works');
}
public function sleep()
{
var_dump('sleep');
}
}
class RobotWorker implements WorkAbleInterface
{
public function work()
{
var_dump('works');
}
}
Принцип инверсии зависимостей (Dependency Inversion Principle)
Высокоуровневые модули не должны зависеть от низкоуровневых. Оба вида модулей должны зависеть от абстракций.
Абстракции не должны зависеть от подробностей. Подробности должны зависеть от абстракций.Проще говоря: зависьте от абстракций, а не от чего-то конкретного.
Применяя этот принцип, одни модули можно легко заменять другими, всего лишь меняя модуль зависимости, и тогда никакие перемены в низкоуровневом модуле не повлияют на высокоуровневый.
Рассмотрим пример:
<?php
class MySQLConnection
{
/**
* db connection
*/
public function connect()
{
var_dump('MYSQL Connection');
}
}
class PasswordReminder
{
/**
* @var MySQLConnection
*/
private $dbConnection;
public function __construct(MySQLConnection $dbConnection)
{
$this->dbConnection = $dbConnection;
}
}
Есть распространённое заблуждение, что «инверсия зависимостей» является синонимом «внедрения зависимостей». Но это разные вещи.
В приведённом коде, невзирая на то, что класс
MySQLConnection
был внедрён в класс PasswordReminder
, последний зависит от MySQLConnection
. Более высокуровневый PasswordReminder
не должен зависеть от более низкуровневого модуля MySQLConnection
.Если нам нужно изменить подключение с
MySQLConnection
на MongoDBConnection
, то придётся менять прописанное в коде внедрение конструктора в класс PasswordReminder
.Класс
PasswordReminder
должен зависеть от абстракций, а не от чего-то конкретного. Но как это сделать? Рассмотрим пример:<?php
interface ConnectionInterface
{
public function connect();
}
class DbConnection implements ConnectionInterface
{
/**
* db connection
*/
public function connect()
{
var_dump('MYSQL Connection');
}
}
class PasswordReminder
{
/**
* @var ConnectionInterface
*/
private $dbConnection;
public function __construct(ConnectionInterface $dbConnection)
{
$this->dbConnection = $dbConnection;
}
}
Здесь нам нужно изменить подключение с
MySQLConnection
на MongoDBConnection
. Нам не нужно менять внедрение конструктора в класс PasswordReminder
, потому что в данном случае класс PasswordReminder
зависит только от абстракции. Комментарии (66)
xvladimirov
31.05.2018 00:56+1Последний пример чересчур упрощен, и скорее заведет в тупик новичка, чем ему поможет, на мой взгляд. У MySQLConnection и MongoDBConnection по определению будут разные интерфейсы, по этому лучше было бы сделать что-то вроде
<?php interface UserRepository { // ... } class MySqlUserRepository implements UserRepository { /* реализация методов с учетом специфики mysql */ } class MongoUserRepository implements UserRepository { /* реализация методов с учетом специфики mongodb */ } class PasswordReminder { /** * @var UserRepository */ private $repository; public function __construct(UserRepository $repository) { $this->repository = $repository; } }
s_suhanov
31.05.2018 13:56Но в последнем примере как раз это и написано. Просто пропущен код для
class MongoDBConnection
.xvladimirov
31.05.2018 14:57+1Вы прочитали мой комментарий, прежде чем на него ответить?
Я говорю о том, что это неудачный пример по тому, что в реальной жизни невозможно сделать общий интерфейс для MongoDBConnection и MysqlConnection ввиду разной специфики реляционных и документ-ориентированных баз.
Если новичок попытается сделать что-то по конкретно этому примеру, его может поставить это в тупик.s_suhanov
31.05.2018 15:03Вы слишком привязались к деталям, а на самом деле имели ввиду то же, что и автор: для разных видов БД можно сделать общий интерфейс и объявить в нашем
PasswordReminder
поле с типом этого интерфейса. А реализация у каждого конечно же своя. Просто у вас этоRepository
, у автора —Connection
, но суть тут не в деталях, а в подходе. :)
kiwokr
31.05.2018 01:35Если честно статья каких миллион, уж извините. https://metanit.com/sharp/patterns/5.1.php
Не сочтите за рекламу, но куда проще и доступнее объясняется.GrWizard
31.05.2018 10:24согласен, уровень новизны в статье нулевой, пробежался глазами и не открыл для себя нового :(
rjhdby
31.05.2018 02:23Стиль изложения такой, что начинающие не поймут, а те, кто поймет — тем оно уже и так известно и понятно.
И немного комментариев по совсем уж в глаза бросившемуся:
1. Если уж перевод, тем паче для новичков, то стоит и комментарии в коде перевести, так как они являются неотъемлемой частью объяснения.
2. Принцип единственной ответственности относится не только к классам.
3. Очередная статья, где НЕ смогли объяснить «Принцип подстановки Барбары Лисков».SH42913
31.05.2018 10:25Подкинешь статью, где нормально разъясняется «Принцип подстановки Барбары Лисков»?
rjhdby
31.05.2018 11:44Да в той же вики вполне доступно описано.
А вообще, Роберт Мартин в свое время очень емко его объяснил (в статье оно есть, только в максимально усложненном варианте, зачем-то)
Функции, которые используют базовый тип, должны иметь возможность использовать подтипы базового типа, не зная об этом.
x-foby
31.05.2018 07:34Господа, принцип ЕДИНСТВЕННОЙ ответственности, а не единой. Это так-то немного разные вещи, чуть менее, чем совсем.
Dywar
31.05.2018 19:37Мартин в своей новой книге дал более понятное объяснение этому принципу.
Имеется ввиду пользователь/группа пользователей/актор, тот кто может инициировать изменение.
«Программное обеспечение изменяется для удовлетворения нужд пользователей и заинтересованных лиц. Пользователи и заинтересованные лица как раз и есть та самая «причина для изменения», о которой говорит принцип.»QtRoS
01.06.2018 20:46Зашёл сюда увидеть этот комментарий. Все верно, в статье традиционно ошибочная интерпретация принципа 'S'. У компонентов должна быть единственная причина изменения (так сказать один заказчик, модуль не должен служить нуждам бухгалтерии и бизнеса, например).
brnovk
31.05.2018 08:47+1Каждый раз при прочтении подобных статей, натыкаюсь на одну и ту же ошибку, которая, видимо, никого кроме меня не раздражает. Почему примеры кода настолько надуманные?
- Пример с Order — ни в одном рабочем приложении не будет такого кода, ибо отчёты отталкиваются от бизнес логики и практически не встречаются в единственном числе. Почти всегда их несколько. Хоть сколько-нибудь опытный программист сразу мысленно пытается выделить общий функционал у различных отчётов, затем в силу знаний и навыков уже реализует какие-либо шаблоны проектирования, вспомогательные методы, хитрую ооп-иерархию и т.д. В приведённом примере используется различная 'кастомизация' вывода и источника данных, замечательно, но тип отчёта остаётся всё тем-же и к расширению не располагает.
- Пример с Area — у кого, в здравом уме, придёт в голову писать ООП-иерархию для вычисления площади геометрических фигур? Среднестатистический разработчик напишет 2-3 вспомогательных метода в утилитарном классе приложения, наподобие RectangleArea, CircleArea, понятные любому другому среднестатистическому программисту. Без этих ненужных усложнений.
- Пример с HumanWorker, RobotWorker — вообще без комментариев.
- Пример с Connection и Password, в сравнении с остальными, уже лучше, но всё равно кажется неестественным — пароль это сущность (чаще даже не сущность, а поле) пользовательского аккаунта, отдельный класс для нотификации выглядит не самым естественным образом.
В вашей компании 3.5 тысячи сотрудников. Неужели не удалось найти хоть пару разработчиков, в практике работы которых, встречались примеры получше?Alozar
31.05.2018 10:43Мне порой кажется, что в таких темах ни разу не было реальных примеров, потому что в реальности оказывается, что приложение всем этим принципам не соответствует и из реального кода показывать нечего (из-за Legacy кода, криворукости разработчиков, необходимости сделать вотпрямщас или ещё чего-то).
Femistoklov
01.06.2018 13:01Так и есть. Мне это напоминает собеседования — спрашивают про принципы, паттерны, лучшие практики. А если устроишься и покопаешься в проектах — так там как раз кривой Legacy код с костылями.
nepster-web
31.05.2018 08:50Это я не совсем правильно понял или принцип «LSP» в данном примере суховат?
Особенно неприятно в 2018 году видеть примеры на php без указания типов (возвращаемых значений).
aosja
31.05.2018 11:42Принципы SOLID — это стандарт программирования, который все разработчики должны хорошо понимать, чтобы избегать создания плохой архитектуры.
SOLID — это Design. Мне кажется, архитектура приложения лежит на более высоком уровне.VolCh
31.05.2018 12:45Design — это «проектирование», проектирование архитектуры — его часть, а не другой уровень.
aosja
31.05.2018 14:36Приведите пример архитектурного решения для принципа «Dependency Inversion».
VolCh
31.05.2018 17:43Использование единого DI-контенейра для иницализации всех зависимостей классов приложения. Это архитектурное решение.
aosja
31.05.2018 17:51+1Это не архитектурное решение
VolCh
31.05.2018 18:02А как вы называете решения, от которых зависит реализация минимум половины кода приложения?
aosja
31.05.2018 18:13зависит реализация минимум половины кода приложения
Вы серьезно? Я могу поменять один DI контейнер на другой и это вряд ли повлечет за собой изменение половины кода. Только сам код конфигурациии DI, а это далеко не половина.
Примеры архитектурных решений: нужна секьюрити/не нужна, используем БД/документную/реляционную/не используем, eventual-consistency/transactional-consistency, и т.п.VolCh
01.06.2018 03:53Использование DI вообще и контейнера в частности — это архитектурное решение. Какой контейнер конкретно — деталь реализации.
aosja
01.06.2018 09:03Архитектурные решения обусловлены функциональными и не функциональными требованиями.
Будет использоваться DI или нет — это выбор программиста, а не решение, которое принимает архитектор. Это, — использовать DI, как руки мыть перед едой, какая еще архитекрута...shuron
01.06.2018 13:57Это вы из какой-то методики цитируете или сами опредилили так? Например жесткие роли.
Fesor
01.06.2018 17:18это выбор программиста, а не решение, которое принимает архитектор.
кто такой архитектор? Зачем он нужен? в чем его функция?
Fesor
01.06.2018 17:16архитектура портов и адаптеров (hexagonal которая) полностью построена на идее dependency inversion.
SOLID это принципы, набор ограничений. Архитектура приложения это совокупность всех ограничений, которые мы накладываем на систему. Все это является частью процесса проектирования. Как выбор ограничений так и их применение.
rjhdby
31.05.2018 14:49SOLID — это не дизайн и не архитектура — это набор рекомендаций, применение которых имеет как свои плюсы, так и свои минусы.
По сути эти рекомендации задают договоренности по ограничениям(constraint) структуры кодовой базы с целью облегчения ее понимания всем с ней работающим.aosja
31.05.2018 15:04SOLID —… это набор рекомендаций
Да, я в курсе. Но все же, не просто рекомендаций на любой случай жизни, а для конкретной области. Возможно, я не так выразился первый раз… Я считаю, что SOLID, это принципы дизайна кода (который я просто назвал дизайном), но не архитектурные принципы.
DistortNeo
31.05.2018 12:35А зачем городить такую монструозную конструкцию:
class AreaCalculator { public function calculate($shape) { $area = 0; $area = $shape->calculateArea(); return $area; } } $circle = new Circle(5); $obj = new AreaCalculator(); echo $obj->calculate($circle);
Когда гораздо проще писать так:
$circle = new Circle(5); echo $shape->calculateArea();
evgwed
31.05.2018 12:51Стоит отметить, что SOLID принципы делают разработку более долгой и сложной. Требуется создавать множество интерфейсов и абстрактных классов, что может показаться избыточным и ненужным.
Но при доработке или расширении вашего приложения вы поймете для чего все это было нужно.
Вывод: SOLID принципы хороши в больших и продолжительных проектах, на коротких проектах это долго и дорого.DistortNeo
31.05.2018 13:03+1Дело даже не в SOLID, а в тестируемости кода. Код, написаный по SOLID, тестировать легко. Код, который не следует SOLID, тестировать сложно.
Если у вас есть необходимость тестировать код (или даже использовать TDD), то вам придётся следовать принципам SOLID, иначе вы попросту не сможете писать тесты.
То есть всё зависит от того, нужно ли писать тесты или нет. На коротких и неподдерживаемых проектах тесты действительно замедляют разработку. На серьёзных же проектах без тестов никуда: выигрыш времени на старте приведёт к значительно большим потерям в будущем.VolCh
31.05.2018 17:46Сильно зависит от языка, например. В некоторых языках мокнуть что-то конкретное у конкретного «продакшен» класса ничуть не сложнее, а то и проще, чем создать специальную тестовую реализацию какой-то абстракции.
evgwed
01.06.2018 10:45По SOLID можно писать без тестов, но вот тесты нельзя писать по SOLID.
Для того, чтобы тесты работали не обязательно следовать каждому принципу, до большинства вещей доходят при расширении приложения.DistortNeo
01.06.2018 10:58Написание хороших тестов автоматически принуждает к использованию SOLID, даже если программист вообще не знает, что это такое.
Для того, чтобы тесты работали не обязательно следовать каждому принципу, до большинства вещей доходят при расширении приложения.
Принцип KISS тоже никто не отменял.
springimport
31.05.2018 16:22А нет какого-то принципа для решения следующей проблемы: имеется базовый класс с наследниками. Нужно сделать кэширование для некоторых методов.
Можно решить это добавлением кэширования в объект с которым работают классы. Но нужна реализация над классами.DistortNeo
31.05.2018 16:36Типа этого?
class Base { public: virtual int CalculateSomething(); }; class Derived: public Base { public: int CalculateSomething() override { ... } }; class Cached: public Base { private: std::unique_ptr<Base> base; std::optional<int> cachedValue; public: Cached(std::unique_ptr<Base> base): base(std::move(base)) {} int CalculateSomething() { if (!cachedValue) { cachedValue = base->CalculateSomething(); } return cachedValue.value(); } };
springimport
31.05.2018 16:52Не это. Для упрощения можно убрать метод в базовом классе и оставить только в наследниках. Каждый наследник считает что-то свое и это нужно закэшировать. Причем некоторые наследники могут остаться не закэшированными.
Можно, конечно, написать для каждого прокси, но это не элегантное решение.DistortNeo
31.05.2018 17:12Для упрощения можно убрать метод в базовом классе и оставить только в наследниках.
Тогда причём здесь вообще базовый класс?
VolCh
31.05.2018 17:47декоратор?
springimport
31.05.2018 18:14Это решение примерно как прокси. Если нужно задекорировать модель с данными то все усложняется.
Dywar
31.05.2018 19:22Нужно кэшивароние — прокси.
Нужен lazy load — прокси.
Нужен контроль доступа — проски.
Нужно имитировать/эмулировать/создать динамическое наследование — декоратор.
greabock
31.05.2018 20:34+2Я не знаю откуда столько плюсов. Примеры очень плохие.
S
У нас был килограмм гуано. После "рефакторинга", мы получили четыре куска гуано по 250 грамм.
O
Показан пример (довольно слабый) с полиморфизмом.
Не показан пример с наследованием.
L
ОЧЕНЬ слабый пример. Который просто говорит о соблюдении интерфейсов, и вообще не затрагивает тот момент, что принцип подстановки при указании возвращаемых значений в PHP работает неправильно. Не разобраны примеры с усилением предусловий и ослаблением постусловий.
I
Ну а где пример с композицией интерфейсов?
D
Ну тут ладно, тут попадание.
Почему инверсия зависимостей идет бок о бок с внедрением зависимостей?
Ну потому, что последнее невозможно без соблюдения первого.greabock
31.05.2018 20:42при указании возвращаемых значений
при указании типов входящих значений, простите
примеры с усилением предусловий и ослаблением постусловий
примеры с ослаблением предусловий и усилением постусловий, конечно же. Опечатался.
Fesor
01.06.2018 17:19Не показан пример с наследованием.
а зачем его показывать? composition over inheritance и все такое. OCP это НЕ про наследование. Это про точки расширения. Точки расширения за счет того что у вас класс не final это так себе решение.
что принцип подстановки при указании возвращаемых значений в PHP работает неправильно.
не неправильно, а просто не умеет в ковариантность/контрвариантность для пре/пост кондишенов. Этим болеют многие языки и анализаторы. да неудобно но жить можно.
Ну потому, что последнее невозможно без соблюдения первого.
наоборот, инверсия зависимостей требует наличия инверсии управления (DI это как раз реализация второго).
BradMaslov
02.06.2018 01:19Тема SOLID, к сожалению, осталась не раскрытой. Нужны примеры по лучше.
Сразу вспоминаю учебу в университете. Тогда его SOLID это не называли (по крайней мере я об этом не слышал тогда) но в книгах по проектированию можно было встретить каждый из этих принципов, который "рекламируют" сейчас как SOLID.
Тогда мой неокрепший ум, начитавшись таких принципов, сразу ринулся распылятся паттернами и принципами направо и налево. Продвигание ООП преподавателями — крепко осело в моем мозгу — "Наследование, полиморфизм, инкапсуляция"...
Я к тому, что новичкам это конечно полезно знать, но это все таки слишком сложная тема, что бы так вот просто (и местами плохо) её изложить. Понимание и целесообразность SOLID и принципов ООП без опыта не придет. Хоть 1000 статей на эту тему сделать — толку мало будет.
Проблема в том, что на сегодняшний день куча технологий и архитектурных решений продвигаются как панацея. Люди продвигают то, что знают или видели. Но только обладая опытом и поддавая все критике можно сбалансировать все эти знания. Вот этот вот последний абзац, нужно прилагать в конце каждой статьи. Что бы наивный неокрепший ум знал, что юзать можно, но ооочень осторожно) А то хуже будет.
Fesor
02.06.2018 19:08Нужны примеры по лучше.
примеры по solid надо рассматривать исключительно в динамике. Я не представляю как принцип описываемый фразой "единая причина для изменений" можно продемонстрировать без этих самых изменений.
можно было встретить каждый из этих принципов
package principle появились в 98-ом вроде, а то и раньше. LSP и OCP — 88-ой. DIP — в целом существовал очень давно. И все эти штуки основаны либо на логике хоара (60-ые) либо на принципах структурного дизайна (1970).
Продвигание ООП преподавателями — крепко осело в моем мозгу — "Наследование, полиморфизм, инкапсуляция"...
В моей памяти как-то особо упор на наследовании делали. От инкапсуляции только data hiding, и то вскользь. Да и полиморфизм только в том контексте который родился из примеров наследования. Я могу объяснить это тем, что это проще показать чем саму идею. Это просто фичи языка. А про связанность, cohesion, information hiding… все это как-то опускается.
но это все таки слишком сложная тема, что бы так вот просто (и местами плохо) её изложить.
тема не сложная, просто это к вопросу о последовательном обучении.
Проблема в том, что на сегодняшний день куча технологий и архитектурных решений продвигаются как панацея.
очень важный момент что как панацею эти решения продвигают немного другие люди, нежели те кому эти технологии приписывают. А тут нужно развивать скил критического мышления и анализа поступающей информации. Особенно если тебе сыпят маркетинговыми терминами или категоричными заявлениями. Увы этому в школах/универах почти не учат.
greabock
02.06.2018 13:27OCP это НЕ про наследование
и НЕ про композицию. А потому, примеры нужны разные.
не умеет в ковариантность/контрвариантность для пре/пост кондишенов
таки с постусловиями там всё норм.
dependency inversion
— вообще не интересует, каким образом поставляются зависимости, ты можешь делать это хоть в императивном стиле, хоть в декларативном через механизм DI. Ничего инверсия зависимостей не требует.
evseev
02.06.2018 12:49По-моему эта статья объясняет только одно. Не стоит слепо следовать ни одному из принципов. Какими-бы замечательными они не казались. Возьмем пример из «Принцип разделения интерфейса». Мы замечательно разделили интерфейс. Разве не чудесно? С первого взгляда да. Но давайте пойдем чуть-чуть дальше. Мы ведь теперь хотим работать с этими объектами. Мы создаем некоторое количество воркеров обоих типов. И даже складываем их в какую-то структуру данных. И теперь хотим каким-то образом их запустить, а потом отправить на отдых. Но просто шарахнуть циклом мы не можем. Объекты вроде-бы и очень похожие, но вот методы не совпадают. Т.е. нам нужна дополнительная проверка. Если мы их куда-то передаем, то там нужно учитывать эту разность. Я просто предчувствую как обрадуется этому факту наши коллеги. В этом случае объектов очень мало. А представим, что это реальный проект и таких объектов 2-3 десятка. И, скажем, 3 из них еще нигде не встречаются. Это новая ветвь развития рабочих. Вы все чудно отладили и отдали заказчикам. И вот в один прекрасный момент оказывается, что один из заказчиков таки запустил эту новинку. Но их пока мало. Поэтому все работает пока мы не доходим до нового образца. И вот именно тогда все вдруг улетает к чертовой бабушке. Мы перезапускаем процесс и опять все хорошо. И все хорошо часами, сутками, неделями. И… вот опять все рухнуло. И ведь проект собирается, и выглядит все замечательно, но заказчик почему-то не радуется, а больше волком смотрит. Но зато все по SOLID.
Fesor
02.06.2018 18:26Но просто шарахнуть циклом мы не можем.
мораль — никто опять ничего не понял.
Суть сегрегации интерфейсов не в том что бы разделить интерфейсы, а в том что бы интерфейсы проектировать специализированные, под конкретную задачу, под конкретный клиентский код, что бы скрыть деталей по максимуму, уменьшить влияние этих зависимостей на клиентский код.
А то что вы описали это не про SOLID. Это про то, как люди думаю что понимают в чем смысл SOLID а на самом деле занимаются фигней.
PashaNedved
Вопрос на миллион, насколько «общей» должна быть эта задача?
AstarothAst
А ответ на миллион заключается в том, что в реальной жизни какой-нибудь сервис авторизации выполняет кучу совершенно разных действий, но при этом внезапно начинает соответствовать SOLID, как только появляется формулировка «он решает задачу авторизации пользователей»! С формальной точки зрения все зависит от того, как дробишь задачи, как группируешь. С практической нужно для начала подумать чего мы SOLIDом добиваемся, а дальше уже смотреть в конкретный случай — добились? не добились?
PashaNedved
Формально, первый пример в статье соответствует SRP — предоставляет отчет по заказам. А практически SRP предлагает нам стать Вангой 99lvl. Я ничего не имею против принципа SRP при создании фреймворка, например, но когда дело доходит до предметной области, то приходит осознание непонимания этого принципа и как ему следовать не нарушая идей ООП.
Fesor
Интересный факт. В книжке Дяди Боба, где он SOLID расписывает, вся эта тема идет после главы о рефакторинге и постоянном улучшении кода.
Это я к чему, попробуйте подумать о solid с другой стороны. Когда у вас код уже написан и вы вносите очередные правки. Все еще ли с этими правками код соблюдает solid или нам надо разбить этот класс и повыносить часть логики?
Пытаться upfront сделать все по solid невозможно. Ведь даже если вы сможете в 100% случаев все делать соблюдая SRP то с OCP уже все намного сложнее.
tangro
Я бы сказал так — задача должна выражаться одним предложением, в котором нет сложноподчинённых частей и частицы «и».
Dreyk
ну так "авторизация пользователей". тут нет "и", но сервис и в базу лезет, и ответ форматирует, и пиццу выносит авторизированным пользователям
VolCh
три «и» :)
Valle
«Поиск в интернете»