【TS】358- 浅析 TypeScript 设计模式
作者:DD菜
https://zhuanlan.zhihu.com/p/43283016
设计模式就是软件开发过程中形成的套路,就如同你在玩lol中的“正方形打野”,“四一分推”,又或者篮球运动中的“二夹一”,“高位单打”一样,属于经验的总结。
熟悉设计模式有什么好处呢?
让你在编程过程中更有自信,使用经过无数前人印证过的最好的设计,自然底气十足
提升编程效率,避免开发过程中的犹豫
更能掌控项目,方便预估开发时间,方便对团队成员进行管理
由于设计模式和软件开发的语言,平台都没有关系,因此,前端工程师对设计模式也是有需求的。
设计模式是对人类工程历史总结,而不单单只是软件工程。
现在大家谈的前端工程化,如果脱离设计模式,只能算徒有其表,设计模式才是工程化的灵魂。当然,既然是经验和历史总结,有时候并不需要系统地进行学习,口口相传也是可以的,但是单独系统地讲解设计模式,就是要将“公共知识”转变为“共有知识”,戳破皇帝的新衣,让大家真正能言之有物,交流通畅。
类型分类
可以将设计模式分为三种类型,分别为创建型,结构型,和行为型。
创建型模式主要解决对象创建什么,由谁创建,何时创建的3w问题,对类的实例化进行了抽象,分离概念和实现,使得系统更加符合单一职责原则。
结构型模式描述如何将类或者对象组合在一起,形成更大的数据结构,因此也可以分为类结构型和对象结构型。
行为型模型对不同的对象划分责任和算法的抽象,关注类和对象之间的相互作用,同样也分为类和对象。
可以看到三种类型的模式正好解决了编程中的数据结构从哪里来?如何组合?如何交流?的问题。
创建型模式
创建型模式一共有4个,分别为工厂(工厂,工厂方法,抽象工厂合并),建造者,原型,单例。
工厂模式
重要程度:⭐⭐⭐⭐⭐ 难度:⭐ 命名建议:xxxFactory,FactoryOfxxx
工厂模式简而言之,就是要替代掉“new操作符”!
为什么需要替代new操作符?
因为有时候创建实例时需要大量的准备工作,而将这些准备工作全部放在构造函数中是非常危险的行为,有必要将创建实例的逻辑和使用实例的逻辑分开,方便以后扩展。
举个例子:
class People {
constructor(des) {
// 出现异步不能使用async await
// 函数调用时可能还未完成初始化
get('someUrl').then(data => {
this.name = data.name
get('someUrl?name=' + this.name).then(data => {
this.age = data.age
})
})
// 非成员函数耦合性变大
this.des = handleDes(des)
}
}
而使用Typescript,配合工厂模式,实现如下:
// 还真别说,形式上好看的代码,质量一般都比较高
class People {
name: string = ''
age: number = 0
des: string = ''
constructor(name: string, age: number, des: string) {
this.name = name
this.age = age
this.des = des
}
}
async function peopleFactory(description:any){
const name = await get('someUrl')
const age = await get('someUrl?name='+name)
const des = handle(description)
return new People(name,age,des)
}
这样的封装,能清楚地分离对象的创建和使用。同时,如果之后的类的定义发生了改变,可以直接修改People,创建类的准备数据发生了改变,则修改工厂函数。
但是,选择工厂模式的原因是因为构造函数足够复杂或者对象的创建面临巨大的不确定性,只需要传入变量即可构造的情况下,用工厂函数实际上是得不偿失的。
几乎所有的设计模式都会带来代码可读性下降的风险,因此需要找到代码可读性降低和可维护性,稳定性之间的平衡!
你也可以用函数根据参数返回相应的工厂函数,又或者用一个类集中管理工厂函数来处理复杂度。
建造者模式
重要程度:⭐⭐⭐⭐ 难度:⭐⭐ 命名建议:xxxBuilder
建造者模式用于直接构建复杂对象,比如上例中的构造函数参数,如果采用一个结构表示:
constructor(peopleConfig:any) {
this.name = peopleConfig.name
this.age = peopleConfig.age
this.des = peopleConfig.des
}
那么有必要将这个人对象的构建单独封装起来:
class PeopleConfigBuilder{
name: string = ''
age: number = 0
des: string = ''
async buildName(){
this.name = await get('someUrl')
}
async buildAge(){
await get('someUrl?name='+this.name)
}
async buildDes(description: any){
this.des = handleDes(description)
}
}
class People {
name: string = ''
age: number = 0
des: string = ''
constructor(peopleConfig: PeopleCofigBuilder) {
this.name = peopleConfig.name
this.age = peopleConfig.age
this.des = peopleConfig.des
}
}
async function peopleFactory(description:any){
const builder = new PeopleConfigBuilder()
builder.buildName()
builder.buildAge()
builder.buildDes()
return new People(builder)
}
当然,仅仅三个属性的对象,远远没有达到复杂对象的程度,因此,只有在对象十分复杂的时候,才需要应用到建造者模式。
原型模式
重要程度:⭐⭐ 难度:⭐ 命名建议:xxxPrototype
创建新对象时是基于一个对象的拷贝,而不是重新实例化一个类。
举例说明,比如上例中的peopleConfig,其实peopleConfig应该是有固定模板的:
function peopleConfigPrototype (){
return {
name: '',
age: 0,
des: ''
}
}
这样每次返回的都是新的对象,也可以相当于是对象的拷贝,但是如果直接拷贝对象,应该怎么写呢?
const peopleConfigPrototype = {
name: '',
age: 0,
des: ''
}
const peopleConfig = Object.create(peopleConfigPrototype)
// 采用Object.create方法,当前对象将被复制到peopleConfig的__proto__上
还有另一种方式进行对象拷贝,但是会丢掉对象中的函数:
const peopleConfig = JSON.parse(JSON.stringfy(peopleConfigProtytype))
注意JSON操作会阻塞线程,导致性能急剧下降,一般不考虑这种方式。
单例模式
重要程度:⭐⭐⭐⭐⭐ 难度:⭐⭐ 命名建议:xxxSingle,xxxSingleton,xxxUnum
单例模式的目的是限制一个类只能被实例化一次,防止多次实例化。其中,根据类被实例化的时间,又被分为懒汉单例和饿汉单例。懒汉单例是指在第一次调用实例的时候实例化,饿汉单例是指在类加载的时候就实例化。
/* 懒汉单例 */
class PeopleSingle{
// 静态成员instance
static instance = null
// 私有构造函数
private constructor(){ }
public static getInstance(){
if(PeopleSingle.instance === null){
PeopleSingle.instance = new PeopleSingle()
}
return PeopleSingle.instance
}
}
PeopleSingle.getInstance()
/* 饿汉单例 */
class PeopleSingle{
static instance = new PeopleSingle()
private constructor(){ }
}
PeopleSingle.instance
四种创建模式都有其使用场景,需要针对使用场景进行组合,才能写出高质量的代码。
结构型模式
结构型模式一共有7种:适配器,桥接,组合,装饰,外观,享元,代理
适配器模式
重要程度:⭐⭐⭐⭐ 难度:⭐⭐⭐ 命名建议:xxxAdapter,xxxWraper
想想你的转接头,实际上就是被适配对象(adaptee)上套上一层封装,将其接口与目标对象(target)相匹配,所以适配器又叫wraper(包皮)。
比如,有一个目标类UsbC:
class UsbC{
slowCharge(){
console.log('slow charging')
}
superCharge(){
console.log('super charging')
}
}
有一个被适配目标MicroUsb:
class MicroUsb{
slowCharge(){
console.log('slow charging')
}
}
所以adapter是如此:
// 精髓在implements target上
class MicroToCAdapter implements UsbC{
microUsb: MicroUsb
constructor(microUsb: MicroUsb){
this.microUsb = microUsb
}
slowCharge(){
this.microUsb.slowCharge()
}
superCharge(){
console.log('cannot super charge, slow charging')
}
}
// 这样就可以直接
new MicroTOCAdapter(new MicroUsb()).superCharge()
适配器模式对多个不同接口的匹配非常有效,实际情况中没有必要完全使用类来封装,一个函数也可以搞定。
桥接模式
重要程度:⭐⭐⭐⭐ 难度:⭐⭐⭐ 命名建议:xxxBridge,xxx(具体实现)
桥接模式的主要目的,是将抽象与实现解耦,使得二者可以独立地进行变化,以应对不断更细的需求。
其实通俗地来说,就是将所有概念想象成“灵魂——肉体”,凡是能用这个概念代入的,都可以用桥接模式重构。
比如汽车这个概念和颜色这个概念,可以将颜色作为汽车的成员变量,但是当颜色变得更加复杂时,比如渐变,模糊,图案等属性加入,不得不将其解耦,桥接模式就很重要了。
我们先定义抽象类Car和Color(Ts的抽象类功能对于实现之一模式非常重要):
abstract class Color {
color: string
abstract draw(): void
}
abstract class Car {
color: Color
abstract setColor(color: Color): void
}
再定义其实例:
class Red extends Color {
constructor() {
super()
}
draw() {
this.color = 'red'
}
}
class Van extends Car {
constructor() {
super()
}
setColor(color: Color) {
this.color = color
}
}
抽象类和实现是解耦的,这时候我们如果要利用所有的类,就需要一个桥接类:
class PaintingVanBridge {
van: Car
red: Color
constructor() {
this.red = new Red()
this.red.draw()
this.van = new Van()
this.van.setColor(this.red)
}
}
桥接模式会增加大量代码,所以一定要在使用之前对功能模块有一个恰当的评估!
装饰模式
重要程度:⭐⭐⭐⭐⭐ 难度:⭐ 命名建议:xxxDecorator,xxx(具体实现)
装饰模式是在现有类或对象的基础上,添加一些功能,使得类和对象具有新的表现。
还是之前的Car和Color的问题,可以直接继承Car,添加颜色,这是一个装饰模式:
class Car {
name: string
constructor(name: string) {
this.name = name
}
}
class Benz extends Car {
color: string
constructor(name: string, color: string) {
super(name)
this.color = color
}
}
但是采用继承的方式是静态的,而且会导致在继承复用的过程中耦合,比如Car2继承Car,在创建新的子类时错把Car2作为父类,结果就很容易出错了。
为了解决这个问题,可以采用Ts的装饰器特性:
function colorDecorator<T extends { new(...args: any[]): {} }>(color: string) {
return function (constructor: T) {
return class extends constructor {
name = 'shit'
color = color
}
}
}
@colorDecorator<Car>('red')
class Car {
name: string
constructor(name: string) {
this.name = name
}
}
装饰器会拦截Car的构造函数,生成一个继承自Car的新的类,这样更加灵活(但是注意这个过程只发生在构造函数阶段)。
外观模式
重要程度:⭐⭐⭐⭐⭐ 难度:⭐ 命名建议:xxx(具体实现)
简单一句话总结:“封装复杂,接口简单”,为所有的子系统提供一致的接口,比如轮胎,方向盘和车。
class Tyre{
name: string
constructor(name: string){
this.name = name
}
}
class Steering{
turnRight(){}
turnLeft(){}
}
interface CarConfig{
tyreName: string
ifTurnRight: boolean
}
class Car{
tyre:Tyre
steering:Steering
constructor(carConfig: CarConfig){
this.tyre = new Tyre(carConfig.name)
this.steering = new Steering()
if(carConfig.ifTurnRight){
this.steering.turnRight
}
}
}
可以活用Typescript的接口功能实现这一模式。
享元模式
重要程度:⭐ 难度:⭐⭐ 命名建议:xxx(具体实现)
享元模式避免重新创建对象,其实只要有缓存对象的意思,并且共用一个对象实例,就是享元模式。
比如需要对一个Car的实例进行展示(可以搭配工厂模式):
class Car{
name: string
color: string
changeColor(color: string){
this.color = color
}
changeName(name: string){
this.name = name
}
}
class CarFactory{
static car: Car
static getCar():Car{
if(CarFactory.car === null){
CarFactory.car = new Car()
}
return CarFactory.car
}
}
CarFactory.getCar().changeColor('red')
注意,由于是使用的同一个引用,因此会存在修改的问题。
代理模式
重要程度:⭐⭐⭐⭐ 难度:⭐ 命名建议:xxxProxy
对接口进行一定程度的隐藏,用于封装复杂类。
比如Car有很多属性,我们只需要一个简单的版本:
class Car{
a: number = 1
b: number = 2
c: number = 3
d: number = 4
name: string = 'name'
test(){
console.log('this is test')
}
}
class CarProxy{
private car: Car
name: number
constructor(){
if(this.car === null){
this.car = new Car
}
this.name = this.car.name
}
test(){
this.car.test()
}
}
行为型模式
行为型模式一共有5种:命令,中介者,观察者,状态,策略
命令模式
重要程度:⭐⭐⭐⭐⭐ 难度:⭐⭐ 命名建议:xxxCommand
命令模式的主要目的是让请求者和响应者解耦,并集中管理。
比如大家常用的请求,其实可以这样封装:
function requestCommand(command: string){
let method = 'get'
let queryString = ''
let data = null
let url = ''
const commandArr = command.split(' ')
url = commandArr.find(el=>el.indexOf('http'))
const methods = commandArr.filter(el=>el[0]==='-')
methods[0].replace('-','')
method = methods[0]
const query = commandArr.filter(el=>el.indexOf('='))
if(query.length > 0){
queryString = '?'
query.forEach(el=>{
queryString += el + '&'
})
}
const dataQuery = commandArr.filter(el=>el[0]==='{')
// 对json的判断还不够细致
data = JSON.parse(dataQuery)
if(method === 'get' || method === 'delete'){
return axios[method](url+query)
}
return axios[method](url+query,data)
}
requestCommand('--get https://www.baidu.com name=1 test=2')
requestCommand('--post https://www.baidu.com {"name"=1,"test":2}')
注意命令模式需要提供详尽的文档,并且尽可能集中管理。
中介模式
重要程度:⭐⭐⭐⭐⭐ 难度:⭐⭐⭐⭐ 命名建议:xxxCotroller,xxxMiddleWare,xxx(具体实现)
全权负责两个模块之间的通讯,比如MVC,MVVM就是非常典型的中介模式。
中介模式,桥接模式,代理模式的区别是:
代理模式一对一,只能代理特定类和对象,是对其的扩展或是约束。
桥接模式一对多,是对类或对象成员或属性的扩展。
中介模式多对多,全权承包所有两个概念间的关系。
比如4s店,车,和买家之间的关系:
class Car{
name: string = 'Benz'
}
class Buyer{
name: string = 'Sam'
buy(car: Car){
console.log(`${this.name}购买了${car.name}`)
}
}
class FourSShop{
constructor(){
const benz = new Car()
const sam = new Buyer()
sam.buy(benz)
}
}
可以想象中介模式是一个立体的概念,可以理解成是两个概念发生关系的地点。
观察者模式
重要程度:⭐⭐⭐⭐⭐ 难度:⭐⭐⭐ 命名建议:xxxObserver,xxxEventHandler
观察者模式的目的是为了“检测变更”,既然要检测变更,自然需要记录之前的信息:
class Observer{
states: string[] = []
update(state: string){
this.states.push(state)
}
}
class People{
state: string = ''
observer: Observer
// 可以用getter setter优化
setState(newState: string){
if(this.state !== newState){
this.state = newState
this.notify(this.state)
}
}
notify(state: string){
if(this.observer !== null){
this.observer.update(state)
}
}
setObserver(observer: Observer){
this.observer = observer
}
}
const observer = new Observer()
const people = new People().serObserver(observer)
people.setState('shit')
console.log(observer.state)
可以把观察者模式看成是“记录事件”,这对于理解观察者模式和状态模式区别很有帮助。
实际上前端很多事件处理,就是基于观察者模式的,在上例中的update中的state,就是事件名称,js的事件循环会轮流处理states的状态变化。
状态模式
重要程度:⭐⭐⭐⭐⭐ 难度:⭐⭐⭐ 命名建议:xxxState
与观察者模式相对,表示的是“记录状态”,只要状态变更,表现即不同,这是设计数据驱动的基础。
class State{
tmp: string
set store(state: string){
if(this.tmp !== state){
// do something
this.tmp = state
}
}
get store(): string{
return this.tmp
}
}
class People{
state: State
constructor(state: State){
this.state = state
}
}
const state = new State()
const people = new People(state)
state.store = 1
console.log(people.state.store)
当然,如果一个数据接口既能记录事件,又能记录状态,可以么?
这就是传说中的响应式数据流,也就是大家平时使用的ReactiveX。
策略模式
重要程度:⭐⭐⭐ 难度:⭐⭐⭐⭐ 命名建议:xxxStratege
策略模式表示动态地修改行为,而行为有时候是一系列方法和对象的组合,与命令模式的区别也在这里。
比如从中国到罗马,可以如此封装:
class Location{
position: string
constructor(poosition: string){
this.position = position
}
}
class Stratege{
locations: Location [] = []
constructor(...locations){
this.locations = locations
console.log('路线经过了')
this.locations.forEach(el=>{
console.log(el.position+',')
})
}
}
class Move{
start: Location
end: Location
stratege: Stratege
constructor(){
this.start = new Location('1 1')
this.end = new Location('0 0')
const sea = new Location('0 1')
const land = new Location('1 0')
this.stratege = new Stratege(this.start,sea,this.end)
}
}
设计模式根植于面向对象思想,也就是任何实现都要区分概念(类)和实例(对象),也就是要分清楚白马和马,这样才能竟可能减轻扩展和团队协作的负担。
但是任何东西有利就有弊,扬长避短才是我们应该在意的方向。
回复“加群”与大佬们一起交流学习~
【TS】358- 浅析 TypeScript 设计模式的更多相关文章
- 浅析JAVA设计模式之工厂模式(一)
1 工厂模式简单介绍 工厂模式的定义:简单地说,用来实例化对象,取代new操作. 工厂模式专门负责将大量有共同接口的类实例化.工作模式能够动态决定将哪一个类实例化.不用先知道每次要实例化哪一个类. 工 ...
- 浅析JAVA设计模式之工厂模式(二)
1 工厂方法模式简单介绍 工厂方法 (Factroy Method)模式:又称多态性工厂模式(Polymorphic Factory),在这样的模式中,核心工厂不再是一个详细的类.而是一个抽象工厂,提 ...
- TypeScript设计模式之单例、建造者、原型
看看用TypeScript怎样实现常见的设计模式,顺便复习一下. 单例模式 Singleton 特点:在程序的生命周期内只有一个全局的实例,并且不能再new出新的实例. 用处:在一些只需要一个对象存在 ...
- TypeScript设计模式之工厂
看看用TypeScript怎样实现常见的设计模式,顺便复习一下. 学模式最重要的不是记UML,而是知道什么模式可以解决什么样的问题,在做项目时碰到问题可以想到用哪个模式可以解决,UML忘了可以查,思想 ...
- TypeScript设计模式之策略、模板方法
看看用TypeScript怎样实现常见的设计模式,顺便复习一下. 学模式最重要的不是记UML,而是知道什么模式可以解决什么样的问题,在做项目时碰到问题可以想到用哪个模式可以解决,UML忘了可以查,思想 ...
- TypeScript设计模式之门面、适配器
看看用TypeScript怎样实现常见的设计模式,顺便复习一下. 学模式最重要的不是记UML,而是知道什么模式可以解决什么样的问题,在做项目时碰到问题可以想到用哪个模式可以解决,UML忘了可以查,思想 ...
- TypeScript设计模式之备忘录、命令
看看用TypeScript怎样实现常见的设计模式,顺便复习一下. 学模式最重要的不是记UML,而是知道什么模式可以解决什么样的问题,在做项目时碰到问题可以想到用哪个模式可以解决,UML忘了可以查,思想 ...
- TypeScript设计模式之职责链、状态
看看用TypeScript怎样实现常见的设计模式,顺便复习一下. 学模式最重要的不是记UML,而是知道什么模式可以解决什么样的问题,在做项目时碰到问题可以想到用哪个模式可以解决,UML忘了可以查,思想 ...
- TypeScript设计模式之中介者、观察者
看看用TypeScript怎样实现常见的设计模式,顺便复习一下. 学模式最重要的不是记UML,而是知道什么模式可以解决什么样的问题,在做项目时碰到问题可以想到用哪个模式可以解决,UML忘了可以查,思想 ...
随机推荐
- [ISE使用] 使用ISE的过程中,遇到过的一些“软件上的问题”
1.planahead打不开了. PlanAhead替代文件rdiArgs.bat的下载链接如下: http://www.eevblog.com/forum/microcontrollers/guid ...
- [java] 计算时间复杂度
一.精算: 1.所有的声明,都不计时间: 2.赋值语句占1个时间单位(下称:单位),比如sum=0: 3.return占1个单位,比如,reeturn sum. 我怀疑范围一个链表头,不止占1 ...
- 用这个库 3 分钟实现让你满意的表格功能:Bootstrap-Table
本文作者:HelloGitHub-kalifun 这是 HelloGitHub 推出的<讲解开源项目>系列,今天给大家推荐一个基于 Bootstrap 和 jQuery 的表格插件:Boo ...
- 初识JSP:JSP的注释、脚本、声明、表达式
1.JSP的注释 在HTML当中,如果使用传统的注释我们可以在客户端,也就是网页上右键查看源代码里面看得到该注释,但是JSP注释无法在客户端里看到.源码里面会只会看到JSP注释的地方空出来. 使用方法 ...
- [spark程序]统计人口平均年龄(HDFS文件)(详细过程)
一.题目描述 (1)请编写Spark应用程序,该程序可以在分布式文件系统HDFS中生成一个数据文件peopleage.txt,数据文件包含若干行(比如1000行,或者100万行等等)记录,每行记录只包 ...
- Unix, Linux以及NT内核和它们各自衍生的系统关系图
- js数组之sort()函数
一般我们使用sort函数进行数组的排序,sort()方法有一个可选参数,是用来确定元素顺序的函数.如果这个参数被省略,那么数组中的元素将按照ASCII字符顺序进行排序.如: var arr = [&q ...
- js 关于apply和call的理解使用
关于call和apply,以前也思考良久,很多时候都以为记住了,但是,我太难了.今天我特地写下笔记,希望可以完全掌握这个东西,也希望可以帮助到任何想对学习这个东西的同学. 一.apply函数定义与理解 ...
- Spring Security之多次登录失败后账户锁定功能的实现
在上一次写的文章中,为大家说到了如何动态的从数据库加载用户.角色.权限信息,从而实现登录验证及授权.在实际的开发过程中,我们通常会有这样的一个需求:当用户多次登录失败的时候,我们应该将账户锁定,等待一 ...
- wake on lan定时开机部署
在Linux下通过Wake On LAN实现网络唤醒远程开机 我们经常有这样的场景或需求,人在外面,需要将家里的机器或公司的机器开启,进行远程控制操作. 有几种方式可以实现远程开机,一是通过主板的来电 ...