сценарий
Веб-приложение определяет интерфейс пользователя IUserBackend
с методами
- GetUser (UID)
- CreateUser (UID)
- deleteUser (UID)
- setPassword (uid, пароль)
- ...
Различные пользовательские бэкэнды (например, LDAP, SQL, ...) реализуют этот интерфейс, но не каждый бэкэнд может делать все. Например, конкретный сервер LDAP не позволяет этому веб-приложению удалять пользователей. Таким образом, LdapUserBackend
класс, который реализует IUserBackend
, не будет реализован deleteUser(uid)
.
Конкретный класс должен сообщить веб-приложению, что веб-приложению разрешено делать с пользователями бэкэнда.
Известное решение
Я видел решение, в котором IUserInterface
есть implementedActions
метод, который возвращает целое число, которое является результатом побитового ИЛИ действий побитового И с запрошенными действиями:
function implementedActions(requestedActions) {
return (bool)(
ACTION_GET_USER
| ACTION_CREATE_USER
| ACTION_DELTE_USER
| ACTION_SET_PASSWORD
) & requestedActions)
}
где
- ACTION_GET_USER = 1
- ACTION_CREATE_USER = 2
- ACTION_DELETE_USER = 4
- ACTION_SET_PASSWORD = 8
- .... = 16
- .... = 32
и т.п.
Таким образом, веб-приложение устанавливает битовую маску с тем, что ему нужно, и implementedActions()
отвечает логическим значением, поддерживает ли оно их.
мнение
Эти битовые операции для меня выглядят как реликты эпохи Си, которые не всегда легко понять с точки зрения чистого кода.
Вопрос
Каков современный (лучший?) Шаблон для класса для передачи подмножества методов интерфейса, которые он реализует? Или «метод битовой операции сверху» все еще является лучшей практикой?
( В случае, если это имеет значение: PHP, хотя я ищу общее решение для ОО-языков )
источник
IUserBackend
не должен содержатьdeleteUser
метод вообще. Это должно быть частьюIUserDeleteBackend
(или как вы хотите это назвать). Код, который должен удалять пользователей, будет иметь аргументыIUserDeleteBackend
, код, который не нуждается в этой функциональности, будет использоватьIUserBackend
и не будет иметь проблем с нереализованными методами.Ответы:
Вообще говоря, есть два подхода, которые вы можете использовать здесь: test & throw или композиция через полиморфизм.
Тест и бросок
Это подход, который вы уже описали. С помощью некоторых средств вы указываете пользователю класса, реализованы ли некоторые другие методы или нет. Это можно сделать с помощью одного метода и побитового перечисления (как вы описываете) или с помощью ряда
supportsDelete()
методов etc.Затем, если
supportsDelete()
возвращаетсяfalse
, вызовdeleteUser()
может привести к тому,NotImplementedExeption
что его бросят или метод просто ничего не сделает.Это популярное решение среди некоторых, так как оно простое. Однако многие, включая меня, утверждают, что это является нарушением принципа подстановки Лискова (L в SOLID) и, следовательно, не является хорошим решением.
Композиция через полиморфизм
Подход здесь заключается в том, чтобы рассматривать
IUserBackend
слишком тупой инструмент. Если классы не всегда могут реализовать все методы в этом интерфейсе, разбейте интерфейс на более сфокусированные части. Таким образом, вы можете иметь:IGeneralUser IDeletableUser IRenamableUser ...
Другими словами, все методы, которые могут быть реализованы всеми вашими бэкэндами, включаются,IGeneralUser
и вы создаете отдельный интерфейс для каждого из действий, которые могут выполнять только некоторые.Таким образом,
LdapUserBackend
не реализуется,IDeletableUser
и вы тестируете это с помощью теста, такого как (используя синтаксис C #):(Я не уверен в механизме в PHP для определения, реализует ли экземпляр интерфейс и как вы затем приводите его к этому интерфейсу, но я уверен, что в этом языке есть эквивалент)
Преимущество этого метода заключается в том, что он хорошо использует полиморфизм, позволяющий вашему коду соответствовать принципам SOLID, и, на мой взгляд, более элегантен.
Недостатком является то, что он может стать слишком громоздким. Если, например, вам придется реализовать десятки интерфейсов, потому что каждый конкретный бэкэнд имеет немного разные возможности, то это не очень хорошее решение. Поэтому я бы просто посоветовал вам использовать свое суждение о том, является ли этот подход практичным для вас в этом случае, и использовать его, если он есть.
источник
if (backend instanceof IDelatableUser) {...}
Divide(float,float)
методе. Входное значение является переменной, и исключение охватывает небольшое подмножество возможных исполнений. Но если вы выбрасываете в зависимости от типа реализации, то его неспособность выполнить - это конкретный факт. Исключение охватывает все возможные входы , а не только их подмножество. Это все равно, что ставить знак «мокрый пол» на каждом мокром полу в мире, где каждый этаж всегда мокрый.NotImplementedException
. Это исключение предназначено для временных отключений, то есть кода, который еще не разработан, но будет разрабатываться. Это не то же самое, что окончательное решение о том, что данный класс никогда не будет ничего делать с данным методом, даже после завершения разработки.Текущая ситуация
Текущая настройка нарушает принцип разделения интерфейса (I в SOLID).
Ссылка
Другими словами, если это ваш интерфейс:
Затем каждый класс, который реализует этот интерфейс, должен использовать каждый из перечисленных методов интерфейса. Не исключение
Представьте, если есть обобщенный метод:
Если вы действительно должны были сделать так, чтобы только некоторые из реализующих классов действительно могли удалить пользователя, то этот метод иногда взорвется вам (или вообще ничего не сделает). Это не хороший дизайн.
Ваше предлагаемое решение
То, что вы по сути хотите сделать, это:
Я игнорирую, как именно мы определяем, может ли данный класс удалить пользователя. Будь то логическое значение, битовый флаг ... не имеет значения. Все сводится к двоичному ответу: может ли он удалить пользователя, да или нет?
Это решит проблему, верно? Ну, технически это так. Но теперь вы нарушаете принцип подстановки Лискова (L в SOLID).
Отказавшись от довольно сложного объяснения в Википедии, я нашел достойный пример на StackOverflow . Обратите внимание на «плохой» пример:
Я полагаю, вы видите здесь сходство. Это метод, который должен обрабатывать абстрагированный объект (
IDuck
,IUserBackend
), но из-за скомпрометированной конструкции класса он вынужден сначала обрабатывать определенные реализации (ElectricDuck
убедитесь, что это не тотIUserBackend
класс, который не может удалять пользователей).Это противоречит цели разработки абстрактного подхода.
Примечание: пример здесь легче исправить, чем ваш случай. Для примера, достаточно иметь
ElectricDuck
сам поворот на внутри вSwim()
методе. Обе утки еще умеют плавать, поэтому функциональный результат одинаков.Вы можете сделать что-то подобное. Не . Вы не можете просто притворяться, что удалили пользователя, но в действительности у вас есть пустое тело метода. Хотя это работает с технической точки зрения, невозможно определить, действительно ли ваш реализующий класс что-то сделает, когда его попросят что-то сделать. Это питательная среда для неуправляемого кода.
Мое предлагаемое решение
Но вы сказали, что реализующий класс может (и правильно) обрабатывать только некоторые из этих методов.
Для примера скажем, что для каждой возможной комбинации этих методов существует класс, который будет реализовывать его. Он охватывает все наши базы.
Решением здесь является разделение интерфейса .
Обратите внимание, что вы могли видеть это в начале моего ответа. Название Принципа Разделения Интерфейса уже показывает, что этот принцип разработан, чтобы заставить вас разделить интерфейсы в достаточной степени.
Это позволяет вам смешивать и сочетать интерфейсы по своему усмотрению:
Каждый класс может решить, что он хочет делать, не нарушая контракт своего интерфейса.
Это также означает, что нам не нужно проверять, может ли определенный класс удалить пользователя. Каждый класс, реализующий
IDeleteUserService
интерфейс, сможет удалить пользователя = Нет нарушения принципа подстановки Лискова .Если кто-то попытается передать объект, который не реализует
IDeleteUserService
, программа откажется от компиляции. Вот почему нам нравится иметь безопасность типов.сноска
Я довел пример до крайности, разделив интерфейс на наименьшие возможные куски. Однако, если ваша ситуация отличается, вы можете избежать больших кусков.
Например, если каждая служба, которая может создать пользователя, всегда способна удалить пользователя (и наоборот), вы можете оставить эти методы как часть единого интерфейса:
Нет технической выгоды делать это вместо разделения на более мелкие куски; но это немного облегчит разработку, поскольку требует меньшего количества компоновки.
источник
TryDeleteUser
чтобы отразить это); или у вас есть метод преднамеренно выдать исключение, если это возможно, но проблемная ситуация. Использование методаCanDoThing()
иDoThing()
метода работает, но это потребует от ваших внешних абонентов использовать два вызова (и будут наказаны за невыполнение), что является менее интуитивным и не таким элегантным.Если вы хотите использовать типы более высокого уровня, вы можете выбрать тип набора на своем языке. Надеемся, что он предоставляет некоторый синтаксический сахар для выполнения пересечений множеств и определения подмножеств.
Это в основном то, что Java делает с EnumSet (за исключением синтаксического сахара, но эй, это Java)
источник
В мире .NET вы можете украшать методы и классы пользовательскими атрибутами. Это может не относиться к вашему делу.
Мне кажется, что проблема может быть связана с более высоким уровнем дизайна.
Если это функция пользовательского интерфейса, такая как страница редактирования пользователя или компонент, то как маскируются различные возможности? В этом случае тест и бросок будут довольно неэффективным подходом для этой цели. Предполагается, что перед загрузкой каждой страницы вы запускаете фиктивный вызов для каждой функции, чтобы определить, должен ли виджет или элемент быть скрыт или представлен по-другому. В качестве альтернативы, у вас есть веб-страница, которая в основном заставляет пользователя обнаруживать то, что доступно путем ручного «тестирования и выбрасывания», какой бы путь кодирования вы не выбрали, потому что пользователь не обнаруживает, что что-то недоступно, пока не появится всплывающее предупреждение.
Поэтому для пользовательского интерфейса вы можете захотеть посмотреть, как вы управляете функциями, и связать с этим выбор доступных реализаций, а не выбирать, какие реализации реализуют управление функциями. Возможно, вы захотите взглянуть на каркасы для составления функциональных зависимостей и явно определить возможности как сущности в вашей доменной модели. Это может быть даже связано с авторизацией. По сути, решение о том, доступна ли функция или нет на основе уровня авторизации, может быть расширено до принятия решения о том, реализована ли эта возможность, и тогда «функции» пользовательского интерфейса высокого уровня могут иметь явные сопоставления с наборами возможностей.
Если это веб-API, то общий выбор дизайна может быть затруднен из-за необходимости поддерживать несколько общедоступных версий API-интерфейса «Управление пользователем» или ресурса «REST пользователя», поскольку возможности со временем расширяются.
Подводя итог, можно сказать, что в мире .NET вы можете использовать различные способы Reflection / Attribute, чтобы заранее определить, какие классы реализуют что, но в любом случае кажется, что реальные проблемы будут связаны с тем, что вы делаете с этой информацией.
источник