товары

Открыто / Закрыто, по принципу ТВЕРДЫХ

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

Разработка программного обеспечения: модулей, классов и функций таким образом, чтобы при необходимости новой функциональности мы не изменяли существующий код, а, скорее, писали новый код, который будет использоваться существующим кодом. Это может показаться странным, особенно с такими языками, как Java, C, C ++ или C #, где это применимо не только к самому исходному коду, но и к двоичному файлу. Мы хотим создавать новые функции таким образом, чтобы не требовалось перераспределение существующих двоичных файлов, исполняемых файлов или библиотек DLL.
OCP в контексте SOLID

 

Дополнительные SRP и OCP

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

 

Пример нарушения принципа ОСР

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

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

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

 

function testItCanGetTheProgressOfAFileAsAPercent () {
     $ file = новый файл ();
     $ file-> length = 200;
     $ файл-> отправлено = 100;
     $ progress = новый прогресс ($ file);
     $ this-> assertEquals (50, $ progress-> getAsPercent ());
}

В этом коде мы являемся пользователями Progress. Мы хотим получить значение в процентах, независимо от фактического размера файла. Мы используем файл как источник информации. Файл имеет длину в байтах и ​​поле с именем sent, которое представляет объем данных, отправленных загрузчику. Нам все равно, как эти значения обновляются в приложении. Мы можем предположить, что есть какая-то магическая логика, которая делает это за нас, поэтому в тесте мы можем явно установить их.

 

файл класса {
     public $ length;
     публичный $ отправлен;
}

 

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

 

класс Прогресс {

     частный файл $;

     функция __construct (File $ file) {
          $ this-> file = $ file;
     }

     function getAsPercent () {
          вернуть $ this-> file-> sent * 100 / $ this-> file-> length;
     }

}

Progress - это просто класс, который принимает файл в своем конструкторе. Для наглядности мы указали тип переменной в параметрах конструктора. В Progress есть единственный полезный метод, getAsPercent (), который берет отправленные значения и длину из File и преобразует их в проценты. Просто и это работает.

Этот код кажется правильным, однако он нарушает принцип открытия / закрытия.

Но почему?

И как?

 

Попробуем изменить требования

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

Нет мы не можем. Наш прогресс привязан к файлу. Он может управлять только информацией о файлах, но также может применяться к музыкальному контенту. Но для этого мы должны изменить его, мы должны сделать так, чтобы Progress узнал музыку и файлы. Если бы наш дизайн соответствовал OCP, нам не нужно было бы нажимать File или Progress. Мы могли бы просто повторно использовать существующий прогресс и применить его к музыке.

 

Инновационный бюллетень
Не пропустите самые важные новости об инновациях. Зарегистрируйтесь, чтобы получать их по электронной почте.

Возможное решение

Языки с динамической типизацией имеют преимущество управления типами объектов во время выполнения. Это позволяет нам удалить подсказку типа из конструктора Progress, и код продолжит работать.

класс Прогресс {

     частный файл $;

     функция __construct ($ file) {
         $ this-> file = $ file;
     }

    function getAsPercent () {
         вернуть $ this-> file-> sent * 100 / $ this-> file-> length;
     }

}

Теперь мы можем запускать что угодно в Progress. И под чем угодно я имею в виду буквально все:

класс Музыка {

public $ length;
публичный $ отправлен;

public $ artist;
публичный альбом $;
public $ releaseDate;

function getAlbumCoverFile () {
верните 'Images / Covers /'. $ this-> художник. '/'. $ this-> альбом. '.png';
}
}

И класс музыки, подобный приведенному выше, будет работать отлично. Мы можем легко проверить это с помощью теста, очень похожего на File.
function testItCanGetTheProgressOfAMusicStreamAsAPercent () {
$ music = новая музыка ();
$ music-> length = 200;
$ music-> отправлено = 100;

$ progress = новый прогресс ($ music);

$ this-> assertEquals (50, $ progress-> getAsPercent ());
}

Таким образом, с классом Progress можно использовать практически любой измеримый контент. Возможно, нам следует выразить это в коде, также изменив имя переменной:

класс Прогресс {

частный $ MeasurableContent;

функция __construct ($ MeasurableContent) {
$ this-> MeasurableContent = $ MeasurableContent;
}

function getAsPercent () {
вернуть $ this-> MeasurableContent-> sent * 100 / $ this-> MeasurableContent-> length;
}

}

Когда мы указали File в качестве подсказки типа, мы с оптимизмом смотрели на то, что может обрабатывать наш класс. Это было недвусмысленно, и если бы что-нибудь еще пришло, нам бы сказала большая ошибка.

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

Мы не хотим в конечном итоге пытаться вызывать методы или обращаться к полям объектов, которые не соответствуют нашему контракту. Когда у нас была типовая подсказка, контракт оговаривался ею. Поля и методы класса File. Теперь, когда у нас ничего нет, мы можем отправить что угодно, даже строку, и это приведет к серьезной ошибке.

Хотя конечный результат в обоих случаях одинаков, то есть код ломается, в первом случае получается хорошее сообщение. Однако это очень мрачно. Невозможно узнать, что это за переменная — в нашем случае это строка — и какие свойства искались, но не были найдены. Трудно отладить и исправить проблему. Программист должен открыть класс Progress, прочитать и понять его. Контракт в этом случае, когда вы явно не указываете typehint, defiзавершено поведением Прогресса. Это подразумеваемый контракт, известный только Прогрессу. В нашем примере это defiзавершается доступом к двум полям: send и length в методе getAsPercent(). В реальной жизни подразумеваемый контракт может быть очень сложным, и его трудно обнаружить, просто посмотрев несколько секунд в классе.

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

Ercole Palmeri

Инновационный бюллетень
Не пропустите самые важные новости об инновациях. Зарегистрируйтесь, чтобы получать их по электронной почте.

АРТИКОЛИ recenti

Издатели и OpenAI подписывают соглашения, регулирующие поток информации, обрабатываемой искусственным интеллектом.

В прошлый понедельник Financial Times объявила о сделке с OpenAI. FT лицензирует свою журналистику мирового уровня…

Апрель 30 2024

Онлайн-платежи: вот как потоковые сервисы заставляют вас платить вечно

Миллионы людей платят за стриминговые сервисы, выплачивая ежемесячную абонентскую плату. Распространено мнение, что вы…

Апрель 29 2024

Veeam предлагает наиболее полную поддержку программ-вымогателей: от защиты до реагирования и восстановления.

Coveware от Veeam продолжит предоставлять услуги по реагированию на инциденты, связанные с кибер-вымогательством. Coveware предложит возможности криминалистики и исправления…

Апрель 23 2024

Зеленая и цифровая революция: как прогнозируемое обслуживание меняет нефтегазовую отрасль

Прогнозируемое техническое обслуживание производит революцию в нефтегазовом секторе благодаря инновационному и упреждающему подходу к управлению предприятием…

Апрель 22 2024

Читайте «Инновации» на вашем языке

Инновационный бюллетень
Не пропустите самые важные новости об инновациях. Зарегистрируйтесь, чтобы получать их по электронной почте.

Следуйте за нами