Всем привет!
На самом деле хочется ещё раз поднять тему наследования реализаций.
После погружения в Dependency Injection практику программирования стало ясно, что наследование создает очень сильную связь между наследником и базовым классом.
Соответственно встает вопрос, а нужно ли наследование (реализации) вообще.
И даже если уж так нужно именно наследование реализации (допущу что в редких случаях оно может понадобится) то какие обходные пути есть.
Первое что приходит в голову — использовать включение. Конечно оно повлечет некоторый "синтаксический оверхэд" , но всё же этот вариант присуствует.
Особенно он будет хорошо смотреться в некоторых языках в которых код можно генерировать налету (не будем называть такие языки вслух )
Какие есть ещё варианты реализации наследования реализации без использования собственно самого наследования.
И в целом какие будут мысли:
PS:
Интересно не задумывался ли кто о написании языка который был бы изначально дружелюбным к Dependency Injection-у.
Т.е. в котором не было бы операции инстанциирования конкретного класса а только получение инстанса каким либо образом по его интерфейсу.
PPS:
Проще обсуждение вести в контексте языка C# & .NET в целом.
Здравствуйте, Tom, Вы писали:
Tom>Интересно не задумывался ли кто о написании языка который был бы изначально дружелюбным к Dependency Injection-у. Tom>Т.е. в котором не было бы операции инстанциирования конкретного класса а только получение инстанса каким либо образом по его интерфейсу.
Вместо одного класса писать пару интерфейс+класс? Увольте.
А язык дружелюбный есть, ага. Прямо в книге Одерски подход описан, и сказано, что это типа замена DI. Впрочем, это не совсем DI, это не-очень-доброе старое ручное связывание, но по сравнению, скажем, с "классическим" (до аннотаций) Spring-ом, где все эти же связи прописывались в XML (громоздко до тошноты и легко напортачить), здесь у нас static type checking. И суммарно система стала проще (IoC-контейнер не нужен, AOP не нужен, долгое сканировование классов в поисках аннотаций при запуске не нужно).
// Классы с зависимостями.
class A { def s = "world" }
class B(a: A) { def s = "hello " + a.s }
class C(a: A, b: B) { def main() { println(b.s + ", goodbye " + a.s) }}
object MyApp extends App {
// Выполняем связывание (если простыня длинная, или несколько длинных и похожих
// в разных точках входа, её можно произвольно декомпозировать средствами языка).
val a = new A
val c = new C(a, new B(a))
// Запускаем приложение.
c.main()
}
Здравствуйте, Tom, Вы писали:
Tom>Соответственно встает вопрос, а нужно ли наследование (реализации) вообще. Tom>И даже если уж так нужно именно наследование реализации (допущу что в редких случаях оно может понадобится) то какие обходные пути есть.
А по теме имею сказать следующее: люблю я всякие data-driven алгоритмы клепать. Но иногда случается такое, что по мере усложнения задачи растёт сложность управляющих метаданных, и в итоге единственный разумный выход — превратить иерархию пассивных структур в иерархию стратегий, т.е. вынести часть логики из алгоритма в метаданные. И вот тут просто с целью убирания задвоений кода часть этого кода частенько перемещается в protected final методы базового класса. Оно короче и понятнее выходит, чем если поднимать на щит "нет наследованию!" и плодить всякие хелперы сбоку-припёку, да ещё и с более широкой областью видимости.
Ну и "каркасом" тоже иногда балуюсь, хотя гораздо реже.
Здравствуйте, dimgel, Вы писали:
D>Оно короче и понятнее выходит, чем если поднимать на щит "нет наследованию!" и плодить всякие хелперы сбоку-припёку, да ещё и с более широкой областью видимости.
Особенно если стратегии stateful (в этом случае время их жизни ограничено потрохами алгоритма) — передавать состояние хелперам запаришься.
Здравствуйте, Tom, Вы писали:
Tom>Соответственно встает вопрос, а нужно ли наследование (реализации) вообще.
Не нужно.
Tom>Первое что приходит в голову — использовать включение. Конечно оно повлечет некоторый "синтаксический оверхэд" , но всё же этот вариант присуствует.
Ну да, агрегация. Да и оверхеда большого не должно быть, ИМХО.
Здравствуйте, Tom, Вы писали:
Tom>Соответственно встает вопрос, а нужно ли наследование (реализации) вообще.
Нужна возможность подключения реализаций существующих контрактов. Соответственно, ответ очень простой — если есть лучшая альтернатива, то наследование реализации не нужно.
Tom>Первое что приходит в голову — использовать включение. Конечно оно повлечет некоторый "синтаксический оверхэд" , но всё же этот вариант присуствует.
У агрегации с автоматической делегацией реализации контракта есть свои тараканы.
... << RSDN@Home 1.2.0 alpha 5 rev. 23 on Windows 8 6.2.9200.0>>
Tom>>Соответственно встает вопрос, а нужно ли наследование (реализации) вообще. AVK>Нужна возможность подключения реализаций существующих контрактов. Соответственно, ответ очень простой — если есть лучшая альтернатива, то наследование реализации не нужно.
Мне нравится слово подключение.
Tom>>Первое что приходит в голову — использовать включение. Конечно оно повлечет некоторый "синтаксический оверхэд" , но всё же этот вариант присуствует.
AVK>У агрегации с автоматической делегацией реализации контракта есть свои тараканы.
Какие?
Здравствуйте, Tom, Вы писали: Tom>Соответственно встает вопрос, а нужно ли наследование (реализации) вообще.
Не то чтобы нужно, а скорей часто удобно. Из вариантов реализации наследования, лично мне нравятся Scala-traits.
Tom>PS: Tom>Интересно не задумывался ли кто о написании языка который был бы изначально дружелюбным к Dependency Injection-у. Tom>Т.е. в котором не было бы операции инстанциирования конкретного класса а только получение инстанса каким либо образом по его интерфейсу.
Я задумываюсь
Между тем,что я думаю,тем,что я хочу сказать,тем,что я,как мне кажется,говорю,и тем,что вы хотите услышать,тем,что как вам кажется,вы слышите,тем,что вы понимаете,стоит десять вариантов возникновения непонимания.Но всё-таки давайте попробуем...(Э.Уэллс)
Здравствуйте, dimgel, Вы писали:
D>Здравствуйте, dimgel, Вы писали:
D>>Оно короче и понятнее выходит, чем если поднимать на щит "нет наследованию!" и плодить всякие хелперы сбоку-припёку, да ещё и с более широкой областью видимости.
D>Особенно если стратегии stateful (в этом случае время их жизни ограничено потрохами алгоритма) — передавать состояние хелперам запаришься.
Здравствуйте, michael_isu, Вы писали:
_>Откройте для себя еxtensions methods.
1. Я не на шарпе.
2. Они не делают логику проще, а наоборот запутывают связность.
3. Незачем плодить странные костыли там, где работает нормальное ООП-наследование.
Здравствуйте, dimgel, Вы писали:
D>Здравствуйте, michael_isu, Вы писали:
_>>Откройте для себя еxtensions methods.
D>1. Я не на шарпе.
Какая досада.
D>2. Они не делают логику проще, а наоборот запутывают связность.
Автора не читали, но обсуждаем? Понятно.
D>3. Незачем плодить странные костыли там, где работает нормальное ООП-наследование.
Работает до поры до времени.
Здравствуйте, michael_isu, Вы писали:
_>Здравствуйте, Tom, Вы писали:
Tom>>Соответственно встает вопрос, а нужно ли наследование (реализации) вообще.
_>Слишком абстрактный вопрос задаете. Лучше пример Иначе опять 30 страниц флуда ни о чем будет.
Да пример очень простой:
class DerivedClass : BaseClass
{
// Bl bla bla
}
Что делать если при тестировании кода мне надо подменить базовый класс?
Делать нечего, надо вешаться ибо в данном случае его не подменить, а вот если было бы так:
class DerivedClass
{
public DerivedClass(IBaseClass baseClassImplementation)
{
_baseClassImplementation = baseClassImplementation;
}
}
В данном случае зависимость не жёсткая и передать мы можем что угодно, в качестве "базового" класса.
Другое дело что если вдруг нам надо действительно расширить или поменять функционал базового класса то придётся кучу методов просто "прокидывать".
Здравствуйте, Tom, Вы писали:
Tom>Соответственно встает вопрос, а нужно ли наследование (реализации) вообще.
+1
Единственный известный мне случай, когда оно имеет смысл — Template Method. Хотя и его можно легко "вывернуть" в Стратегию.
Здравствуйте, Tom, Вы писали:
Tom>Здравствуйте, michael_isu, Вы писали:
_>>Здравствуйте, Tom, Вы писали:
Tom>>>Соответственно встает вопрос, а нужно ли наследование (реализации) вообще.
_>>Слишком абстрактный вопрос задаете. Лучше пример Иначе опять 30 страниц флуда ни о чем будет.
Tom>Да пример очень простой:
Пример в смысле из жизни, реальный.
Tom>Другое дело что если вдруг нам надо действительно расширить или поменять функционал базового класса то придётся кучу методов просто "прокидывать".
Вообще термин "базовый" какой-то нехороший, что значит базовый? Программа это набор скомпонованных компонентов, у каждого своя важная роль и задача. Выделять "базовые" классы, от которых начинает все разрастаться — значит надежно цементировать весь проект вокруг этих классов, т.к. все вокруг зависит от них и каждый норовит сколько-нибудь общий код запихать повыше в базовый. Что ещё больше цементирует проект вокруг них, т.к. этот общий функционал начинают все юзать. Такая вот положительная обратная связь нарастающим итогом. Итог — плачевный.
Базовые классы иногда нужны, но должны быть очень веские причины и большие полномочия для их создания и изменения. Но в большинстве случаев туда пишут все кто хотят, не понимая последствий.
Tom>>Да пример очень простой: _>Пример в смысле из жизни, реальный.
Куда реальнее, самый реальный пример из реального проекта.
Tom>>Другое дело что если вдруг нам надо действительно расширить или поменять функционал базового класса то придётся кучу методов просто "прокидывать".
_>Базовые классы иногда нужны, но должны быть очень веские причины и большие полномочия для их создания и изменения. Но в большинстве случаев туда пишут все кто хотят, не понимая последствий.
Под базовым имелось ввиду просто класс от которого кто то наследуется.
Здравствуйте, Tom, Вы писали:
Tom>стало ясно, что наследование создает очень сильную связь между наследником и базовым классом.
не вижу никакой проблемы.
Наследование применяется там, где экземпляры типа-наследника являются экземплярами типа-предка (по крайней мере должно применяться). Более того, наиболее правильно наследование применять, если имеет место уточнение реализации.
Лично в базовый класс выношу только те методы, которые являются общими для всех классов "семейства", а реализацию в базовый тип переношу только в том случае, если соответственно реализация оказывается общей для всех классов "семейства".
В таких случаях как IUnkonwn понятно, где могут быть реализованы методы AddRef() и Release()
после отмены наследования реализации именно потому, что внутри иерархии они не зовутся, но если метод базового класса планируется вызвать чуть ли не во всей иерархии, то куда его помещать — загадка, но есть и более запутанные случаи:
1)
abstract class Base {protected void BaseMethod(){}; protected abstract void Do();}
abstract class A : Base { protected void MethodA() { BaseMethod() };}
class B : A { protected void MethodB() { MethodA() }; override void Do {MethodB();} }
2)
class Control {protected virtual void WndProc(); protected virtual void DefWndProc(); }
class ScrollableControl :Control {protected override void WndProc() { base.WndProc(); } }
class ContainerControl :ScrollableControl {protected override void WndProc() { base.WndProc(); } }
class Form :ContainerControl {protected override void WndProc() { base.WndProc(); } }
class MyForm :Form {protected override void WndProc() { base.WndProc(); DefWndProc()} }
во что превратятся подобные иерархии после отмены наследования реализации?
Tom>Соответственно встает вопрос, а нужно ли наследование (реализации) вообще.
Нет не нужно.
Ровно настолько же, насколько не нужно и ООП — без него вполне можно жить.
Однако в ряде случаев оно может упростить жизнь.
Всё сказанное выше — личное мнение, если не указано обратное.