Страницы

Поиск по вопросам

пятница, 24 января 2020 г.

Как разрешать зависимости не подходящих друг другу классов в PHP (ООП)?

#php #ооп #шаблоны_проектирования


Представим, что нужно написать библиотеку, которая:


Получает товары из прайс-листа (артикул, цена), который может быть в разных форматах
Обрабатывает их
Сохраняет в базу.


Вопрос в правильной организации кода с точки зрения ООП.

Начнём создавать классы:

Readers/XmlReader.php
Readers/XlsReader.php
Readers/ReaderInterface.php

Handlers/DefaultHandler.php
Handlers/CustomHandler.php
Handlers/HandlerInterface.php

Savers/DefaultSaver.php
Savers/SaverInterface.php

Service.php - класс, в который внедряем зависимости
Item.php - сущности, которые будет создавать ридер


Начинаем эту красоту использовать и всё отлично работает:

$service = new Service(
    new XlsReader($path),
    new DefaultHandler(),
    new DefaultSaver()
);
$service->run();


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

Если так сделать, получится не универсальная система - нельзя просто взять и заменить
один Reader на другой (например XlsReader + NewHandler), поскольку это приведёт к ошибке.
Надо проверять, что они друг другу подходят. Теряется смысл интерфейсов.

Можно условиться, что у Item всегда есть 3 свойства, просто категория может быть
пустой. Но тогда получается, что мы нагружаем класс Item лишним функционалом, который
ему в большинстве случаев будет не нужен. Тоже плохо.

Как правильно поступать в данном случае с точки зрения ООП?

UPD1: уточню, что в базу всегда сохраняются только article и price. А другие поля
(как category в моём примере) нужны только для Handler'ов, чтобы вели всякие расчёты
и меняли цену Item'а перед сохранением.

UPD2: может быть создать фабрику для этого?

public static function make(string $type)
{
    if($type === 'xls')
        return new Service(new XmlReader(), new DefaultHandler(), new DefaultSaver());
    elseif($type === 'csv')
        return new Service(new CsvReader(), new CsvHandler(), new DefaultSaver());
}


И вызывать так:

$service = ServiceFactory::make('xls');

    


Ответы

Ответ 1



Здесь проблема не в объектах, а в технических требованиях. Если речь идёт о фиксированном наборе атрибутов, проще всего увековечить его в коде в виде так называемого Объекта переноса данных, на английском Data Transfer Object, DTO. Это паттерн проектирования, в котором только поля примитивного типа и нет поведения. В вашей программе ему соответствует класс Item. Но если набор атрибутов может быть изменён, это надо отразить в явном виде. Если речь идёт о редких изменениях, то можно остановиться на ручной правке кода. Как гарантировать, что при добавлении нового поля вы добавите код в все классы чтения и классы обработки? Например, с помощью модульных тестов. Придётся опираться на механизм рефлексии: перебрать все поля класса Item, и убедиться, что все из них имеют значения. Если вы добавите новое поле, которому не будет присвоено значение, оно должно быть равным null, и в этом случае тест должен проваливаться. Если же речь идёт о том, чтобы пользователь мог добавлять и убирать атрибуты по своему желанию, то эта задача гораздо более сложная. Её тоже делают, но надо иметь в виду, что хорошего решения на классических реляционных базах она не имеет. Посмотрите в сторону паттерна Сущность-Атрибут-Значение (Entity-Attribute-Value). Вот, например, есть статья на хабре на эту тему. Другим решением в современных СУБД являются XML или JSON поля. Это решение мне кажется более простым, чем EAV. В этом случае класс для чтения должен возвращать схему (описание столбцов) и сами значения, а класс для обработки должен уметь сохранять нефиксированный набор столбцов. В сложных случаях вам может также понадобиться конвертер (или описание способов конвертации их исходной схемы в целевую). Очень надеюсь, что ничего подобного вашим заказчикам не нужно. Наконец, ещё один способ, когда набор полей может быть изменён, но это может случиться только один раз. Заказчик говорит вам, какие атрибуты применяются в его задачах, а вы быстро адаптируете свой код. Тут хорошо подойдут скрипты кодо-генерации. Тогда у вас может быть один JSON с описанием схемы, по которой будет идти генерация, и скрипт, который сгенерирует Item, классы чтения и классы обработки по вашим образцам.

Комментариев нет:

Отправить комментарий