Js设计模式篇一:创建型设计模式

268 阅读4分钟

什么是设计模式

日常开发中,一些特定的场景下你的处理方法可能并不是很理想,一个模式就是一个可重用的方案,可应用于在软件设计中的常见问题。往往这时借助一些设计模式可以让你优雅而高效的实现这些逻辑, 模式的另一种解释就是一个我们如何解决问题的模板,那些可以在许多不同的情况里使用的模板。

设计模式的好处

  • 模式是有效的解决方法:他们提供固定的解决方法来解决在软件开发中出现的问题,这些都是久经考验的反应了开发者的经验和见解的使用模式来定义的技术。
  • 模式可以很容易地重用:一个模式通常反映了一个可以适应自己需要的开箱即用的解决方案。
  • 模式善于表达:当我们看到一个提供某种解决方案的模式时,一般有一组结构和词汇可以非常优雅地帮助表达相当大的解决方案。

设计模式原则 SOLID

  • S – Single Responsibility Principle 单一职责原则

    • 一个程序只做好一件事
    • 如果功能过于复杂就拆分开,每个部分保持独立
  • O – OpenClosed Principle 开放/封闭原则

    • 对扩展开放,对修改封闭
    • 增加需求时,扩展新代码,而非修改已有代码
  • L – Liskov Substitution Principle 里氏替换原则

    • 子类能覆盖父类
    • 父类能出现的地方子类就能出现
  • I – Interface Segregation Principle 接口隔离原则

    • 保持接口的单一独立
    • 类似单一职责原则,这里更关注接口
  • D – Dependency Inversion Principle 依赖倒转原则

    • 面向接口编程,依赖于抽象而不依赖于具体
    • 使用方只关注接口而不关注具体类的实现

设计模式建模

UML(Unified Modeling Language)是一种统一建模语言,为面向对象开发系统的产品进行说明、可视化、和编制文档的一种标准语言。

类图分三层,第一层显示类的名称,如果是抽象类,则就用斜体显示。第二层是类的特性,通常就是字段和属性。第三层是类的操作,通常是方法或行为。前面的符号,+ 表示public,- 表示private,# 表示protected(js中为严格区分,ts中有)

  • 场景例子

平时打车为例:

1、车分为快车和专车,它们都有车牌号和名字; 2、快车每公里2元,专车每公里4元; 3、添加一个为5公里路程的行程;

  • 分析

每个车都有车牌号和名字,车分为快车和专车,可以将车抽象为父类,不同的车继承父类进行实例化。每个行程是由车完成的,所以要为每个行程添加一个车和路程。

  • 画出UML类图 工具是比较流行的ProcessOn的在线化编辑工具 www.processon.com/

    1、画出车的父类,有两个属性:number和name,添加属性和方法要使用+

2、画出行程类,我们知道行程是要靠车来完成的,所以行程含有:car和行程长度属性,并且含有两个行为:开始行程start()和终止行程end()。

3、画出快车子类和专车子类,车子的实例都有价格属性,每个车是不同的。

4、标明继承和引用的关系,继承关系使用空心箭头,实例传递使用实心箭头

代码实现和测试

    //车的父类
class Car {
  constructor(number, name) {
    this.name = name
    this.number = number
  }
}
//行程类
class Trip {
  constructor(car, length) {
    this.car = car
    this.length = length
  }
  start() {
    console.log(`行程开始:名称:${this.car.name},车牌号:${this.car.number}`)
  }
  end() {
    console.log(`行程结束:价格:¥ ${this.car.price * this.length}元`)
  }
}
//快车子类
class Kuaiche extends Car {
  constructor(number, name) {
    super(number, name)
    this.price = 1
  }
}
//专车子类
class Zhuanche extends Car {
  constructor(number, name) {
    super(number, name)
    this.price = 2
  }
}
//测试
let car = new Kuaiche('京A11010B', '宝马X5')
let trip = new Trip(car, 10) 
trip.start() //行程开始:名称:宝马X5,车牌号:京A11010B
trip.end() // 行程结束:价格:¥ 10元

设计模式分类

设计模式分成:创建型设计模式、结构型设计模式、行为型设计模式、技巧型设计模式、架构型设计模式。常用的是23种设计模式,会重点学习,剩下的模式稍微了解一下。

一、创建型设计模式

1.工厂模式

工厂模式定义一个用于创建对象的接口,这个接口由子类决定实例化哪一个类。

