0%

设计模式之美学习笔记1

面向对象

封装、抽象、继承、多态

面向对象的关键是其四大特性

封装 Encapsulation

也叫作信息隐藏或者数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类提供的方式(或者叫函数)来访问内部信息或者数据。
课程中提供的代码是 java,我用 ts 重新实现了一遍,当做是练习 ts 的熟练度了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
let id = 0;
class Wallet {
private id: number;
private createTime: Date;
private balance: number;
private balanceLastModifiedTime: Date;
constructor() {
this.id = id += 1;
this.createTime = new Date();
this.balance = 0;
this.balanceLastModifiedTime = new Date();
}

public getId(): number {
return this.id;
}
public getCreateTime(): Date {
return this.createTime;
}
public getBalance(): number {
return this.balance;
}
public getBalanceLastModifiedTime(): Date {
return this.balanceLastModifiedTime;
}
public increaseBalance(amount: number): void {
this.balance += amount;
this.balanceLastModifiedTime = new Date();
}
public decreaseBalance(amount: number): void {
this.balance -= amount;
this.balanceLastModifiedTime = new Date();
}
}

对于封装这个特性,需要编程语言本身提供访问权限控制的语法机制来支持。

之所以这样设计,是因为从业务的角度来说,id、createTime 在创建钱包的时候就确定好了,之后不应该再被改动,所有只提供 get 方法,这两个属性的初始化设置,对于调用者也是透明的,所以不提供用构造参数的方式进行外部赋值。

对于钱包余额 balance 这个属性,从业务的角度来说,只能增或者减,不会被重新设置。所在 Wallet 类中,只暴露了 increaseBalance() 和 decreaseBalance() 方法,并没有暴露 set 方法。alanceLastModifiedTime 这个属性,它完全是跟 balance 这个属性的修改操作绑定在一起的。只有在 balance 修改的时候,这个属性才会被修改,所以其相关的操作,都封装在 increaseBalance() 和 decreaseBalance() 方法中了

封装可以解决这些问题

如果对类中的属性访问不做控制,类的属性可能在代码的各个角落被随意修改,影响代码的可读性和可维护性
类通过有效的方法暴露必要的操作,也可以提高类的易用性

抽象 Abstraction

隐藏方法的具体实现,让调用者只需要关心方法提供了哪些功能,并不需要知道这些功能是如何实现的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
interface Picture {
id: string;
blob: Blob;
metaInfo: Object;
}

interface IPictureStorage {
savePicture(pic: Picture): void;
getPicture(id: string): Picture;
deletePicture(id: string): void;
midifyMetaInfo(id: string, metaInfo: Object);
}

class PictureStorage implements IPictureStorage {
savePicture(pic: Picture): void {}
getPicture(id: string): Picture {
return {
id: "1",
blob: new Blob(),
metaInfo: {},
};
}
deletePicture(id: string): void {}
midifyMetaInfo(id: string, metaInfo: Object) {}
}

抽象这个特性非常用以实现,不需要编程语言提供特殊的语法机制来支持,只需要提供“函数”这一非常基础的语法机制,函数本身就是一种抽象。

相较于课程中代码,为了简化代码,有所改动。调用者在使用图片存储功能时,只需要了解 IPictureStorage 暴露了哪些方法就可以了,不需要查看 PictureStorage 里面的具体实现。

抽象可以解决这些问题

只关注功能点,不关注实现,很多设计原则体现了抽象的设计思想,如基于接口而非实现编程,开闭原则,代码解耦。我们在定义方法的时候,也要有抽象思维,不要再方法定义中,暴露太多的细节。如 getAliyunPictureUrl() 就不好,如果某一天讲图片存储地址改变了,那么函数命名也要改变,相反,应该叫做 getPictureUrl(),即便内部存储方式修改了,我们也不需要修改命名

继承 Inheritance

上升一个思维层面,去思考继承这一特性,可以这么理解:我们代码中有一个猫类,有一个哺乳动物类。猫属于哺乳动物,从人类认知的角度上来说,是一种 is-a 关系。我们通过继承来关联两个类,反应真实世界中的这种关系,非常符合人类的认知,而且,从设计的角度来说,也有一种结构美感。

度使用继承,继承层次过深过复杂,就会导致代码可读性、可维护性变差。我们应当 “多用组合少用继承”

多态 Polymorphism

子类可以替换父类,在实际的代码运行过程中,调用子类的方法实现

这里就没有改写课程中的代码,换了一个更浅显的例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Animal {
bark(): void {
console.log("animal bark");
}
}

class Dog extends Animal {
bark(): void {
console.log("dog wang wang");
}
}

class Cat extends Animal {
bark(): void {
console.log("cat miao miao");
}
}

function PlayWith(ani: Animal): void {
// do something
ani.bark();
}

let dog = new Dog();

let cat = new Cat();

PlayWith(dog);
PlayWith(cat);

多态需要编程语言拥有一下特殊语法机制

  • 编程语言要支持父类对象可以引用子类对象
  • 第二个语法机制是编程语言要支持继承
  • 第三个语法机制是编程语言要支持子类可以重写(override)父类中的方法

多态除了利用 继承加方法重写的方式来实现外,还有两种比较常见的实现方式,一个是利用接口类语法,另一个是 duck-typing 语法。

接口类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
interface CanBark {
bark(): void;
}

class Tiger implements CanBark {
bark(): void {
console.log("老虎吼");
}
}

class Lion implements CanBark {
bark(): void {
console.log("狮子吼");
}
}

