Andriy Shyrokoryadov

.Net developer, data scientist

Принципы SOLID - вопрос №4 на собеседование по программированию

Текст к видео "SOLID - принцип открытости / закрытости" на канале YouTube

Приветствую Вас на моём канале. Как я и обещал в предыдущем видео сегодня мы рассмотрим более подробно принцип открытости / закрытости SOLID, который является вторым принцип и скрывается под буквой «О» в аббревиатуре. В сокращении данный принцип звучит следующим образом: «классы должны быть открыты для расширений, но закрыты для изменений». Здесь можно заметить, что в определении принципа есть противоречие – можно расширять, но нельзя изменять, а ведь часто изменение – это расширение. В данном видео мы разберем, что понимается под понятиями «расширение класса» и «изменение класса» и как это работает в программировании.

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

С другой стороны, «класс должен быть открытый для модификаций». Это выражение предполагает, что должны существовать способы добавления модификаций функциональности. Одним из этих способов является наследование. В случае наследования класс-наследник надписывает функциональности из класса-родителя. Здесь возникает проблема нарушения принципа D, так как мы делаем наш код зависимым от конкретных классов (в данном случае это класс-родитель, в предыдущем видео это был класс «Посылка»). Как следствие, изменения в конкретном классе родителя приведут к изменениям в классах-наследниках. Если мы изменяем некоторый класс и данные изменения требуют от нас сделать изменения в других частях кода, то, к сожалению, нельзя сказать, что наш класс написан в соответствии с принципом открытости / закрытости. В связи с этим использование механизма наследования не всегда позволяет реализовать принцип открытости / закрытости.

Как противопоставление «наследованию» существует понятие «композиции». В этом случае у нас есть возможность объединения в одном классе нескольких классов, которые реализуют отдельные функциональности. Естественно, класс в котором осуществляется композиция, должен зависеть от абстракций, а не от конкретных классов. В предыдущем видео мы воспользовались композицией, когда создавали классы – декораторов. Они объединяли в себе функциональность существующих классов (например, класс «Посылка») и измененную функциональность, например расчет стоимости доставки в декораторах транспортных компаний. В итоге, класс, который мы должны были изменить (класс «Посылка»), остался без изменений, но тем не менее создавая классы декораторов, нам удалость модифицировать класс «Посылка». Обе части принципа открытости / закрытости были выполнены.

Важно отметить, что между классами – существующими и их модификациями – должна быть связь. Данная связь выражается в наличии общего интерфейса. В примере из предыдущего видео это был базовый абстрактный класс PackageBase и интерфейс IPackage. И декорируемый объект, и классы – декораторы использовали этот класс и интерфейс. Здесь наследование использовалось не для того, чтобы изменить функциональность, а чтобы привести все типы к одному общему типу, чтобы потом эти типы взаимозаменять в коде в зависимости от потребностей. Может возникнуть хорошая идея сделать комбинацию шаблонов проектирования – можно декораторы создавать из фабрики декораторов на основании каких-то параметров. В итоге у нас будет комбинация 2 шаблонов: фабрика и декоратор.

Когда мы пишем код в соответствии с принципом открытости / закрытости мы должны помнить еще об одном принципе. Это принцип здравого смысла. Речь идет о том, что если бы хотели писать весь наш код в соответствии с принципом открытости / закрытости, то он получился бы сложным из-за необходимости использования дополнительных абстракций между существующими классами и их модификациями. Абстракция – это всегда усложнение. Возможно, вы слышали такое понятие, как «спагетти-код», которое означает, что в коде слишком много уровней абстракции. Когда уровней абстракций слишком много, чтение и понимание кода затруднено, осуществление изменений и модификации может привести к ошибкам.

Как можно сделать вывод? Исходя из нашего опыта и знаний на тему работы определенного приложения мы должны выделить относительно стабильные области приложения и области, которые подвержены изменениям. В областях подверженных изменениям необходимо внедрять принцип открытости / закрытости. То есть внедрение и реализация данного принципа должно осуществляться точечно. Пример шаблона «Декоратор» из предыдущего видео показывает, что данный шаблон является большим и немного запутанным, но его использование было оправдано – благодаря ему у нас появилась возможность комбинировать различные варианты доставки и платежей без особых усилий. С другой стороны, использование данного шаблона повсеместно в нашем коде было бы излишним.

Также следует помнить, что декорирование классов, которые реализуют большие интерфейсы (интерфейсы с большим числом членов), может быть трудоемко. Необходимо будет переопределить все члены интерфейса в декораторе и только лишь изменить один член, который, собственно, модифицирует функциональность. С другой стороны, мы пока оставим вопрос наличия интерфейсов с большим числом членов без ответа. Это будет темой одного из следующих видео на тему принципа I SOLID – принцип сегрегации интерфейсов.

На данный момент это наверно всё, что я хотел сказать на тему принципа открытости / закрытости. Спасибо за внимание. До новых встреч.