function Extends(clazz) {
return class extends clazz {
// ...
}
}
让我解释一下它是如何工作的。代替常规继承,我们使用上面的机制。然后,仅在创建对象时指定基类:
const Class = Extends(Base)
const object = new Class(...args)
我将尝试说服您,这是我母亲的类继承朋友,他是继承类的一种方式,并且是一种使继承回到真正的OOP工具标题的方法(当然是在原型继承之后)。
几乎没有题外话
, , , pet project , pet project'. , .
让我们在名称上达成一致:我将这种技术称为mixin,尽管这仍然有些不同。在被告知这些是来自TS / JS的mixin之前,我使用了名称LBC(后期绑定类)。
类继承的“问题”
我们都知道“每个人”“不喜欢”类继承的方式。他有什么问题?让我们弄清楚它,同时了解混合如何解决它们。
实现继承破坏了封装
OOP的主要任务是将数据和对其进行的操作绑定在一起(封装)。当一个类从另一个类继承时,这种关系就被打破了:数据在一个地方(父),操作在另一个地方(继承者)。而且,继承者可以重载该类的公共接口,以使基类的代码或继承类的代码都无法分别判断对象状态将发生什么。即,类是耦合的。
Mixins反过来大大减少了耦合:如果在声明继承类时根本没有基类,继承者应该依赖哪个基类的行为?但是,由于此方法的后期绑定和方法重载,“溜溜球问题”遗迹。如果你在设计中使用继承,从中逃脱不了,但是,例如,在科特林关键字
open
和override
应大大缓和的情况下(我不知道,不与科特林过于密切熟悉)。
继承不必要的方法
具有列表和堆栈的经典示例:如果您从列表继承堆栈,则列表接口中的方法将进入堆栈接口,这可能会违反堆栈不变式。我不会说这是一个继承问题,因为,例如,在C ++中,对此存在私有继承(并且可以使用来公开各个方法
using
),因此这是单个语言的问题。
缺乏灵活性
- , : . , , : , , cohesion . , .
- ( ), . , : , .
- . - , . , , ? - — , .. .
- . - , - . , , .
如果一个类继承自另一个类的实现,则更改该实现可能会破坏继承的类。在这篇文章中,很好地说明了这些问题
Stack
和问题MonitorableStack
。
使用mixin,程序员必须考虑到他编写的继承类不仅必须与某些特定的基类一起工作,而且还必须与与该基类的接口相对应的其他类一起工作。
香蕉,大猩猩和丛林
OOP保证了可组合性,即 在不同情况下甚至在不同项目中重用各个类的能力。但是,如果一个类从另一个类继承,则为了重用继承者,您需要复制所有依赖项,基类及其所有依赖项及其基类……。那些。要香蕉,拔出大猩猩,再挖出丛林。如果在创建对象时就考虑了依赖倒置原则,则依赖关系并不是很糟糕-只需复制其接口即可。但是,这不能通过继承链来完成。
反过来,Mixins使得(必须)使用与继承相关的DIP。
Mixins的其他设施
mixins的优点还不止于此。让我们看看您还能对他们做什么。
继承体系的死亡
类不再相互依赖:它们仅依赖于接口。那些。实现成为依赖关系图的叶子。这应该使重构更加容易-现在,域模型不依赖于其实现。
抽象类的死亡
不再需要抽象类。让我们看一下从重构专家那里借来的Java中的Factory Method模式的示例:
interface Button {
void render();
void onClick();
}
abstract class Dialog {
void renderWindow() {
Button okButton = createButton();
okButton.render();
}
abstract Button createButton();
}
是的,当然,工厂方法演变为构建器和策略模式。但是您可以使用mixins做到这一点(让我们想象一下Java具有一流的mixins):
interface Button {
void render();
void onClick();
}
interface ButtonFactory {
Button createButton();
}
class Dialog extends ButtonFactory {
void renderWindow() {
Button okButton = createButton();
okButton.render();
}
}
您几乎可以使用任何抽象类来完成此技巧。一个不起作用的示例:
abstract class Abstract {
void method() {
abstractMethod();
}
abstract void abstractMethod();
}
class Concrete extends Abstract {
private encapsulated = new Encapsulated();
@Override
void method() {
encapsulated.method();
super.method();
}
void abstractMethod() {
encapsulated.otherMethod();
}
}
在这里,
encapsulated
重载method
和实现都需要该字段abstractMethod
。也就是说,在不破坏封装的情况下,Concrete
无法将类拆分为子Abstract
类和“超类” Abstract
。但是我不确定这是否是一个好的设计的例子。
灵活性可与类型媲美
细心的读者会注意到,这些都与Smalltalk / Rust特质非常相似。有两个区别:
- Mixin实例可以包含不在基类中的数据。
- Mixins不会修改它们继承的类:要使用mixin的功能,您需要显式创建mixin对象,而不是基类。
第二个差异导致这样一个事实,即混合素在本地起作用,而不是在基类的所有实例上起作用的特征。它的方便程度取决于程序员和项目,我不会说我的解决方案肯定更好。
这些差异使mixins更接近于正常继承,因此在我看来,这是继承与特性之间的一个有趣折衷。
mixins的缺点
哦,就这么简单。混合素肯定有一个小问题和一个减去脂肪。
爆炸界面
如果只能从该接口继承,显然,项目中将有更多接口。当然,如果在项目中观察到DIP,则其他几个界面将无法正常运行,但并非所有界面都遵循SOLID。如果基于每个类生成包含所有公共方法的接口,并在提及类名时区分该类是作为对象的工厂还是作为接口,则可以解决此问题。在TypeScript中完成了类似的操作,但是由于某些原因,在生成的接口中提到了私有字段和方法。
复杂的构造函数
使用mixins,最困难的任务是创建一个对象。根据构造函数是否包含在基类接口中,考虑两个选项:
- , , . , - . , .
- , . :
interface Base { new(values: Array<int>) } class Subclass extends Base { // ... } class DoesntFit { new(values: Array<int>, mode: Mode) { // ... } }
DoesntFit
Subclass
, - .Subclass
DoesntFit
,Base
. - 实际上,还有另一个选择-向构造函数传递的不是参数列表,而是字典。这解决了上面的问题,因为它
{ values: Array<int>, mode: Mode }
显然与模式匹配{ values: Array<int> }
,但是却导致这种字典中名称的不可预测的冲突:例如,超类A
和继承者都B
使用相同的参数,但是该名称未在for的基类的接口中指定B
。
而不是结论
我确定我错过了这个想法的某些方面。或者说这已经是一种手风琴式手风琴,而在20年前,就有一种语言使用了这种想法。无论如何,我都在等待您的评论!
来源清单
neethack.com/2017/04/Why-inheritance-is-bad
www.infoworld.com/article/2073649/why-extends-is-evil.html
www.yegor256.com/2016/09/13/inheritance-is- procedural.html
refactoring.guru/ru/design-patterns/factory-method/java/example
scg.unibe.ch/archive/papers/Scha03aTraits.pdf