面对对象程序的设计准则
面对对象程序由对象组成, 对象包括数据和对数据进行操作的过程, 过程通常被称为方法或操作.
针对接口编程, 而不是针对实现编程,即往往我们不直接生成具体的类,而是先定义一个抽象类,让子类实现相应接口
优先使用对象组合, 而不是类继承。类继承导致父类的改动可能会影响所有能够追溯到此类的子类(如a继承b,b继承c,那么c的改动可能会影响a和b),并且继承嵌套越多,越难排查。
使用组合意味着往往一个大的复杂的对象可以合理的被拆分为可复用的多个小对象。
保证代码的松耦合,紧耦合的代码部分考虑使用中介者模式
创建型模式
创建型设计模式抽象了实例化过程。
Abstract Factory(抽象工厂)
意图
提供一个接口(结构中的 AbstractFactory)以创建一系列相关或互相依赖的对象,而无需指定它们具体的类。
结构
场景
考虑构建适应不同系统的UI界面,如Windows 7,Windows 10,macOS。
一个UI界面应当由许多基本的Widget(组件)构成,如Window(窗口)、ScrollBar(滚动条)等。
我们需要为不同系统的窗口、滚动条定义相应的外观和行为。
实现
abstract class WidgetFactory {
abstract getWindow(): any;
abstract getScrollBar(): any;
}
class Win7WidgetFactory extends WidgetFactory {
private static Window: any;
private static ScrollBar: any;
getWindow() {
if (!Win7WidgetFactory.Window) {
Win7WidgetFactory.Window = {
};
}
return Win7WidgetFactory.Window;
}
getScrollBar() {
}
}
class Win10WidgetFactory extends WidgetFactory {
}
class UI {
Window: any;
ScrollBar: any;
constructor(widgetFactory: WidgetFactory) {
this.Window = widgetFactory.getWindow();
this.ScrollBar = widgetFactory.getScrollBar();
}
}
Builder(生成器)
意图
将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。
结构
场景
假设我们需要表示一条街道上的门面房。
那么我们最终应该得到一个Street(街道)实例,里面包含了我们需要的信息。
大部分情况下,我们需要Street实例表示出标准的信息,但是对于部分应用,我们仅需要Street实例表示出街道拥有的房子数量。
实现
class StreetBuilder {
buildStreet() {}
buildRoom() {}
}
class StandardStreetBuilder extends StreetBuilder {
buildStreet() {
}
buildRoom() {
}
getStreet() {
return {
};
}
}
class CountingStreetBuilder extends StreetBuilder {
private roomCount = 0;
buildRoom() {
this.roomCount += 1;
}
getRoomCount() {
return this.roomCount;
}
}
class Street {
street: any;
constructor(streetBuilder: StreetBuilder) {
streetBuilder.buildStreet();
streetBuilder.buildRoom();
streetBuilder.buildRoom();
if (streetBuilder instanceof StandardStreetBuilder) {
this.street = streetBuilder.getStreet();
}
if (streetBuilder instanceof CountingStreetBuilder) {
this.street = streetBuilder.getRoomCount();
}
}
}
Factory Method(工厂方法)
意图
定义一个用于创建对象的接口,让子类决定实例化哪一个类。
结构
场景
在上面Street(街道)的例子中,我们直接在构造函数里初始化了street实例。
实际上,街道中的房子数量是不确定的,这里应该有种方式能够提供可变的类实例。
实现
class Street {
street: any;
constructor(protected streetBuilder: StreetBuilder) {
this.street = this.createStreet();
}
createStreet() {
this.streetBuilder.buildStreet();
this.streetBuilder.buildRoom();
this.streetBuilder.buildRoom();
if (this.streetBuilder instanceof StandardStreetBuilder) {
return this.streetBuilder.getStreet();
}
if (this.streetBuilder instanceof CountingStreetBuilder) {
return this.streetBuilder.getRoomCount();
}
}
}
class MyStreet extends Street {
constructor(protected streetBuilder: StreetBuilder) {
super(streetBuilder);
}
createStreet() {
this.streetBuilder.buildStreet();
this.streetBuilder.buildRoom();
this.streetBuilder.buildRoom();
this.streetBuilder.buildRoom();
if (this.streetBuilder instanceof StandardStreetBuilder) {
return this.streetBuilder.getStreet();
}
if (this.streetBuilder instanceof CountingStreetBuilder) {
return this.streetBuilder.getRoomCount();
}
}
}
Prototype(原型)
意图
用原型实例指定创建对象的种类,并且通过拷贝这些原型创建新的对象。
补充:js的核心思想,不过稍有区别,js是通过原型链查询属性,如chrome中的实现:__proto__ -> prototype ,需要注意,原型本质是继承的一种,滥用原型或过深的原型链结构会导致系统紧耦合。
结构
场景
作为JS里的核心概念,这里不做举例,想了解原型和原型链的可以参考我的另一篇文章。
Singleton(单件)
意图
保证一个类仅有一个实例,并提供一个访问它的全局访问点。
结构
场景
考虑我们使用的Vue、React等框架,都有一个 root(根)
现在我们要确保root的唯一性且全局可访问。
实现
class Root {
private static app: any;
static getApp() {
if (!Root.app) {
Root.app = new Root();
}
return Root.app;
}
constructor() {
}
}
结构型模式
结构型模式涉及如何组合类和对象以获得目标结构。
Adapter(适配器)
意图
将一个类的接口转换成客户希望的另一个接口。
结构
Adapter有俩种实现方式,其中一种为多重继承,目前 ts 是无法实现的,但是我们可以通过 implements 实现接口的方式类比这种行为
场景
现在我们有一个图像编辑器,定义了基本的 Shape 接口。
我们的 LineShape、CircleShape 等都实现了 Shape 接口。
现在需要实现 TextShape,我们发现了一个库中的 Text 类基本满足 TextShape 中的某些接口的实现。
实现
下面俩种实现各有优劣,主要为继承让我们可以修改 Text 的部分行为, 组合让我们可以引入多个Adaptee去生成Adapter。
- 多重继承
class Text {
specificGetArea() {}
}
interface Shape {
getArea(): any;
}
class TextShape extends Text implements Shape {
getArea() {
this.specificGetArea();
}
}
- 对象组合
class Text {
specificGetArea() {}
}
interface Shape {
getArea(): any;
}
class TextShape implements Shape {
text = new Text();
getArea() {
this.text.specificGetArea();
}
}
Bridge(桥接)
意图
将抽象部分与实现部分分离,使它们可以独立地变化。
结构
场景
现在我们需要实现一个剪切板。
对于web应用,用Clipboard很容易实现该功能,但是浏览器支持不佳,我们需要考虑兼容性问题。
实现
abstract class Implementor {
abstract read(): any;
abstract write(): any;
}
class IE6Implementor extends Implementor {
read() {
}
write() {
}
}
class ChromeImplementor extends Implementor {
}
class Clipboard {
constructor(private implementor: Implementor) {}
copy() {
this.implementor.write();
}
paste() {
this.implementor.read();
}
}
Composite(组合)
意图
将对象中成树形结构以表示 “部分 - 整体” 的层次结构。Composite 使得用户对单个对象和组合对象的使用具有一致性。
结构
场景
DOM树结构就是Composite设计模式
Decorator(装饰器)
意图
动态地给一个对象添加一些额外的职责。
结构
场景
即ts中的装饰器
Facade(外观)
意图
对于一个复杂系统,我们往往会将其划分为若干个子系统来降低复杂度,而我们希望子系统之间的通讯或者说依赖关系能够尽可能的少和清晰,这就是Facade的工作。
由 Facade 对外暴露一些常用接口。
结构
Flyweight(享元)
意图
运用共享技术有效地支持大量细粒度的对象。
结构
场景
享元本质上是为了节约创建大量对象的内存存储。
而 js 天然的原型链已经提供相应的解决方案。
思考处理几万字的英文文档,可以修改每个字符的样式。
如果为每个字符创建一个对象,那么我们需要几万个对象。
享元操作如下:
- 为26个字母创建对象(Flyweight),每个Flyweight对象有内部状态,即对应的字母和一些其它的基本信息,内部状态应该不受到应用场景的影响,以及如何处理外部状态,如字符的大小,颜色等。
- 历遍文档的字符,查询到对应的Flyweight对象,将外部状态传递给Flyweight处理,得到一个完整的字符对象,我们可以渲染该字符,执行完毕,完整的字符对象被释放。
- 在整个系统当中,我们一直保持的只有26个Flyweight对象
享元设计得到的效果受很多因素影响,如这里文档如果包含中文字符,创建Flyweight对象显然不可取,还有外部状态是否不变,如果每个文字都有相同格式,那么我们可以用一个对象存储外部状态,这样就不用每次都计算一遍外部状态。
Proxy(代理)
意图
为其他对象提供一种代理以控制对这个对象的访问。
结构
场景
参考js中的 Proxy。
行为型模式
行为型模式涉及算法和对象间职责的分配。
Chain of Responsibility(职责链)
意图
使多个对象都有机会处理请求,从而避免请求的发送者和接收者之间的耦合关系。将这些对象连成一条链,并沿着这条链传递该请求,直到有一个对象处理它为止。
结构
场景
DOM 事件流就是职责链设计,如果需要事件传递到某个元素为止,只需要stopPropagation()
Command(命令)
意图
简单来说 Command 实现了js中的回调函数, 只不过 Command 是作为面对对象设计的模式。
Command 可以让用户提供自定义的处理程序。
结构
Interpreter(解释器)
意图
就是编译器中的解释器。
结构
Iterator(迭代器)
意图
就是 JS 中的迭代器 [Symbol.iterator]。
结构
Mediator(中介者)
意图
用一个中介对象来封装一系列的对象交互。中介者使各对象不需要显示地相互引用,从而使其耦合松散,而且可以独立地改变他们之间的交互。
类似于 Facade(外观)模式,只不过 Facade(外观)针对系统设计,Mediator(中介者)针对对象之间的交互。
结构
场景
思考这样一种场景,我们有若干按钮以及若干弹窗,若干按钮可以控制弹窗的展现,隐藏以及弹窗的叠加顺序等。
如果我们直接在按钮和弹窗里实现它们之间的逻辑交互,这明显会导致紧耦合,组件的交互和组件本身紧密结合在了一起。
其实前端使用的组件库和编写业务代码本身就是一种Mediator(中介者)模式,我们分离了组件对象之间的交互。
Memento(备忘录)
意图
在不破坏封装性的情况下,捕获一个对象的内部状态,并在该对象之外保存这个状态,这样以后就可将该对象恢复到原先保存的状态。
结构
Observer(观察者)
意图
定义对象间的一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并执行更新。
著名的 rxjs 库就是典型的观察者模式。
结构
State(状态)
意图
允许一个对象在其内部状态改变时改变它的行为。
结构
场景
当我们建立网络连接时,其行为依赖于当前的的网络状态,简单来说,如果当前未连接网络,我们可以连接网络,而不能断开网络,如果当前已经连接网络,我们可以重新连接网络(即断开网络后连接网络)同时我们也可以断开网络
实现
abstract class NetState {
abstract Open(): any;
abstract Close(): any;
}
class NetConnected extends NetState {
Open() {
this.Close();
return "Connected";
}
Close() {
return "Closed";
}
}
class NetClosed extends NetState {
Open() {
return "Connected";
}
Close() {
return "Closed";
}
}
class NetConnect extends NetState {
private state = "Closed";
private netConnected = new NetConnected();
private netClosed = new NetClosed();
Open() {
this.state = this.state === "Closed" ? this.netClosed.Open() : this.netConnected.Open();
}
Close() {
this.state = this.state === "Closed" ? this.netClosed.Close() : this.netConnected.Close();
}
}
Strategy(策略)
意图
定义一系列的算法,把它们一个个封装起来,并且使它们可互相替换。
结构
场景
经典的排序问题拥有非常多的算法,对于不同的情况我们往往有最合适的排序算法解决问题。与state模式相似,我们可以提供抽象类,然后实现不同的算法。
Template Method(模板方法)
意图
定义一个操作中的算法的骨架,将一些步骤延迟到子类中。
结构
场景
非常类似于 Factory Method(工厂方法),只不过Template Method(模板方法)由子类提供一些操作(步骤)。
众所周知,对于vue、angular这些框架,组件都有一个生命周期,我们可以在特定的钩子(hook)执行一些操作,这就是典型的Template Method(模板方法)设计。
Visitor(访问者)
意图
定义一个操作中的算法的骨架,将一些步骤延迟到子类中。
结构
场景
假设我们实现了虚拟DOM,虚拟DOM由 很多类对象组成,如 style、class、事件等,但是我们对虚拟DOM的某些操作是只依赖于具体类的,与虚拟DOM本身其实是没有关系的,如我们希望修改class时能够有相应的事件通知。
实现
class Visitor {
classChange(dom: VirtualDOM) {
}
}
class VirtualDOM {
style = {
};
class = {
};
event = {
};
visitor: Visitor;
accept(visitor) {
this.visitor = visitor;
}
someOperation() {
this.visitor.classChange(this);
}
}
|