function LookAt(ani: CanBark) {
ani.bark();
}

let tiger = new Tiger();

let lion = new Lion();

LookAt(tiger);

LookAt(lion);
duck-typing
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
interface CanBark {
bark(): void;
}

const tiger = {
bark(): void {
console.log("老虎吼");
},
};

const lion = {
bark(): void {
console.log("狮子吼");
},
};

function LookAt(ani: CanBark) {
ani.bark();
}

LookAt(tiger);

LookAt(lion);

多态能提高代码的可扩展性和复用性,也是很多设计模式、设计原则、编程技巧的代码实现基础,比如策略模式、基于接口而非实现编程、依赖倒置原则、里式替换原则、利用多态去掉冗长的 if-else 语句等等。

抽象类与接口

抽象类是对成员变量和方法的抽象,是一种 is-a 关系,是为了解决代码复用问题。接口仅仅是对方法的抽象,是一种 has-a 关系,表示具有某一组行为特性,是为了解决解耦问题,隔离接口和具体的实现,提高代码的扩展性。

基于接口而非基于实现编程

越抽象、越顶层、越脱离具体某一实现的设计,越能提高代码的灵活性,越能应对未来的需求变化。好的代码设计,不仅能应对当下的需求,而且在将来需求发生变化的时候,仍然能够在不破坏原有代码设计的情况下灵活应对。

假设我们的系统中有很多涉及图片处理和存储的业务逻辑。图片经过处理之后被上传到阿里云上。为了代码复用,我们封装了图片存储相关的代码逻辑,提供了一个统一的 AliyunImageStore 类,供整个系统来使用。

整个上传流程包含三个步骤:创建 bucket(你可以简单理解为存储目录)、生成 access token 访问凭证、携带 access token 上传图片到指定的 bucket 中。代码实现非常简单,类中的几个方法定义得都很干净,用起来也很清晰,乍看起来没有太大问题,完全能满足我们将图片存储在阿里云的业务需求。

而有一天,我们要将图片改为上传到私有云,首先,AliyunImageStore 类中有些函数命名暴露了实现细节,比如,uploadToAliyun() 和 downloadFromAliyun()。
其次,将图片存储到阿里云的流程,跟存储到私有云的流程,可能并不是完全一致的。比如,阿里云的图片上传和下载的过程中,需要生产 access token,而私有云不需要 access token。

而要解决这些问题,编写代码的时候,要遵从 “基于接口而非实现编程”的原则

  • 函数的命名不能暴露任何实现细节。比如,前面提到的 uploadToAliyun() 就不符合要求,应该改为去掉 aliyun 这样的字眼,改为更加抽象的命名方式,比如:upload()。
  • 封装具体的实现细节。比如,跟阿里云相关的特殊上传(或下载)流程不应该暴露给调用者。我们对上传(或下载)流程进行封装,对外提供一个包裹所有上传(或下载)细节的方法,给调用者使用。
  • 为实现类定义抽象的接口。具体的实现类都依赖统一的接口定义,遵从一致的上传功能协议。使用者依赖接口,而不是具体的实现类来编程。

在定义接口的时候,不要暴露任何实现细节。接口的定义只表明做什么,而不是怎么做。而且,在设计接口的时候,我们要多思考一下,这样的接口设计是否足够通用,是否能够做到在替换具体的接口实现的时候,不需要任何接口定义的改动。

课后习题

在项目中很多地方,我们都是通过下面第 8 行的方式来使用接口的。这就会产生一个问题,那就是,如果我们要替换图片存储方式,还是需要修改很多类似第 8 行那样的代码。

1
2
3
4
5
6
public class ImageProcessingJob {
private static final String BUCKET_NAME = "ai_images_bucket";
public void process() { Image image = ...;
ImageStore imageStore = new PrivateImageStore(/*省略构造函数*/);
imagestore.upload(image, BUCKET_NAME);
}

看了评论区的答案,一种比较好的解决方式是使用工厂方法+配置文件

1
ImageStore imageStore = ImageStoreFactory.newInstance(SOTRE_TYPE_CONFIG);

多用组合 少用继承

继承最大的问题就在于:继承层次过深、继承关系过于复杂会影响到代码的可读性和可维护性。我们可以利用组合(composition)、接口、委托(delegation)三个技术手段,一块儿来解决继承存在的问题。

接口只声明方法,不定义实现。也就是说,每个会下蛋的鸟都要实现一遍 layEgg() 方法,并且实现逻辑是一样的,这就会导致代码重复的问题。那这个问题又该如何解决呢?

我们可以针对三个接口再定义三个实现类,它们分别是:实现了 fly() 方法的 FlyAbility 类、实现了 tweet() 方法的 TweetAbility 类、实现了 layEgg() 方法的 EggLayAbility 类。然后,通过组合和委托技术来消除代码重复。具体的代码实现如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
interface Flyable {
fly(): void;
}

class FlyAbility implements Flyable {
fly() {
console.log("fly");
}
}
interface Tweetable {
tweet(): void;
}

class TweetAbility implements Tweetable {
tweet() {
console.log("tweet");
}
}
interface Egglayable {
egglay(): void;
}

class EgglayAbility implements Egglayable {
egglay() {
console.log("egglay");
}
}

class Ostrich implements TweetAbility, EgglayAbility {
private tweetAbility = new TweetAbility();
private egglayAbility = new EgglayAbility();
public tweet(): void {
this.tweetAbility.tweet();
}

public egglay(): void {
this.egglayAbility.egglay();
}
}