从 EventEmitter 继承是一种反模式吗?

Is inheriting from the EventEmitter an antipattern?

如果您希望 class 支持事件,从 EventEmitter 继承似乎是常见的做法。 例如 Google 为 Puppeteer, the WebSocket module does it, mongoose does it 执行此操作,...仅举几例。

但这真的是好做法吗?我的意思是确保它看起来漂亮干净,但从 OOP 的角度来看它似乎是错误的。例如:

const EventEmitter = require('events')
class Rectangle extends EventEmitter {
    constructor(x,y,w,h) {
        super()
        this.position = {x:x, y:y}
        this.dimensions = {w:w, h:h}
    }
    setDimensions(w,h) {
        this.dimensions = {w:w, h:h}
        this.emit('dimensionsChanged')
    }
}

会让人觉得 Rectangle 的核心是 EventEmitter,尽管事件功能是次要的。

如果您决定 Rectangle 现在需要从一个名为 Shape 的新 class 继承怎么办?

class Shape {
    constructor(x,y) {
        this.position = {x:x, y:y}
    }
}

class Rectangle extends Shape {
    constructor(x,y,w,h) {
        super(x,y)
        this.dimensions = {w:w, h:h}
    }
}

现在您必须 Shape 从 EventEmitter 继承。即使只有一个 class 继承自 Shape 实际上需要事件处理。

这样的事情不是更有意义吗?

class Shape {
    constructor(x,y) {
        this.position = {x, y}
    }
}

const EventEmitter = require('events')

class Rectangle extends Shape {
    constructor(x,y,w,h) {
        super(x,y)
        this.dimensions = {w, h}
        this.em = new EventEmitter()
    }
    setDimensions(w,h) {
        this.dimensions = {w:w, h:h}
        this.em.emit('dimensionsChanged')
    }
}

const rectangle = new Rectangle(1,2,3,4)
rectangle.em.on('dimensionsChanged', ()=>{console.log('dimensions changed')})
rectangle.setDimensions(10,20)

是的,这绝对更有意义。

继承应该用来表示是一个关系:class Rectangle extends Shape是可以的,因为Rectangle是一个形状,但是这里的矩形本身 不是 EventEmitter.

相反,我们有一个 can 关系:Rectangle can emit an event,这正是你应该支持 composition over inheritance,这就是您上一个代码片段中发生的情况。

我们只能推测为什么一些著名的库不这样做——追溯兼容性、API 的简单性,或者仅仅是糟糕的设计。