Показаны сообщения с ярлыком solid. Показать все сообщения
Показаны сообщения с ярлыком solid. Показать все сообщения

суббота, 25 июня 2016 г.

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

Принцип разделения интерфейса (ISP), четвертый из принципов SOLID

Клиенты не должны зависеть от методов, которые они не используют.

Или "меньше знаешь - крепче спишь". Принцип разделения интерфейсов говорит о том, что слишком «толстые» интерфейсы необходимо разделять на более маленькие и специфические, чтобы клиенты маленьких интерфейсов знали только о методах, которые необходимы им в работе. В итоге, при изменении метода интерфейса не должны меняться клиенты, которые этот метод не используют.

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

Использование же "толстых" интерфейсов приводит к:

  • появлению множества "заглушек", и как следствие - ко множеству лишнего кода, который сложно поддерживать
  • соблазну нарушить принцип SRP, поскольку клиентский код "знает" о переданном классе, реализующем такой интерфейс, слишком много

вторник, 14 июня 2016 г.

Принцип открытости/закрытости (Open-closed principle)

Принцип открытости/закрытости (OCP), второй из принципов SOLID

Программные сущности должны быть открыты для расширения, но закрыты для модификации.

Как и принцип единственной ответственности применим не только к классам, но и к модулям, методам и т.д.

Все программные продукты в течение своего жизненного цикла изменяются. Изменения могут возникнуть, например, из-за изменения или дополнения бизнес-логики, бизнес-процессов и т.п. Как результат - необходимость изменять или дополнять уже существующий код в соответствии с новыми требованиями.

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

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

Применительно к ООП этот принцип можно реализовать двумя способами:

  • изменять функциональность через композиию, заменяя один класс в композиции другим, реализующим тот же интерфейс, что и заменяемый.
  • расширять функциональность через наследование, расширяя существующие функции класса.

Первый как правило использует паттерн Startegy (Стратегия) для внедрения зависимостей, второй - Template method (Шаблонный метод). Оба способа правильные, каждый нужно использовать исходя из свойств соответствующих отношений.

Наиболее характерное нарушение этого принципа - использование отношения "ассоциация", то есть прямое обращение к объекту или его создание (кроме тех случаев, когда создание другого объекта - непосредственная функция данного). Например, статическое обращение к системе логирования или каждый раз порождение нового объекта:

class FirstLogger
{
    /**
     * @return FirstLogger
     */
    public static function me() { /* реализуем синглетон */ }

    public function log($message) { /* логируем */ }
}

class SecondLogger
{
    public function log($message) { /* логируем */ }
}

class Foo
{
    public function doSomething()
    {
        // ...
        FirstLogger::me()->log('Some log info');
        // ...
        (new SecondLogger())->log('Some log info');
        // ...
    }
}

// Использование
(new Foo())->doSomething();

Недостатки такой реализации очевидны: замена, например, классов *Logger на другой класс влечет за собой значительные изменения. Так же проблематично писать юнит-тесты для подобного кода.

Тот же код с применением принципа открытости/закрытости:

class FirstLogger
{
    /**
     * @return FirstLogger
     */
    public static function me() { /* реализуем синглетон */ }

    public function log($message) { /* логируем */ }
}

class SecondLogger
{
    public function log($message) { /* логируем */ }
}


class Foo
{
    /** @var FirstLogger */
    public $firstLogger;

    /** @var SecondLogger */
    public $secondLogger;

    public function __construct(FirstLogger $firstLogger, SecondLogger $secondLogger)
    {
        $this->firstLogger = $firstLogger;
        $this->secondLogger = $secondLogger;
    }

    public function doSomething()
    {
        // ...
        $this->firstLogger->log(('Some log info'));
        // ...
        $this->secondLogger->log('Some log info');
        // ...
    }
}

// Использование
(new Foo(FirstLogger::me(), new SecondLogger()))->doSomething();

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

Еще пример. Допустим в системе есть класс, записывающий лог в файл first_log.log:

class FirstLogger
{
    public function log($message)
    {
        $handler = fopen('first_log.log', 'w+');
        fwrite($handler, sprintf('[%s]%s%s', date('c'), $message, PHP_EOL));
        fclose($handler);
    }
}

// Использование
(new FirstLogger())->log('Some message');

В такой реализации класс полностью закрыт для расширения. Необходимость логирования в другой файл приводит или к переделке интерфейса метода log($message), так, чтобы он принимал имя файла как параметр, или написанию класса, почти полностью дублирующего данный:

class FirstLogger
{
    public function log($file, $message)
    {
        $handler = fopen($file, 'w+');
        fwrite($handler, sprintf('[%s]%s%s', date('d-m-Y H:i:s'), $message, PHP_EOL . PHP_EOL));
        fclose($handler);
    }
}

class SecondLogger
{
    public function log($message)
    {
        $handler = fopen('second_log.log', 'w+');
        fwrite($handler, sprintf('[%s]%s%s', date('d-m-Y H:i:s'), $message, PHP_EOL . PHP_EOL));
        fclose($handler);
    }
}

// Использование
(new FirstLogger())->log('second_log.log', 'Some message');
(new SecondLogger())->log('Some message');

Тот же код с применением принципа открытости/закрытости, опять через внедрение зависимостей:

class FileLogger
{
    private $logFile;

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

    public function log($message)
    {
        $handler = fopen($this->logFile, 'w+');
        fwrite($handler, sprintf('[%s]%s%s', date('c'), $message, PHP_EOL));
        fclose($handler);
    }
}

