Краткое руководство, которое поможет вашему коду продвинуться дальше
Хранение конкретных реализаций на периферии программного обеспечения — это хорошо известная передовая практика в разработке программного обеспечения, и она обычно хорошо работает в качестве эвристики для создания мощных объектно-ориентированных структур и простого в обслуживании кода.
Общая идея заключается в том, чтобы классы знали интерфейсы друг друга (например, какие операции могут быть вызваны для каждого из них), не зная их конкретной реализации или «типа данных». Например, к какому конкретному классу или подклассу принадлежит объект).
Обычно эта практика проектирования достигается посредством определения программных зависимостей с точки зрения интерфейсов, таким образом оставляя некоторый механизм внедрения зависимостей для определения того, какие конкретные реализации следует использовать во всем потоке выполнения.
Конкретные реализации часто определяются в начале потока выполнения, на самых первых уровнях программного обеспечения, перед входом в основные компоненты. Мы называем эти места «краями программного обеспечения», которые соответствуют фактической точке входа при каждом его выполнении.
Сделав скучное теоретическое введение в ООП, я много раз сталкивался с конкретным проблемным паттерном за несколько лет работы PHP-разработчиком. Здесь я описываю это и даю некоторые идеи, которые помогли мне справиться с этим в различных сценариях, пытаясь придерживаться вышеуказанной практики проектирования.
Проблема
Проблемный шаблон можно описать как наличие класса, который использует результат, предоставляемый другим. Результат определяется в терминах универсального интерфейса, поэтому мы можем знать некоторые операции, которые можно вызывать в экземпляре. Однако результат также содержит некоторые специфические операции и фрагменты данных, которые хочет использовать конечный потребитель.
Потребитель должен иметь возможность работать с различными возможными экземплярами результатов, реализуя различные алгоритмы обработки результатов в зависимости от типа каждого результата. Как мы можем добиться этого, не распространяя детали реализации по всему коду?
Я знаю, что это немного утомительно для понимания, поэтому давайте поработаем над этим на конкретном примере, используя настройку шаблона проектирования Command:
Несмотря на количество файлов, эту настройку легко понять: есть интерфейс Command
, который возвращает экземпляр CommandResult
в результате выполнения определенной команды. У нас есть класс ExecutionContext
, который определяет общую структуру конкретного контекста.
Контекст будет отвечать за обработку (выполнение и обработку результата) каждой команды. Мы видим, что каждый конкретный подкласс результата команды содержит разные операции, но класс ExecutionContex
t знает только об интерфейсе CommandResult
. Как мы могли бы соответствующим образом обработать каждый тип результата команды?
Это было моим заклятым врагом во многих случаях. Отсутствие подсказок полиморфного типа и обобщений в PHP затрудняет работу в этих сценариях; в отличие от Java, у которой есть хорошие собственные решения для таких случаев. В любом случае, я могу выделить три полезных подхода, которые позволили мне успешно решить эту проблему в прошлом:
- Тип, приводящий общий результат к ожидаемому типу
- Использование шаблона
Visitor
- Реструктуризация дизайна компонентов
Преобразование общего результата в ожидаемый
Честно говоря, это мое наименее предпочтительное решение. Поскольку мы знаем, что за экземпляром CommandResult
будет конкретный экземпляр класса, мы могли бы попытаться предоставить разные реализации для каждого конкретного экземпляра следующим образом:
Реализация processResult
спрашивает, какой конкретный тип имеет результат, чтобы обработать его соответствующим образом. Поскольку каждый закрытый метод processCommandResultX
ожидает получения экземпляра CommandResultX
, как определено в его подписи, наша IDE может помочь нам с автозаполнением и подсказкой типа в каждом из этих методов.
Метод processResult
можно определить непосредственно в базовом классе ExecutionContext
, оставив определение каждого метода processCommandResultX
каждому подклассу.
Более того, базовый класс мог бы предоставлять реализации по умолчанию для каждого конкретного метода, наряду с реализацией по умолчанию, которая будет выполняться в случае, если ни один оператор instanceof
не поймает конкретный результат команды (хотя в этом случае мы можем захотеть выдать ошибку, предупреждающую нас, что мы забыли обработать его).
Более мощная и гибкая версия будет использовать шаблон Chain of Responsibility, чтобы определить каждую конкретную обработку как стальную цепь. Таким образом, сталь могла обрабатывать только CommandResultA
экземпляров, оставляя все остальные типы результатов для следующих сталей и так далее.
У нас могут быть стали либо в начале, либо в конце цепочки, которые выполняют дополнительную обработку для всех типов результатов, например, позволяя регистрировать данные для каждого результата, независимо от его типа.
Хотя наличие большого количества instanceof
проверок и методов обработки может быть немного сложным и многословным, этот первый подход удерживает конкретный код реализации на периферии программного обеспечения, пока мы рассматриваем классы контекста выполнения как часть краев. Таким образом, добавление нового типа результата команды влечет за собой:
- Добавление нового конкретного метода обработки в иерархию
ExecutionContext
и реализация его в дочерних классах - Добавление новой проверки «
instanceof
» в методеprocessResult
- При использовании цепочки ответственности добавление новой цепной стали и, возможно, изменение некоторых существующих.
Использование шаблона проектирования посетителей
С помощью шаблона проектирования Visitor
мы можем добиться более элегантной настройки компонентов, как показано ниже:
Здесь мы позволяем каждому конкретному результату управлять тем, как его следует обрабатывать: каждый ExecutionContext
имеет набор определенных методов обработки. Конкретный результат должен выбрать подходящий, реализуя метод processOnContext
.
Элегантность обеспечивается отсутствием необходимости проверять тип класса каждого результата с помощью операторов instanceof
. Однако этот подход не так уж отличается от предыдущего. Добавление новой команды вместе с новым типом результата команды повлечет за собой:
- Добавление нового метода
processCommandResultX
в базовый классExecutionContext
и реализация его в каждом конкретном подклассе контекста.
Реструктуризация дизайна
Если мы снова включим теоретический режим, мы можем подумать, что эта проблема является просто проблемой дизайна, и, таким образом, эта текущая установка не работает должным образом из-за некоторых неправильных представлений и предположений, которые были сделаны неправильно.
Во-первых, ExecutionContext
был разработан для обработки CommandResult
экземпляров. Но мы пытаемся обработать экземпляры CommandResultA
и CommandResultB
. Следовательно, мы смешиваем два разных уровня абстракции в одном классе. В любой точке кода мы должны работать либо с низким, либо с высоким уровнем абстракции, но не с обоими одновременно.
Во-вторых, CommandResultA
определяет совершенно новый метод getString
, который не определен на уровне родительского класса. То же самое происходит с CommandResultB
. Более того, оба типа результатов команд не взаимозаменяемы друг с другом.
Мы нарушаем принцип замещения Ликскоу, и это исходит из предположения, что CommandResultA
и CommandResultB
являются разновидностями CommandResult
, что, в свою очередь, неверно, поскольку у них нет ничего общего. Не используйте одни и те же поля данных и не выполняйте одни и те же операции.
Если вы заметите эти ошибки проектирования, это может привести к хорошим идеям для рефакторинга и перепроектирования текущего решения. Однако, поскольку я предполагаю, что две команды могут возвращать в качестве результатов разные поля данных, я определенно хочу обрабатывать их по-разному. Типы результатов команд различаются в зависимости от алгоритмов обработки. Следовательно, мы могли бы попытаться держать их обоих на самом краю программного обеспечения, как вы можете видеть ниже:
Я добавил ProcessingStrategy
набор классов, которые определяют, как должен обрабатываться каждый конкретный тип результата. ExecutionContext
now является основным компонентом и должен быть установлен в качестве стратегии обработки перед отправкой ему команды. Мы сохраняем обработку типов результатов на границах системы, используя этот подход, поскольку они определены только в файле index.php
.
Этот подход предполагает наличие одной конкретной стратегии обработки для каждого типа результата команды, хотя метод processResult
ожидает экземпляр CommandResult
вместо определенного типа результата (например, CommandResultA
).
Как следствие, я явно проверяю, что тип передаваемого результата соответствует тому, который я ожидаю получить. Я использую два подхода: ProcessingStrategyA
приводит результат к CommandResultA
, тогда как ProcessingStrategyB
явно проверяет тип данного результата и полагается на PHPDoc для автозаполнения и подсказки типа.
Обратите внимание, что также может быть предоставлена стратегия обработки, которая обрабатывает оба типа результатов. Таким образом, это более гибкий подход, чем другие. Также обратите внимание, что если мы передадим неожиданный тип результата команды в контекст выполнения, это вызовет ошибку. Более безопасная альтернатива будет включать некоторую обработку ошибок и поведенческие механизмы по умолчанию.
Вариантом этого подхода с расслабленным типом может быть тот, в котором класс ExecutionContext
использует processingStrategy
без явного определения его ожидаемого типа, что вы можете увидеть ниже:
Эта версия позволяет ExecutionContext
использовать все, что имеет метод processResult
в качестве допустимой стратегии обработки. Ослабив требования к типу, мы можем удалить наследование ProcessingStrategy
, тем самым явно заявив, что ProcessingStrategyA
instance сможет обрабатывать только CommandResultA
instances вместо CommandResult
ones. Основным преимуществом здесь является то, что мы избавляемся от instanceof
проверок и приведения типов, а ограничение того, как стратегия обработки обрабатывает результат, делается явным.
Добавление нового типа результата команды с этим макетом повлечет за собой:
- Добавление новой стратегии обработки, которая может работать с новым типом результата, хотя существующую можно использовать повторно.
Я надеюсь, что эта статья была полезной. Спасибо за чтение, и следите за обновлениями!