Принцип открытости/закрытости (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); } }
Теперь класс полностью открыт для расширения, при этом нет необходимости изменять код базового и уже реализованных классов при расширении системы.
В заключении замечу, что применяя принцип открытости/закрытости совместно с принципом единственной ответсвенности получаем интересный результат: система "строится" из небольших "кирпичиков", каждый из которых может быть легко заменен на другой, тем самым нужным образом изменив функциональность системы в целом. При этом затраты на разработку и тестирование минимальны, поскольку затрагивается только необходимая часть кода.
Комментариев нет:
Отправить комментарий