// Использование
(new FileLogger('first_log.log'))->log('Some message');
(new FileLogger('second_log.log'))->log('Some message');

Но класс по прежнему не полностью открыт для расширения. Точнее открыт исключительно настолько, насколько требуется для логирования в разные файлы. Теперь представим, что нужно будет писать логи еще и в удаленную систему (Graylog, Elastic, etc). Да еще и в разных форматах для каждой. Уже не обойтись без абстракции:

abstract class AbstractLogger
{
    public function log($message)
    {
        $this->connect();
        $this->write(
            $this->formatMessage(
                $message
            )
        );
        $this->disconnect();
    }

    /**
     * @return void
     */
    abstract protected function connect();

    /**
     * @param string $message
     * @return void
     */
    abstract protected function write($message);

    /**
     * @return void
     */
    abstract protected function disconnect();

    /**
     * @param string $message
     * @return string
     */
    abstract protected function formatMessage($message);
}

class FileLogger extends AbstractLogger
{
    private $logFile;

    private $handler;

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

    protected function connect()
    {
        $this->handler = fopen($this->logFile, 'w+');
    }

    protected function write($message)
    {
        fwrite($this->handler, $message);
    }

    protected function disconnect()
    {
        fclose($this->handler);
    }

    protected function formatMessage($message)
    {
        return sprintf('[%s]%s%s', date('c'), $message, PHP_EOL);
    }
}

Теперь класс полностью открыт для расширения, при этом нет необходимости изменять код базового и уже реализованных классов при расширении системы.

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

Принцип единственной ответственности (Single responsibility principle)

Принцип единственной ответственности (SRP), первый из принципов SOLID.

На каждый объект должна быть возложена одна единственная обязанность.

То есть — конкретный класс должен решать конкретную задачу — ни больше, ни меньше. Применим не только к классам, но и к методам, модулям, подсистемам и системам. Старое доброе правило "разделяй и властвуй", изложенное для программирования. Следование этому принципу упрощает отладку, написание тестов и делает код более читаемым.

Существует ряд патологических случаев нарушения принципа единственной обязанности, которые сегодня будут очевидны практически любому:

  • Крайний - антипаттерн GodObject.
  • классы бизнес-логики, которые "знают" о пользовательском интерфейсе или о базе данных.
  • "толстые" контроллеры в шаблоне MVC. Обязанности контролера - увязать между собой модель бизнес-логики и представление. То есть, принять данные извне, передать их в модель, получить результат раболты модели и передать его в представление. Ни больше, ни меньше.

Менее очевидные нарушения есть, как ни странно, в ряде известных паттернов:

  • Singleton и его расширение - Multiton. Здесь "подмешан" контроль за количеством порождаемых объектов никакого отношения не имеющий к основным функциям, реализуемых классами на основе этих шаблонов.
  • Active Record, строго говоря, противоречит этому принципу - с одной стороны он является сущностью (entity), с другой - объектом доступа к хранилищу тех же самых сущностей (DAO). Даже если его "правильно готовить", то в нем сосредотачиваются как свойства сущности, так и атомарные методы поиска этих сущностей (ofSomeProperty(property: PropertyType)).

Разделять классы на составляющие необходимо руководствуясь здравым смыслом и моделью предметной области. В противном случае можно сделать только хуже. Один из таких примеров - антипаттерн Anemic Domain Model.

воскресенье, 12 июня 2016 г.

Принципы. SOLID.

SOLID - акроним из первых букв названий пяти принципов разработки программного обеспечения, названных Робертом Мартином в начале 2000-х.

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

Принципам не нужно бездумно следовать. Когда приложение относительно не велико и не предвидится его рост, то дизайн как таковой вообще не нужен. Достаточно аккуратно написать несколько классов и методов. Проблемы возникают, когда система растет и увеличивается. Поскольку сложность растет не линейно при увеличении размера приложения приходится подходить к дизайну более осмысленно, вспоминать всякие умные слова: абстракция, сокрытие информации и т.п., лучше продумывать обязанности каждого класса, чтобы у разработчика сегодня, завтра и через год была возможность понять написанный код и сосредоточиться на главной проблеме класса/модуля, не обращая внимания на второстепенные для решения текущей задачи детали. Другими словами: если есть хотя бы небольшая вероятность, что приложение будет расти и есть время на обдумывание, то лучше следовать принципам сразу. Потом времени и ресурсов на наведение порядка в коде может и не быть, а с заложенными изначально в проект проблемами прийдется мирится весь его жизненный цикл или затевать масштабный рефакторинг.

Принцип единственной ответственности (Single responsibility principle, SRP).

На каждый объект должна быть возложена одна единственная обязанность.

Другими словами — конкретный класс должен решать конкретную задачу — ни больше, ни меньше. Не должно быть больше одной причины для изменения класса, иначе получим хрупкий дизайн: правим одно - ломаем другое. Можно применить не только к классам, но и к методам, модулям, подсистемам и системам.

Принцип открытости/закрытости (Open-closed, OCP)

Программные сущности должны быть открыты для расширения, но закрыты для модификации.

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

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

Объекты в программе могут быть заменены их наследниками без изменения свойств программы.<.p>

Наследующий класс должен дополнять, а не замещать поведение базового класса.

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

Клиенты не должны зависеть от методов, которые они не используют.

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

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

  • Модули верхних уровней не должны зависеть от модулей нижних уровней. Оба типа модулей должны зависеть от абстракций.
  • Абстракции не должны зависеть от деталей. Детали должны зависеть от абстракций.

Другими словами: зависимости должны строится на абстракциях, а не на конкретных реализациях.