Чистый код: Принцип открытости закрытости (OCP)

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

Идеальной реализацией данного принципа является интерфейс. Ничего лишнего, нечего модифицировать, можно только расширять.

class IMyInterface { public: virtual void execute() = 0; };

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

interface IRepository<T> : IDisposable where T : class { IEnumerable<T> GetBookList(); // получение всех объектов T GetBook(int id); // получение одного объекта по id void Create(T item); // создание объекта void Update(T item); // обновление объекта void Delete(int id); // удаление объекта по id void Save(); // сохранение изменений }

Здесь представлены сразу несколько нарушений ограничивающих гибкость кода. Начнем с первого же метода, который возвращает все объекты из условного хранилища. Хочется задать вопрос, а зачем все объекты нужны разом, не проще ли сразу сделать отбор. Если мы в качестве параметра добавим предикат, то нарушим принцип открытости закрытости для данного интерфейса. Следовательно метод лучше выделить в отдельный интерфейс.

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

Остались последние четыре метода. Очевидно, что автор примера пытался применить понятие CRUD — акроним, обозначающий четыре базовые функции, используемые при работе с базами данных: создание (англ. create), чтение (read), модификация (update), удаление (delete) (1). Но даже в определении сказано, что это только сокращение, не требующее строгого соответствия. Следовательно и включать все эти методы в один класс не целесообразно.

Здесь стоит сделать отступление. Искушенный читатель, может указать, что реализация CRUD в одном классе является шаблоном ActiveRecord (2). Но сам шаблон является довольно спорным. Читателю стоит ознакомиться с определением, материалами в примечаниях, и самому принять решение, использовать ли его в разработке.

Кстати, автору приведенного выше примера, при его дальнейшей реализации, пришлось отказаться от использовании метода сохранения. Оставив его пустым. Ведь все методы интерфейса подлежат обязательной реализации. Нарушив принципы SOLID, даже в небольшом примере, был создан артефакт.

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

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

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

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

Диаграмма разделения на классы<br />
Диаграмма разделения на классы

Согласно условию задачи, мы должны иметь возможность расширить функционал не нарушая описываемого принципа. Класс Product может быть расширен путем добавления полей в дочерних классах. Инструмент преобразования элемента в строку мы можем создавать для каждого дочернего класса с данными, не исключая базового. К тому же можем создавать новые форматы преобразования отличные от JSON, не меняя структуру программы. Класс сериализации массива данных, обеспечивает форматирование только концевых элементов, следовательно его можно изменять, когда существенно меняется формат данных, например с JSON на XML, или SQL.

Поясняющая диаграмма классов выглядит следующим образом.

Диаграмма классов.<br />
Диаграмма классов.

Ниже приведен полный код примера.

enum Classifier { NONE, CEREALS, DRINKS, PACKS }; class Product { private: std::string m_name; // Наименование товара Classifier m_category; // Классификатор товара double m_price; // Цена public: Product(std::string name, Classifier category, double price) : m_name(name), m_category(category), m_price(price) {} std::string name() const { return m_name; } Classifier classifier() const { return m_category; } double price() const { return m_price; } }; class ISerialize { public: virtual std::string serialize(const std::shared_ptr<Product> obj) const = 0; }; class ProductToJSON : public ISerialize { public: virtual std::string serialize(const std::shared_ptr<Product> obj) const { std::string str; str += "{name:" + obj->name() + ","; str += "classifier:" + std::to_string(obj->classifier()) + ","; str += "price:" + std::to_string(obj->price()) + "}"; return str; }; }; class ISerialization { virtual void serialization() = 0; virtual std::string str() const = 0; }; class SerializationToJSON : public ISerialization { private: std::stringstream sstream; std::list<std::shared_ptr<Product>> products; std::shared_ptr<ISerialize> serializer; public: SerializationToJSON(std::shared_ptr<ISerialize> serializer, const std::list<std::shared_ptr<Product>>& products) { this->serializer = serializer; this->products = products; } virtual void serialization() { sstream << "["; for (auto& elem : products) { sstream << serializer->serialize(elem); } sstream << "]"; } virtual std::string str() const { return sstream.str(); } }; int main() { std::list<std::shared_ptr<Product>> products{ std::make_shared<Product>("Product 1", Classifier::CEREALS, 500), std::make_shared<Product>("Product 2", Classifier::DRINKS, 400), std::make_shared<Product>("Product 3", Classifier::PACKS, 300) }; SerializationToJSON serializer(std::make_shared<ProductToJSON>(), products); serializer.serialization(); std::cout << serializer.str() << std::endl; return 0; }
  1. РУВИКИ - CRUD
  2. РУВИКИ - ActiveRecord
11
2 комментария

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

Ответить

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

1
Ответить