В ролике четко обозначена проблема наследования.
Ты думаешь, что у тебя связь
C extends B extends A
, а потом оказывается, что надо
X extends B but not A and C
,
Y extends A and C bot not B
, ты пытаешься создавать деревья классов типа XAB, YAC и т.д. и у тебя всё летит к чертям, и надо переписывать пол-проекта.
Т.е. наследование создаёт прибитую гвоздями связь между интерфейсом и реализацией. Поэтому в проекте на века нужно разделить на интерфейс и класс с реализацией этого интерфейса. И потом ты когда создаешь конкретный класс, пишешь (немного ООП-псевдокода)
class MyX implements A, B {
AImpl aImpl = new AImpl();
BImpl bImpl = new BImpl();
int foo() { return
aImpl.foo(); }
int foo2() { return aImpl.foo2(); }
int bar() { return
aImpl.bar(); }
int bar2() { return aImpl.bar2(); }
void spam() { bImpl.spam(); }
void spamspam() { bImpl.spamspam(); }
void eggs() { bImpl.eggs(); }
void eggsAB() { bImpl.eggsAB(); }
}
И так ты достиг плоской, а не древовидной схемы, в любой момент ты можешь заменить один объект реализации другим, ничего не потеряв и ни с какими проблемами не столкнувшись.
Но тут другая проблема: слишком много шаблонных методов делегирования
И нормальные дефолт методы это решают.
Пишешь
interface A {
AImpl getAImpl();
int foo() { return getAImpl().foo(); }
int foo2() { return getAImpl().foo2(); }
int bar() { return getAImpl().bar(); }
int bar2() { return getAImpl().bar2(); }
}
interface B {
BImpl getBImpl();
void spam() { getBImpl().spam(); }
void spamspam() { getBImpl.spamspam(); }
void eggs() { getBImpl().eggs(); }
void eggsAB() { getBImpl().eggsAB(); }
}
Весь боилерплейт делегирования остался в интерфейсе, в классе нужно только объявить геттер к объекту.
class MyX implements A, B {
AImpl aImpl = new AImpl();
BImpl bImpl = new BImpl();
AImpl getAImpl() { return aImpl; }
BImpl getBImpl() { return bImpl; }
}
class MyOtherX implements A, B {
AImpl aImpl = new OtherAImpl();
BImpl bImpl = new OtherBImpl();
AImpl getAImpl() { return aImpl; }
BImpl getBImpl() { return bImpl; }
}
class MyY implements A, C {
AImpl aImpl = new AImpl();
CImpl cImpl = new CImpl();
AImpl getAImpl() { return aImpl; }
CImpl getCImpl() { return cImpl; }
}
class MyZ implements A, B, C {
AImpl aImpl = new AImpl();
BImpl bImpl = new OtherBImpl();
CImpl cImpl = new CImpl();
AImpl getAImpl() { return aImpl; }
BImpl getBImpl() { return bImpl; }
CImpl getCImpl() { return cImpl; }
}
И ты можешь наштамповать огромное количество самых разнообразных классов просто создавая класс, геттер к реализатору и имплементируя интерфейс. Реализатор действует по принципу паттерна Статегия.
И не будет деревянной связи. И не придется думать, типа че делать, мне нужно животное, которое умеет ходить, потом которое умеет ходить и летать, потом которое умеет ходить и плавать, мне что создавать AbstractWalkingAnimal, AbstractWalkingFlyingAnimal, AbstractWalkingSwimingAnimal? А если мне в будущем понадобится добавить животным атаку, мне каждому варианту добавлять наследника AbstractAttack***Animal и AbstractNotAttack***Animal?
Эти проблемы наследование не решает. Наследование решает только проблему уровня "здесь и сейчас", на долгую игру оно в основном только создает проблемы.
Почему в шарпах нужно кастовать объект к интерфейсу, чтобы вызывать его методы, я хз. В расте не нужно.