该模式使一个类的实例化延迟到了子类。而子类可以重写接口方法以便创建的时候指定自己的对象类型。 通过这个函数就可以创建我需要的对象,都不用再关注创建这些对象到底依赖于哪个基类了,而我们知道这个函数就可以了。这个函数通常也被称为工厂函数,这种模式叫简单工厂模式,它是工厂模式中最简单的一种形式。

简单工厂模式的理念就是创建对象,方式是对不同的类实例化,不过除此之外简单工厂模式还可以用来创建相似对象。没有父类,所以无需做任何继承,只需简单创建一个对象即可,然后通过对这个对象大量拓展方法和属性,并在最终将对象返回出来。

  • 适用场景

如果你不想让某个子系统与较大的那个对象之间形成强耦合,而是想运行时从许多子系统中进行挑选的话,那么工厂模式是一个理想的选择 将new操作简单封装,遇到new的时候就应该考虑是否用工厂模式; 需要依赖具体环境创建不同实例,这些实例都有相同的行为,这时候我们可以使用工厂模式,简化实现的过程,同时也可以减少每种对象所需的代码量,有利于消除对象间的耦合,提供更大的灵活性

  • 优点

创建对象的过程可能很复杂,但我们只需要关心创建结果。 构造函数和创建者分离, 符合“开闭原则” 一个调用者想创建一个对象,只要知道其名称就可以了。 扩展性高,如果想增加一个产品,只要扩展一个工厂类就可以。

  • 缺点

添加新产品时,需要编写新的具体产品类,一定程度上增加了系统的复杂度 考虑到系统的可扩展性,需要引入抽象层,在客户端代码中均使用抽象层进行定义,增加了系统的抽象性和理解难度

  • 案例
  let popUpFactory = function (type, text) {
        if(this instanceof popUpFactory){
            var s = new this[type](text)
            return s;
        }else{
            return new popUpFactory(text)
        }
    }
    popUpFactory.prototype = {
        loginForm: function(text){
            this.name = '登录框'
            this.text = text
            console.log(this.text)
        },
        alertForm: function(text){
            this.name = '提示框'
            this.text = text
            console.log(this.text)
        },
        messageForm: function(text){
            this.name = '消息框'
            this.text = text
            console.log(this.text)
        }
    }
    //调用
    let loginFm = popUpFactory('loginForm','欢迎')
    let alertFm = popUpFactory('alertForm','密码不正确')
    let messageFm = popUpFactory('messageForm','通知xxxx')
ES6重写工厂模式
class popUp{
   //构造器
   constructor(name = '', text = []) {
   if(new.target === popUp) {
     throw new Error('抽象类不能实例化!');
   }
   this.name = name;
   this.text = text;
   console.log(text)
 }
}
class Factory extends popUp {
   constructor(name, text){
       super(name, text)
   }
   create(typename){
       switch(typename){
           case 'loginForm':
               return new Factory('loginForm','欢迎');
               break;
           case 'alertForm':
               return new Factory('alertForm','密码不正确');
               break;
           case 'messageForm':
               return new Factory('messageForm','通知xxxx');
               break;
           default:
               throw new Error('参数错误')
       }
   }
}
let factoryFm = new Factory();
let loginFm = factoryFm.create('loginForm')
let alertFm = factoryFm.create('alertForm')
let messageFm = factoryFm.create('messageForm')

抽象工厂模式

在抽象工厂中,类簇一般用父类定义,并在父类中定义一些抽象方法,再通过抽象工厂让子类继承父类。所以,抽象工厂其实是实现子类继承父类的方法。

let popFormFactory = function(subType, superType) {
  //判断抽象工厂中是否有该抽象类
  if(typeof popFormFactory[superType] === 'function') {
    //缓存类
    function F() {};
    //继承父类属性和方法
    F.prototype = new popFormFactory[superType] ();
    //将子类的constructor指向子类
    subType.constructor = subType;
    //子类原型继承父类
    subType.prototype = new F();

  } else {
    throw new Error('抽象类不存在!')
  }
}

//LoginForm抽象类
popFormFactory.LoginForm = function() {
  this.type = 'loginForm';
}
popFormFactory.LoginForm.prototype = {
  getName: function() {
    return new Error('抽象方法不能调用');
  }
}

//AlertForm抽象类
popFormFactory.AlertForm = function() {
  this.type = 'alertForm';
}
popFormFactory.AlertForm.prototype = {
  getName: function() {
    return new Error('抽象方法不能调用');
  }
}

