如果您熟悉面向对象的编程,那么您很可能熟悉子类化和继承。然而,继承的名声一直很差。我相信这是因为当您需要修改程序时,一些开发人员将其视为包罗万象的解决方案。这样做的问题是类层次结构可能变得难以管理。
我们可以使用其他设计模式 来使我们的应用程序更易于理解并为更改做好准备。我将向您展示如何使用继承以及装饰器和复合模式来改进程序设计的示例。
遗产
继承背后的想法是一个对象“是”另一个对象的特殊版本。有一个父类(也称为超类)定义了我们对象的基本属性。并且有一个子类(子类)继承了父类的属性。
继承的一个例子是狗和贵宾犬。所有的狗都有特定的特征,比如四只腿和吠叫的能力。贵宾犬“是”一种狗。S uv是一种车辆。圆是一种形状。如果我们正在设计一个用于创建形状的程序,这就是我们的类层次结构的样子。
拥有 Shape 类的好处是我们可以重用我们在其他类中定义的属性和方法。
请注意,该getArea
方法在我们的每个子类中都重新定义了。我们不必重新定义此方法,但我们这样做是为了替换父级对该方法的实现。那是因为每个形状都有自己计算面积的方法。
在子类中覆盖父方法是多态性的一个例子。多态性是对象具有多种形式的能力。它允许子类拥有与其超类同名但实现不同的方法。
这是创建 Shape 和 Circle 子类的代码示例:
class Shape { constructor(x, y) { this.xposition = x; this.yPosition = y; } getArea() {...} } class Circle extends Shape { constructor(x, y, radius) { super(x, y, radius); this.radius = radius } getArea() {...} } let circle = new Circle(1,2,3);
这种设计选择的缺点之一是,如果您决定父类需要更改,那么子类可能也需要更改,以及我们创建的所有对象。
例如,假设我们稍后决定用对象替换 Shape 类的 x 和 y 参数会更好。因此,我们需要更改所有子类的构造函数以及我们实例化的每个对象的参数。
我们可以看到这很容易成为问题。我们必须确保我们的设计第一次就正确,这样我们就可以避免做出改变。但这不切实际,也不是我们应该努力的方向。程序是一个不断发展的实体,如果我们能够灵活地轻松进行更改,对我们开发人员来说会更好。至少,我们不应该有超过一层的子类。
装饰图案
装饰器允许我们在创建对象后将属性附加到对象。这意味着我们可以添加功能而无需子类化或关注对象的实现。
我们可以使用 Shape 类来创建圆并用我们想要的附加属性包装它,而不是认为圆是一个形状。这是使用装饰器创建圆形对象的替代方法:
class Shape { constructor(data) { this.x = data.x; this.y = data.y; } getArea() {...} } function CircleDecorator(shape) { shape.radius = 3; shape.getArea = function() {...}; return shape; } let shape = new Shape({x:1,y:2}); let circle = new CircleDecorator(shape);
我们可以使用装饰器添加或修改 Shape 类的成员,而不是对其进行子类化。在我们的形状示例中,您可能会发现您只想创建一个具有您需要的所有属性的圆形对象,并对其他形状执行相同操作。那也行。但是装饰器允许我们重用 Shape 类中的代码,并使用每个形状不同的功能对其进行修改。结果,我们将拥有更多的对象,但更多的对象比更多的子类更容易管理。
这种模式不限于创建图形。您可以在任何想要向对象添加职责的情况下使用它。
例如,我们可能有一个类来处理将用户注册到我们的帐户。在我们将他们的信息保存到我们的数据库之前,明智的做法是检查输入是否有效并且不包含任何恶意脚本。我们可以用一个方法来装饰这个类来首先验证信息。这个相同的装饰器可以在我们接受用户输入的应用程序的任何地方重用。
复合图案
对象可以由其他对象组成。应用程序的视图可以被认为是由其他视图组成的组件。如果我们正在制作游戏,我们的游戏世界将显示我们创建的所有图形,例如圆形和正方形。每次更新视图时,我们都需要重新绘制每个元素。我们需要一种方法来将所有元素作为一个组进行管理。
这就是复合模式可以帮助我们的地方。我们可以创建一个负责所有元素的类。当我们想要重绘元素时,我们调用这个类的 draw 方法,它会在每个单独的元素上调用 draw 方法。
class Component { constructor(name){ this.components = []; this.element = document.createElement(name); } add(elem) { this.components.push(elem); } draw() { for (const elem of this.components) { elem.draw(); } } } class Circle { constructor(data) { this.x = data.x; this.y = data.y; this.radius = data.radius; } getArea() {...} draw() {...} } let world = new Component('div'); let circle = new Circle({x: 1, y:1, radius: 2}); let circle2 = new Circle({x: 10, y:10, radius: 2}); world.add(circle); world.add(circle2); world.draw();
webp时代也可以被认为是一个组件。这个组件可以有一个菜单、一个侧边栏和一篇博文。帖子将是一个包含图片、标题和正文的子组件。我们的主应用程序组件的draw
方法将在菜单、侧边栏和帖子上调用 draw。帖子组件将依次调用帖子图像、标题和正文的绘制。
这是一个网页想要分成组件的视图:
这种模式不限于创建视图。例如,如果我们正在制作游戏,我们可能有一个组件来管理屏幕上的元素,一个组件来管理音频,一个组件来管理游戏状态。
这些都将在我称为游戏引擎的组件中。游戏引擎将有一个初始化方法,该方法将在其每个子组件上调用初始化方法。这就是使用复合模式的力量。我们可以将它们视为一个对象,而不是处理单个对象。
结论
继承允许我们通过根据另一个对象定义一个对象来重用代码。装饰器模式允许我们在不更改原始代码或子类的情况下向对象添加职责。复合模式对部分-整体关系进行建模。这些模式并不意味着单独使用。