理解并掌握好设计模式

笔者一直秉持一个观点:“大多数软件开发者所遇到的问题都是其它开发者已遇到过的”。举一个很简单的例子,当你想要解决某个问题时,你可以在 github issues、stackoverflow、某个开源仓库等等地方找到你想要的答案,或者是相类似的答案,或者是能给予你灵感的解决思路。

设计模式作为已有编程经验的总结,理解并运用好它一定可以让你在短时间内收获大量的编程知识和掌握丰富的编程技巧。当然,随着对设计模式越来越深入的学习,你会发现很多设计模式和设计原则已经融入了编程语言本身,你对多种设计模式之间组合和改良的需求越来越高,直到最后“无招胜有招”。

从面向对象开始

很多 TypeScript 开发者是从 JavaScript 过渡而来的,但是在 JS 这门语言里,面向对象的特性表现的并不突出,在 ES6 之前,连 class 都是用 function 模拟出来的,也难怪很多 JS 开发者在网络上搜索面向对象时会对多态、继承、封装等概念感觉既熟悉又陌生。

不过不用担心,在我们正式开始探索 TypeScript 设计模式之前,先来聊一聊面向对象吧~

1.类与对象

很多初学者对于类和对象的概念比较模糊,特别是仅熟悉面向过程的语言时,那我们就尝试将一段面向过程的代码改为面向对象。

现在有这样一个功能,当用户点击按钮时,提示用户他点击了按钮。

面向过程的代码看起来大概是这样:



btn.click = function() {
  alert("您点击了该按钮")
}



虽然这段代码不能说有问题,但是在真实业务场景下,显得过于贫瘠,我们尝试将其改为面向对象的方式:



class Notification {
  info(msg) {
    alert(msg);
  }
}

btn.click = function() {
  // 这段代码就是实例化类,并使用 new 关键字创建
  const notification = new Notification();  
  notification.info("您点击了该按钮")
}



  1. 引入了 Notification 类来帮助我们创建消息提示,而不是直接提示
  2. 普通的消息提示使用 info() 方法

当然,有经验的开发者一定会提出更好的建议,如果恰巧又是 Angular 的开发者,还会立刻想到使用 service 的方式,不过这里不做延伸。

针对于类和对象概念十分模糊的初级开发者,这里简单描述下:



// 这段代码里,new 右边的 Notification 是类名,等于号左边的 notification 是对象
// 对象名不需要限制,使用小写的形式是为了标识他是 Notification 类创建的对象
const notification = new Notification();



我们再来看看他们的定义:

类:类就是具有相同的属性和功能的对象的抽象的集合
对象:对象是一个自包含的实体,用一组可识别的特性和行为来标识




typescript 重命名as_typescript 重命名as


2.构造函数

在 TypeScript 的类里面有个很特殊的方法 constructor,当使用 new 创建对象时,constructor 就会被调用


class Test {
  constructor() {
    console.log("测试");
  }
}

const t = new Test();


如果不写 constructor,那么默认就会创建一个 。

构造函数还可以传入参数初始化类属性


class Test {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

const t = new Test("名称");
t.name; // 名称


3. TypeScript 的方法重载

在传统的面向对象语言中,方法重载可以让同名函数拥有不同的功能。

但是在 JavaScript 中,同名函数会被覆盖。例如:


function foo() {
  console.log("foo_1");
}

function foo(a) {
  console.log("foo_2");
}

foo();  // foo_2


TypeScript 中的函数重载并不会真正的合并函数,而只是函数声明中的重载,函数体中的实际逻辑还是需要自己去写。例如:


// 声明重载
function add(a: number, b: number): number;
function add(a: string, b: string): string;

function add(a: number | string, b: number | string ) {
   // 具体的实现逻辑...
}


4.属性及方法修饰符

在面向对象类语言中,修饰符是非常重要的一部分,这定义了类中成员的访问权限和代码边界,而 JavaScript 并没有显式的声明,更多的是则是模拟。最近的私有变量草案中则是通过 # 来标识私有变量。

在使用 TypeScript 的情况下,明晓这些修饰符的作用是十分重要的,即使一时半会你不知道该使用哪个修饰符。

public

公共的,在 TypeScript 中成员默认都是 public 的,这意味着你可以在类之外自由的访问这些 public 修饰的成员。

private

私有的。只有在类的内部才可以访问。

protected

受保护的。在 private 的基础上,类的子类也可以访问。

getter setter

getter 和 setter 我们一般称之为存取器,不使用存取器的类一般是这样:


class Book {
  name = 'TypeScript';
}


而使用存取器的类,则会是这样:


class Book {
  private _name = 'JavaScript';
  get name(): string {
    return this._name;
  }

  set name(value: string): string {
    this._name = value;
  }
}


5.封装

在开篇的例子中,我们将提示信息提炼为 Notification 类,如果我们想要用户点击时打印出警告信息,那么可以轻松更改代码:


class Notification {
  info(msg: string) {
    console.warn(msg); 
  }
}


如果还需要增加一个弹出消息的功能,那么同样可以封装出一个 Toast 类:


class Toast {
  info(msg: string) {
    let msgNode = document.createElement('span');
    let textNode = document.createTextNode(`提示:${msg}`);
    msgNode.appendChild(textNode);
    document.getElementById('container').appendChild(msgNode);
  }
}


我们仔细对比一下上面的两个类,除了 info() 方法中的方法体不太一样以外,其它的其实都很相似,那这些相似的代码我们如何规范起来?或者说如何复用这些代码?来看看面向对象的第二大特性”继承“。

6.继承

无论是 Toast 还是 Notification 都是一种消息类型,我们可以认为他们都是 Message,这种 is-a 的关系就可以看作是继承。

例如消息类通常还会有提示标题之类的字段,我们添加一下 title 字段:


class Message {
  protected title: string;
	
  info(msg: string) {
		
  }
}


使用 Notification 继承 Message 时,也就拥有了 title 字段和 info() 方法


class Notification extends Message {}


在熟悉继承中,还是要记住这三点:

  1. 子类拥有父类非 private 的所有属性和方法
  2. 子类具有自己的属性和功能
  3. 子类可以以自己的方式实现父类的功能

7.多态

在继承中,我们说到 Message 的 info() 方法会被 Notification 类和 Toast 类继承,然而 Notification 和 Toast 中针对于消息的提示显然不一样的,也就是说子类会通过自己的实现代码来执行。

如果在 C# 中重写父类方法需要 override 关键词,而在 TypeScript 中则直接改写相同方法中的方法体即可


class Notification extends Message {
  info(msg: string) {
    console.log(msg);
  }
}


8.抽象类和接口

其实我们发现 Message 根本不需要被实例化,他只会作为父类来被其它子类继承,那么这时候我们就可以认为它可以是一个抽象类


// 定义为抽象类
abstract class Message {
  // 定义为抽象方法
  abstract info(msg: string);
}

class Notification extends Message {
  // 子类必须实现抽取方法,否则会抛出异常
  info(msg: string) {
    console.log(msg); 
  }
}


抽象类有三点要求:

  1. 抽象类不能被实例化
  2. 抽象方法必须要被子类重写
  3. 如果类中含有抽象方法,那么该类必须要被声明为抽象类

通常与抽象类相比较的则是接口,相对于抽象类,接口则完全不包含成员的实现,或者从某一方面来说:

  • 抽象类是对类的抽象
  • 接口是对行为的抽象