//MesssageForm抽象类
popFormFactory.MesssageForm = function() {
  this.type = 'messsageForm';
}
popFormFactory.MesssageForm.prototype = {
  getName: function() {
    return new Error('抽象方法不能调用');
  }
}
//login子类
function login(name) {
  this.name = name
}
//抽象工厂实现login类的继承
popFormFactory(login, 'LoginForm');
//子类中重写抽象方法
login.prototype.getName = function() {
  return this.name;
}

//alert子类
function Alert(name) {
  this.name = name
}
popFormFactory(Alert, 'AlertForm');
Alert.prototype.getName = function() {
  return this.name;
}

//消息子类
function Message(name) {
  this.name = name
}
popFormFactory(Message, 'MesssageForm');
Message.prototype.getName = function() {
  return this.name;
}
//实例化登录弹窗
let lgformA = new login('欢迎1');
console.log(lgformA.getName(), lgformA.type); 
let lgformB = new login('欢迎2');
console.log(lgformB.getName(), lgformB.type); 

//实例化提示
let alertFormA = new Alert('用户名不对');
console.log(alertFormA.getName(), alertFormA.type); 
let alertFormB = new Alert('密码不对');
console.log(alertFormB.getName(), alertFormB.type); 

//实例化消息
let messFormA =new Message('通知1234');
console.log(messFormA.getName(), messFormA.type); 
let messFormB =new Message('支付345.00元');
console.log(messFormB.getName(), messFormB.type); 
ES6重写抽象工厂模式
class Form {
 constructor(type) {
   if (new.target === Form) {
     throw new Error('抽象类不能实例化!')
   }
   this.type = type;
 }
}
class loginFormes extends Form {
 constructor(text) {
   super('login');
   this.text = text
   console.log(text)
 }
}

class alertFormes extends Form {
 constructor(text) {
   super('alert');
   this.text = text
   console.log(text)
 }
}

class messageFormes extends Form {
 constructor(text) {
   super('message');
   this.text = text
   console.log(text)
 }
}
function getForm(type) {
 switch (type) {
   case 'login':
     return loginFormes;
     break;
   case 'alert':
     return alertFormes;
     break;
   case 'message':
     return messageFormes;
     break;
   default:
     throw new Error('参数错误, 可选参数:superAdmin、admin、user')
 }
}
let loginClass = getForm('login');
let alertClass = getForm('alert');
let messageClass = getForm('message');

let loginA = new loginClass('欢迎3');
let alertA = new alertClass('密码不对123');
let messageA = new messageClass('通知支付111');

2.单例模式

又被称为单体模式,是只允许实例化一次的对象类,这种模式经常为我们提供一个命名空间。 一个类只有一个实例,并提供一个访问它的全局访问点。

  • 适用场景

定义命名空间和实现分支型方法、登录框、vuex 和 redux中的store

  • 优点

划分命名空间,减少全局变量 增强模块性,把自己的代码组织在一个全局变量名下,放在单一位置,便于维护 且只会实例化一次。 简化了代码的调试和维护

  • 缺点

由于单例模式提供的是一种单点访问,所以它有可能导致模块间的强耦合 从而不利于单元测试。无法单独测试一个调用了来自单例的方法的类,而只能把它与那个单例作为一个单元一起测试。

  • 案例(惰性单例)
var Lazysingle = (function(){
     //单例实例引用
     var _instance = null
     //单例
     function single(){
        //  这里定义私有属性和方法
        return {
            publicMethod: function(){

            },
            publicProperty:'1.0'
        }
     }
     //获取单例对象接口
     return function(){
         //如果为创建单例将创建单例
         if(!_instance){
            _instance = single()
         }
         //返回单例
         return _instance;
     }
 })()
 console.log(Lazysingle().publicProperty)
Es6重写
class LoginForm{
    constructor(){
        this.state = 'hide'
    }
    show(){
        if(this.state === 'show'){
            console.log('已经显示')
            return
        }
        this.state = 'show'
        console.log('登录框显示成功')
    }
    hide(){
        if(this.state === 'hide'){
            console.log('已经隐藏')
            return
        }
        this.state = 'hide'
        console.log('登录框隐藏成功') 
    }
}
LoginForm.getInstance =(function(){
    let instance
    return function(){
        if(!instance){
            instance = new LoginForm()
        }
        return instance
    }
})()
let obj1 = LoginForm.getInstance()
obj1.show()

let obj2 = LoginForm.getInstance()
obj2.hide()

console.log(obj1 === obj2)

3.建造者模式

