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