Comments 38
Картинок маловато… Вот например на тему композиция+делегирование: коротко, жизненно и с картинками:
http://citforum.ru/programming/oop_rsis/glava2_1_10.shtml
(Эмм, полиморфизм через as
? Больно же будет, особенно при модификациях.)
На мой взгляд, методы-расширения не стоит считать полноправной ОО-функциональностью в C#, это скорее та его часть, которая идет в функциональную часть. Если у вас есть некий интерфейс, экспонирующий некий (простите) метод, то я хочу иметь возможность этот метод определить в своей реализации этого интерфейса. Решение, когда этот метод внезапно уезжает в расширение, приводит к тому, что я не могу этого сделать.
Просто представьте себе, что я добавил новый наследник от IAuthCredentials
. Что нужно сделать, чтобы для него продолжало работать IsValid
?
В процессе аутентификации взаимодействуют аккаунты и креденшалы: IAccount и ICredentials.
IAccount — Сущности, которым разрешено проходить процесс аутентификации.
ICredentials — это набор параметров, секретов и тд., которые предъявляет вызывающая сторона.
ICredentialsValidator — валидирует конкретный тип креденшалов.
Так же некорректно использовать понятие провайдер аутентификации — вы в своей задаче решаете проблему валидации креденшалов, что является частью процесса, а не им самим.
Весь ваш код сводится к набору:
User: IAccount
UsernamePasswordCredentials: ICredentials
TokenCredentials: ICredentials
UsernamePasswordCredentialsValidator: ICredentialsValidator
TokenCredentialsValidator: ICredentialsValidator
Ну и простенький ресолв валидаторов. Заодно валидаторы сразу могут получать ссылки на нужный сторадж и выставлять провалидированный аккаунт в ICredentials.
Расширять можно как угодно.
Затем, на шаге 2.2 добавили ещё 1 интерфейс и переписали класс расширений с использованием as. Я согласен с lair, что это немного странное решение. А что если появятся ещё интерфейсы? Придётся допиливать этот класс расширений?
Затем на шаге 3 в проекте А добавили ещё 3 интерфейса, 2 реализации, 1 «инфраструктурный» интерфейс, 1 его реализацию и 1 фабрику.
Потом идет проект B, в котором добавляются уже 4 интерфейса, а не 3 как в проекте А. Соответственно 3 реализации, а не 2 как в проекте А. Я не понял чем вызвана эта асимметрия. Ну, и ещё «инфраструктура»: 1 интерфейс, 1 реализация, 1 фабрика.
Итого: 15 интерфейсов, 3 хелпера, 1 класс расширений, 7 классов реализаций, 2 фабрики. Даже нарисовав схему, я не очень понимаю что именно там происходит.
Может быть это решается более простыми способами? Внедрением зависимостей, чтобы не хардкодить варианты через as? Или, например, что вы думаете о примесях (mixin)? Есть интересная статья, правда на примере JavaScript, где примеси рассматриваются как более общий случай наследования, при котором не известен заранее родительский класс. Мне это кажется безумно интересным и по-моему это решает проблемы, описанные в вашей статье. Я писал очень немного про такой подход в этой статье, там есть один пример использования: категория множеств наследуется от абстрактной категории и к ней примешивается поведение полной и колполной категорий.
class BaseClass { }
class Mixin1<T> : T where T : new() { }
class Mixin2<T> : T where T : new() { }
class DerivedClass : Mixin2<Mixin1<BaseClass>> { }
Если какие-то примеси часто используются вместе, то их можно объединять в смеси:
class CompoundMixin<T> : Mixin2<Mixin1<T>> where T : new() { }
Т.е. примесь — это более общий случай наследования, при котором заранее не известен суперкласс.
1) Это элегантный способ встроить в класс методы из нескольких классов. Проще чем шаблон делегирования.
2) Примеси не зависят от конкретного родительского класса. Не нужно пытаться заранее выстроить какую-то идеальную иерархию классов на все случаи в жизни. Эту иерархию можно подстраивать под каждый случай отдельно.
Лично мне в JavaScript такой подход помог решить некоторые проблемы, тут пример.
Например, traits не позволят вам единообразно обращаться ко всем базовым классам, пережившим встраивание. Возможно, js гораздо толерантнее к подобным запросам, но C++ без плясок с шаблонами не позволит единообразно обращаться к элементам, а любые попытки хранить их единообразно закончатся как и должны: болью.
https://ideone.com/MZOUOk
А в C++ как-раз можно. Переписал ваш пример. Всё работает. Единственное, на мой взгляд, неправильно, что примесь обращается к каким-то свойствам расширяемой структуры. По идее, в примеси должна быть реализована какая-то самодостаточная функциональность, независящая от других классов. Но если это нужно, то можно сделать как в моём примере.
Смысл наследования именно в доступе к защищённым полям и перекрытии базовой функциональности. На плюсах это требует специальной подготовки самих базовых классов, что вы сделали. При этом разрушив базовую бизнес-логику, чего делать не должны были.
https://ideone.com/xmDK4b
То есть, опять же, это просто шаблоны, собирающие классы вместо вас. Кто-то говорит, что они безопаснее, чем простое наследование. Лично я смогу прострелить себе ногу даже из своего пальца, хватит желания и достаточного объёма обезболивающих.
Потом идет проект B, в котором добавляются уже 4 интерфейса, а не 3 как в проекте А. Соответственно 3 реализации, а не 2 как в проекте А. Я не понял чем вызвана эта асимметрия.
В проекте «A» две credential-сущности, а в проекте «B» — три.
Одно не является подмножеством другого.
Эти множества сущностей имеют только «пересечение» (в части условно стандартных Credential с id пользователя и токеном сессии).
Асимметрия проектов «A» и «B» как раз призвана продемонстрировать тезис статьи, когда в решении по разным сборкам разбросан набор похожих сущностей, решающих один класс задач.
trait THasEmail
implements IHasEmail
{
protected $email;
public function getEmail()
{
return $this->email;
}
}
class User {
use THasEmail;
}
assert(User instanceof IHasEmail);
https://wiki.php.net/rfc/traits-with-interfaces
P.S. Писал комментарий с калькулятора, возможны неточности.
Зачем вообще так сложно?
Для задачи, сформулированной во вступлении, достаточно определить один интерфейс с единственным методом IsValid.
Это расходится с предыдущим текстом, где речь шла о двух методах.
С двумя методами все опять-таки проще — общая функциональность двух классов выносится в третий путем композиции (объекты двух классов содержат в себе ссылки на объекты третьего вместо наследования).
Изначальная аргументация столь же хаотична и противоречива — с точки зрения уменьшения дублирования кода композиция лучше наследования реализаций. Накладные расходы на вызов виртуальных методов связаны не с наследованием, а с полиморфизмом.
Предлагаемая реализация страдает перепроектированием — для иллюстрации достаточно трех классов и одного интерфейса.
Каким образом лучше реализовать единообразие работы с этими классами, не производя переработку и рефакторинг самих классов?
В данном случае представляется целесообразным объявить набор интерфейсов (свойства и методы интерфейсов будут повторять уже имеющиеся свойства и методы у классов), и объявить, что сущности реализуют эти интерфейсы.
Возможно, некоторые элементы реализации интерфейсов у классов потребуется объявить явно (explicit) путем обращения к соответствующим элементам классов, если элементы классов названы «вразнобой», а рефакторинг классов нецелесообразен.
Затем реализуем класс с методами расширения для новых интерфейсов, и во всех местах обращения к классам заменяем copy-paste некоторой работы с этими классами на вызов одного метода расширения.
Таким образом, предлагаемый подход применим для работы с legacy-кодом, когда необходимо быстро «починить» и реализовать единообразие работы с некоторым набором классов со схожими (в определенном разрезе) декларациями и функциональностью.
(Вопрос, каким образом в проекте может оказаться такой набор классов, вынесем за скобки.)
При разработке API проекта и иерархии классов с нуля следует применять другие подходы.
Каким образом при этом можно реализовать код без copy-paste, если два или более классов имеют один и тот же по смыслу метод, но немного с разной логикой — тема отдельного разговора.
Возможно, это тема новой статьи.
Каким образом лучше реализовать единообразие работы с этими классами, не производя переработку и рефакторинг самих классов?
В данном случае представляется целесообразным объявить набор интерфейсов (свойства и методы интерфейсов будут повторять уже имеющиеся свойства и методы у классов), и объявить, что сущности реализуют эти интерфейсы.
Эти два утверждения друг другу противоречат. Вы либо можете заставить классы реализовывать эти интерфейсы — и тогда это рефакторинг, — либо не можете.
Если не можете, то шаблон "адаптер" ваш друг.
Изначально ООП было задумано для моделирования биологических систем и игр
Это вы почему так считаете?
По опыту скажу, что бизнес-процессы более простые, они не требуют многого того, что заложено в ООП.
Например, чего?
(вообще, утверждение, что "бизнес-процессы более простые" — оно, конечно, громкое, да)
Это вы почему так считаете?
Когда изобрели первые языки ООП, типа small talk, я уже зарабатывал программированием. Одни из первых лекций по Оберону нам читал сам Вирт в НГУ
выдержка из https://habrahabr.ru/company/hexlet/blog/303754/
Я считал объекты чем-то вроде биологических клеток, и/или отдельных компьютеров в сети, которые могут общаться только через сообщения.
Одни из первых лекций по Оберону нам читал сам Вирт в НГУ
Кул, точную аттрибутированную цитату привести можете?
Я считал объекты чем-то вроде биологических клеток
Когда что-то построено "по подобию" чего-то (хотя, на самом деле, даже это не так), еще не факт, что первое построено для моделирования второго. Тем более, что в процитированной вами фразе на равным правах указаны "компьютеры в сети", так что можно было бы сказать, что "изначально ООП было задумано для моделирования компьютеров в сети", но согласитесь, что это странно?
точную аттрибутированную цитату привести можете?
Внезапная просьба. Вам зачем?
Чтобы понять, на основании чего — кроме своего личного мнения — вы утверждаете, что "изначально ООП было задумано для моделирования биологических систем и игр".
А то тут в соседнем посте рассказывают, что Симула была разработана для дискретно-событийной симуляции (вики раскрывает: "для физического моделирования, такого как изучение и улучшение движения судов и их содержимого через грузовые порты". Не сходится.
Но посудите сами ООП — объектно-ориентировано, а бизнес просессы процессно-ориентированы. Какая-то разница уже намечается.
Тот факт, что есть разница, не означает, что "что бизнес-процессы более простые, они не требуют многого того, что заложено в ООП".
Более того, хотя процессы и процессо-ориентированы, участвующие в них сущности (и акторы, кстати) часто прекрасно поддаются объектно-ориентированному подходу (собственно, ничего не мешает представить бизнес-процесс в виде обмена сообщениями).
Наконец, есть и третий взгляд на все это: ООП может быть слоем разработки ниже модельного уровня.
Композиция и интерфейсы