is
Оператор C # и Javainstanceof
позволяют вам ветвиться на интерфейсе (или, что еще более неуклюже, на его базовом классе), реализованном экземпляром объекта.
Уместно ли использовать эту функцию для ветвления высокого уровня на основе возможностей, предоставляемых интерфейсом?
Или базовый класс должен предоставлять логические переменные, чтобы обеспечить интерфейс для описания возможностей, которые имеет объект?
Пример:
if (account is IResetsPassword)
((IResetsPassword)account).ResetPassword();
else
Print("Not allowed to reset password with this account type!");
против
if (account.CanResetPassword)
((IResetsPassword)account).ResetPassword();
else
Print("Not allowed to reset password with this account type!");
Есть ли у них подводные камни для использования реализации интерфейса для идентификации возможностей?
Этот пример был просто примером. Я задаюсь вопросом о более общем применении.
object-oriented
TheCatWhisperer
источник
источник
CanResetPassword
это будет верно только тогда, когда что-то реализуетсяIResetsPassword
. Теперь вы заставляете свойство определять то, что система типов должна контролировать (и, в конечном счете, будет, когда вы преформуете приведение).Ответы:
С учетом предостережения о том, что лучше избегать снижения рейтинга, если это возможно, тогда первая форма предпочтительнее по двум причинам:
is
пожары, то удручающий будет определенно работать. Во второй форме вам нужно вручную убедиться, что только объекты, которые реализуют,IResetsPassword
возвращают true для этого свойства.Это не значит, что вы не можете улучшить эти проблемы. Например, у базового класса может быть набор опубликованных интерфейсов, в которых вы можете проверить включение. Но на самом деле вы просто вручную реализуете часть существующей системы типов, которая является избыточной.
Кстати, мое естественное предпочтение - это композиция, а не наследование, но в основном это касается государства. Наследование интерфейса обычно не так плохо. Кроме того, если вы реализуете иерархию типов бедняков, вам лучше использовать ту, которая уже есть, так как компилятор поможет вам.
источник
(account as IResettable)?.ResetPassword();
илиvar resettable = account as IResettable; if(resettable != null) {resettable.ResetPassword()} else {....}
Возможности объектов вообще не должны быть идентифицированы.
Клиент, использующий объект, не должен знать ничего о том, как он работает. Клиент должен знать только то, что он может сказать объекту. То, что делает объект, как только ему сказали, не является проблемой клиента.
Так что вместо
или
рассмотреть возможность
или
потому что
account
уже знает, будет ли это работать. Зачем это спрашивать? Просто скажите ему, что вы хотите, и позвольте ему делать то, что он собирается делать.Представьте, что это сделано таким образом, что клиенту все равно, сработало ли это или нет, потому что это что-то еще. Работа клиентов состояла только в том, чтобы сделать попытку. Это сделано сейчас, и есть другие вещи, с которыми нужно иметь дело. У этого стиля много имен, но имя, которое мне нравится больше всего, это сказать, не спрашивайте .
Когда вы пишете клиенту, очень заманчиво думать, что вы должны следить за всем и поэтому притягивайте к себе все, что, по вашему мнению, вам нужно знать. Когда вы делаете это, вы выворачиваете предметы наизнанку. Цени свое невежество. Отодвиньте детали и позвольте объектам справиться с ними. Чем меньше ты знаешь, тем лучше.
источник
if (!account.AttemptPasswordReset()) /* attempt failed */;
Таким образом, клиент знает результат, но избегает некоторых проблем с логикой принятия решения в вопросе. Если попытка асинхронная, вы передадите обратный вызов.account
при его создании. В этот момент могут происходить всплывающие окна, ведение журнала, распечатки и звуковые оповещения. Работа клиента - сказать «сделай это сейчас». Это не должно делать больше, чем это. Вам не нужно делать все в одном месте.Задний план
Наследование - это мощный инструмент, предназначенный для объектно-ориентированного программирования. Тем не менее, это не решает каждую проблему элегантно: иногда другие решения лучше.
Если вы вспомните свои ранние уроки информатики (при условии, что у вас есть степень CS), вы можете вспомнить, как профессор давал вам параграф, в котором говорится, что клиент хочет, чтобы программное обеспечение делало. Ваша работа состоит в том, чтобы прочитать абзац, определить действующих лиц и действия и составить общее представление о классах и методах. Там будет несколько задниц, которые выглядят как важные, но это не так. Существует также очень реальная возможность неверного толкования требований.
Это важный навык, который ошибаются даже самые опытные из нас: правильно определять требования и переводить их на машинные языки.
Ваш вопрос
Исходя из того, что вы написали, я думаю, вы, возможно, неправильно понимаете общий случай необязательных действий, которые могут выполнять классы. Да, я знаю, что ваш код является лишь примером, и вы заинтересованы в общем случае. Однако звучит так, будто вы хотите знать, как справиться с ситуацией, когда определенные подтипы объекта могут выполнять действие, а другие подтипы - нет.
Тот
account
факт, что такой объект, как объект, имеет тип учетной записи, не означает, что он переводится в тип на языке OO. «Тип» в человеческом языке не всегда означает «класс». В контексте учетной записи «тип» может более тесно соотноситься с «набором разрешений». Вы хотите использовать учетную запись пользователя для выполнения действия, но это действие может или не может быть выполнено этой учетной записью. Вместо использования наследования я бы использовал делегат или токен безопасности.Мое решение
Рассмотрим класс учетной записи, который имеет несколько дополнительных действий, которые он может выполнять. Вместо определения «может выполнить действие X» с помощью наследования, почему бы учетной записи не вернуть объект делегата (сброс пароля, отправитель формы и т. Д.) Или токен доступа?
Преимущество последнего варианта заключается в том, что если я хочу использовать учетные данные своей учетной записи для сброса чужого пароля?
Мало того, что система стала более гибкой, я не только убрал броски типов, но и дизайн стал более выразительным . Я могу прочитать это и легко понять, что он делает и как его можно использовать.
TL; DR: это читается как проблема XY . Обычно, когда сталкиваются с двумя вариантами, которые являются неоптимальными, стоит сделать шаг назад и подумать: «Чего я действительно здесь пытаюсь достичь? Должен ли я действительно думать о том, как сделать типотипирование менее уродливым, или я должен искать способы удалить типографский ролик целиком? "
источник
account.getPasswordResetter().resetPassword();
.The benefit to the last option there is what if I want to use my account credentials to reset someone else's password?
.. легко. Вы создаете объект домена учетной записи для другой учетной записи и используете его. Статические классы проблематичны для модульного тестирования (этот метод по определению будет нуждаться в доступе к некоторому хранилищу, поэтому теперь вы не можете легко выполнить модульное тестирование любого кода, который больше касается этого класса), и его лучше избегать. Возможность возврата какого-либо другого интерфейса часто является хорошей идеей, но на самом деле она не решает основную проблему (вы просто перенесли проблему на другой интерфейс).Билл Веннер говорит, что ваш подход абсолютно хорош ; перейдите к разделу «Когда использовать instanceof». Вы понижаете до определенного типа / интерфейса, который реализует только это конкретное поведение, поэтому вы абсолютно правы в этом вопросе.
Однако, если вы хотите использовать другой подход, есть много способов побрить яка.
Есть подход полиморфизма; Вы можете утверждать, что все учетные записи имеют пароль, поэтому все учетные записи должны, по крайней мере, иметь возможность попытаться сбросить пароль. Это означает, что в базовом классе учетных записей у нас должен быть
resetPassword()
метод, который реализуют все учетные записи, но по-своему. Вопрос в том, как должен вести себя этот метод, когда возможности отсутствуют.Он может вернуть пустоту и выполнить молча, независимо от того, сбросил ли он пароль или нет, возможно, взяв на себя ответственность за внутреннюю распечатку сообщения, если он не сбросил пароль. Не самая лучшая идея
Он может вернуть логическое значение, указывающее, был ли сброс успешным. При включении этого логического значения мы можем связать сбой сброса пароля.
Он может вернуть строку, указывающую результат попытки сброса пароля. Строка может дать более подробную информацию о том, почему сброс не удалось, и может быть выведен.
Он может вернуть объект ResetResult, который передает больше деталей и объединяет все предыдущие возвращаемые элементы.
Он может вернуть пустоту и вместо этого выдать исключение, если вы попытаетесь сбросить учетную запись, у которой нет такой возможности (не делайте этого, так как использование обработки исключений для нормального управления потоком является плохой практикой по ряду причин).
Наличие
canResetPassword()
метода сопоставления может показаться не самым худшим в мире, поскольку по идее это статическая возможность, встроенная в класс при его написании. Это ключ к пониманию того, почему методный подход является плохой идеей, так как он предполагает, что возможность является динамической, и этоcanResetPassword()
может измениться, что также создает дополнительную возможность, что она может измениться между запросом разрешения и выполнением вызова. Как уже упоминалось, говорите, а не спрашивайте разрешения.Композиция по наследованию может быть вариантом: у вас может быть конечное
passwordResetter
поле (или эквивалентный метод получения) и эквивалентный класс (ы), для которого вы можете проверить значение null, прежде чем вызывать его. Хотя это немного похоже на запрос разрешения, окончательность может избежать любой предполагаемой динамической природы.Вы можете подумать о том, чтобы перенести функциональность в свой собственный класс, который мог бы использовать учетную запись в качестве параметра и воздействовать на нее (например,
resetter.reset(account)
), хотя это также часто является плохой практикой (распространенная аналогия - продавец, идущий в ваш кошелек за наличными).В языках с такими возможностями вы можете использовать миксины или черты, однако вы попадаете в положение, в котором вы начали, где вы можете проверять наличие этих возможностей.
источник
Для этого конкретного примера « могу сбросить пароль » я бы рекомендовал использовать композицию поверх наследования (в данном случае наследование интерфейса / контракта). Потому что, делая это:
Вы сразу указываете (во время компиляции), что ваш класс « может сбросить пароль ». Но если в вашем сценарии наличие возможности является условным и зависит от других вещей, то вы больше не можете указывать что-либо во время компиляции. Затем я предлагаю сделать это:
Теперь во время выполнения вы можете проверить,
myFoo.passwordResetter != null
перед выполнением этой операции. Если вы хотите отделить вещи еще больше (и планируете добавить гораздо больше возможностей), вы можете:ОБНОВИТЬ
После прочтения некоторых других ответов и комментариев от ОП я понял, что вопрос не в композиционном решении. Поэтому я думаю, что вопрос заключается в том, как лучше определить возможности объектов в целом в сценарии, подобном приведенному ниже:
Теперь я попытаюсь проанализировать все упомянутые варианты:
OP второй пример:
Допустим, этот код получает некоторый базовый класс учетной записи (например:
BaseAccount
в моем примере); это плохо, поскольку он вставляет логические значения в базовый класс, загрязняя его кодом, который вообще не имеет смысла находиться там.OP первый пример:
Чтобы ответить на вопрос, это более уместно, чем предыдущий вариант, но в зависимости от реализации он нарушит L принцип solid, и, вероятно, подобные проверки будут распространяться по коду и усложняют дальнейшее обслуживание.
Андер из CandiedOrange:
Если этот
ResetPassword
метод вставлен вBaseAccount
класс, он также загрязняет базовый класс неподходящим кодом, как во втором примере OPСнеговик ответ:
Это хорошее решение, но оно считает, что возможности являются динамическими (и могут меняться со временем). Тем не менее, после прочтения нескольких комментариев от OP, я предполагаю, что речь здесь идет о полиморфизме и статически определенных классах (хотя пример учетных записей интуитивно указывает на динамический сценарий).
AccountManager
Например : в этом примере проверкой разрешения будут запросы к БД; в вопросе ОП проверки - это попытки бросить объекты.Еще одно предложение от меня:
Используйте шаблонный метод для высокоуровневого ветвления. Упомянутая иерархия классов сохраняется как есть; мы только создаем более подходящие обработчики для объектов, чтобы избежать приведений и неуместных свойств / методов, загрязняющих базовый класс.
Вы можете привязать операцию к соответствующему классу учетной записи, используя словарь / хеш-таблицу, или выполнять операции во время выполнения, используя методы расширения, использовать
dynamic
ключевое слово, или, как последний вариант, использовать только одно приведение , чтобы передать объект учетной записи в операцию (в в этом случае количество приведений только в начале операции).источник
Ни один из предложенных вами вариантов не является хорошим ОО. Если вы пишете операторы if вокруг типа объекта, вы, скорее всего, делаете OO неправильно (есть исключения, но это не одно). Вот простой OO-ответ на ваш вопрос (может быть, он недопустим в C #):
Я изменил этот пример, чтобы соответствовать тому, что делал оригинальный код. Основываясь на некоторых комментариях, кажется, что люди зацикливаются на том, является ли это «правильным» конкретным кодом здесь. Это не точка этого примера. Полиморфная перегрузка в целом состоит в условном выполнении различных реализаций логики в зависимости от типа объекта. То, что вы делаете в обоих примерах, - это глушение рук того, что ваш язык дает вам как особенность. В двух словах, вы можете избавиться от подтипов и поставить возможность сброса как логическое свойство типа Account (игнорируя другие функции подтипов.)
Без более широкого представления о дизайне невозможно определить, является ли это хорошим решением для вашей конкретной системы. Это просто, и если это работает для того, что вы делаете, вам, вероятно, больше никогда не придется много думать об этом, пока кто-то не проверит CanResetPassword () перед вызовом ResetPassword (). Вы также можете вернуть логическое значение или выполнить молчание (не рекомендуется). Это действительно зависит от специфики дизайна.
источник
NotResetable
. «Возможно, учетная запись обладает большей функциональностью, чем возможность повторной загрузки», - я ожидаю, но это не кажется важным для рассматриваемого вопроса.Ответ
Мой слегка опрометчивый ответ на ваш вопрос:
Приложение:
бессвязный
Я вижу, что вы не добавили
c#
тег, но спрашиваете в общемobject-oriented
контексте.То, что вы описываете, - это техническая специфика статических / строго типизированных языков, а не специфика "OO" в целом. Ваша проблема связана, помимо прочего, с тем фактом, что вы не можете вызывать методы в этих языках, не имея достаточно узкого типа во время компиляции (либо путем определения переменной, определения параметра метода / возвращаемого значения, либо явного приведения). Я делал оба ваших варианта в прошлом, на таких языках, и оба они кажутся мне безобразными в наши дни.
В динамически типизированных ОО-языках эта проблема не возникает из-за концепции, называемой «типизацией утки». Короче говоря, это означает, что вы в принципе нигде не объявляете тип объекта (кроме случаев его создания). Вы отправляете сообщения объектам, и объекты обрабатывают эти сообщения. Вам не важно, есть ли у объекта метод с таким именем. В некоторых языках даже есть общий, универсальный
method_missing
метод, который делает его еще более гибким.Итак, на таком языке (например, Ruby) ваш код становится:
Период.
Он
account
либо сбросит пароль, либо выдаст исключение «отказано», либо выдаст исключение «не знаю, как обрабатывать« сброс_пароля »».Если по какой-либо причине вам необходимо выполнить явное ветвление, вы все равно сделаете это с возможностью, а не с классом:
(Где
can?
просто метод, который проверяет, может ли объект выполнить какой-либо метод, не вызывая его.)Обратите внимание, что хотя мои личные предпочтения в настоящее время относятся к динамически типизированным языкам, это всего лишь личные предпочтения. В статически типизированных языках тоже есть что-то интересное, поэтому, пожалуйста, не воспринимайте это бессмысленно как удар по статически типизированным языкам ...
источник
Кажется, что ваши варианты происходят от различных мотивационных сил. Первый, похоже, взломан из-за плохого моделирования объектов. Это продемонстрировало, что ваши абстракции неверны, так как они нарушают полиморфизм. Скорее всего, это нарушает принцип открытого закрытого типа , что приводит к более жесткому сцеплению.
Ваш второй вариант может быть хорошим с точки зрения ООП, но приведение типов смущает меня. Если ваша учетная запись позволяет вам сбросить свой пароль, зачем вам в этом случае приведение типов? Таким образом, предполагая, что ваше
CanResetPassword
предложение представляет собой фундаментальное бизнес-требование, являющееся частью абстракции учетной записи, ваш второй вариант в порядке.источник