将一个复杂对象的构建层与其表示层相互分离,同样的构建过程可采用不同的表示。 在创建对象时要更为复杂一些,虽然其目的也是为了创建对象,但是它更多关心的是创建这个对象的整个过程,甚至于创建对象的每一个细节

  • 适用场景

需要生成的对象具有复杂得内部结构;且内部属性本身相互依赖

  • 优点

建造者模式的封装性很好,对象本身与构建过程解耦。 建造者模式很容易进行扩展。如果有新的需求,通过实现一个新的建造者类就可以完成。

  • 缺点

1.当产品内部非常复杂,需要用大量的具体建造者,导致系统庞大 2、产品要有共同点,范围受限制

  • 案例
var Human = function(param){
   this.skill = param && param.skill || '保密'
   this.hobby = param && param.hobby || '保密'
}
Human.prototype ={
   getskill:function(){
       return this.skill
   },
   gethobby:function(){
       return this.hobby
   }
}
//实例化姓名
var Named = function(name){
   //var that = this
   //构造器
   //构造函数解析姓名的姓与名
   (function(name,that){
       that.wholeName = name
       if(name.indexOf(' ') > -1){
           that.FirstName=name.slice(0,name.indexOf(' '));that.secondName=name.slice(name.indexOf(' '));
       }
   })(name,this)
}
//实例化职位类
var Work = function(work){
   //var that = this
   //构造器
   //构造函数中通过传入的职位特征来设置相应职位以及描述
   (function(work, that){
       switch (work){
           case 'code':
               that.work = '工程师';
               that.workDescripy = '每天沉醉于编程';
               break;
           case 'UI':
               that.work = '设计师';
               that.workDescripy = '设计更似一种艺术';
               break;
           case 'teach':
               that.work = '教师';
               that.workDescripy = '分享也是一种快乐';
               break;
           default:
               that.work = work
               that.workDescripy = '对不起,我们不清楚你所选择的职业';
       }
   })(work, this)
}
// 更换期望的职位
Work.prototype.changeWork = function(work){
   this.work = work
}
// 添加对职位的描述
Work.prototype.changeDescript = function(setence){
   this.workDescripy = setence
}

var Person = function(name,work){
   //创建应聘者缓存对象
   var _person=new Human();
   //创建应聘者姓名解析对象
   _person.name= new Named(name);
   //创建应聘者期望职位
   _person.work= new Work(work);
   //将创建的应聘者对象返回
   return _person;
}
var person = new Person('小 明','code');
console.log(person.skill)
console.log(person.name.FirstName)
console.log(person.work.work)
console.log(person.work.workDescripy)
person.work.changeDescript('更改一下职位描述')

4.原型模式

原型模式(prototype)是指用原型实例指向创建对象的种类,并且通过拷贝这些原型创建新的对象。

原型模式,就是创建一个共享的原型,通过拷贝这个原型来创建新的类,用于创建重复的对象,带来性能上的提升。

原型模式适合在创建复杂对象时,对于那些需求一直在变化而导致对象结构不停地改变时,将那些比较稳定的属性与方法共用而提取的继承的实现。

var movieFN = function(name){
    this.name = name
}
 //父类声明原型的方法
movieFN.prototype.getName = function(){
    return this.name
}
// 声明子类
function filmFN (name){
     // 构造函数式继承父类name属性
    movieFN.call(this, name)
}
// 类式继承
filmFN.prototype = new movieFN()
filmFN.prototype.movieShow =function(){
    console.log(`新上映的电影是: ${this.name}`)     
}
var filmA = new filmFN('动物世界')
filmA.movieShow()
ES6重写
class movie {
    constructor(name){
        this.name = name
    }
    getName(){
        return this.name
    }
}
class film extends movie{
    constructor(name){
        super(name)
    }
    movieShow() {
        console.log(`新上映的电影是: ${this.name}`)
    }
}
let filmB =  new film('霹雳娇娃(new)')
filmB.movieShow()

原型模式实质上是一种继承,也是一种创建型模式。 原型模式可以让多个对象分享同一个原型对象的属性与方法,这也是一种继承方式 不过这种继承的实现是不需要创建的,而是将原型对象分享给那些继承的对象 有时候需要让每个继承对象独立拥有一份原型对象,此时我们就需要对原型对象进行复制(prototypeExtend) 所以,原型对象更适合在创建复杂的对象时,对于那些需求一直在变化而导致对象结构不停地改变时,将那些比较稳定的属性与方法共用而提取的继承的实现(初始化对象时将固定的属性和方法放在prototype原型上)

参考: JavaScript设计模式