设计模式 - 原则+工厂模式浅析

278 阅读13分钟

设计模式原理浅析

设计模式的相关原则和思想

SOLID指代了面向对象编程和面向对象设计的五个基本原则,设计原则是设计模式的指导理论,可以规避不良的软件设计;

  • SOLID浅析
    • 单一功能原则(SingleResponsibilityPrinciple)
    • 开放封闭原则(OpenedClosedPrinciple)
    • 里式替换原则(LiskovSubstitutionPrinciple)
    • 接口隔离原则(InterfaceSegregationPrinciple)
    • 依赖反转原则(DependencyInversionPrinciple)
    • 📢:在JS中,主要用到的设计模式是围绕单一功能开放封闭原则来展开的,其他的浅知即可
  • 核心思想 → 封装变化
    • 封装变化封装的正是软件中那些不稳定的因素,是一种防患于未来的行为,提前将变化抽离出来,从而也实现了后续的无限拓展
    • 将变与不变分离,确保变化的部分灵活,不变的部分稳定 → 封装变化
    • 无论是设计模式中的创建型、结构型还是行为型,都是在用自己的方式去封装不同类型的变化 image.png
    • 创建型模式封装了创建对象过程中的变化,如将创建对象的过程进行抽离(工厂模式)
    • 结构型模式封装的是对象之间组合方式的变化,目的在于灵活的表达对象之间配合关系依赖关系
    • 行为型模式则是将对象千变万化的行为进行抽离,确保可以更安全、更方便的对行为进行更改

七大软件架构设计原则

开放封闭原则(OCP:Open-Closed Principle)

开放封闭原则是指一个软件实体如类、模块和函数应该对拓展开放,对修改关闭,这就意味着当代码需要修改时,可以通过编写新的代码里拓展已有的代码,而不是直接修改现有的代码;开放封闭原则是面向对象设计中最基础的设计原则,可以提高软件系统的可复用性和可维护性;
其主要的原理就是再实现一个软件实,体如类,然后继承自原本的类(基类)-super,在该新类中可以实现对旧逻辑类中的方法或属性重写,从而达到不改变原有代码的前提下实现拓展功能; 常规实例 → 表单校验

function validate() {
    // 校验用户名
    if (!username) {
        ...
    } else {
        ...
    }
    // 校验密码
    if (!pswd){
        ...
    } else {
        ...
    }
    // 校验验证码
    if (!captcha) {
        ...
    } else {
        ...
    }
}

对上述的代码进行开放封闭原则重构

"use strict";
// 定义 Validation 类,用于管理校验处理器和执行校验操作
class Validation {
    constructor() {
        // 私有属性,存储校验处理器的数组
        this.validateHandlers = [];
    }
    // 公共方法,用于添加校验处理器
    addValidateHandler(handler) {
        this.validateHandlers.push(handler);
    }
    // 公共方法,用于执行所有校验处理器的校验操作,将输入参数传递给每个处理器
    validate(input) {
        for (let i = 0; i < this.validateHandlers.length; i++) {
            // 调用每个校验处理器的 validate 方法,并传递输入参数
            console.log(this.validateHandlers[i].validate(input),55555)
            if (this.validateHandlers[i].validate(input) !== true) {
                // 如果某个校验不通过,可以在这里进行相应的处理,比如抛出错误或返回特定的结果
                console.error(`第 ${i + 1} 个校验处理器失败,错误信息:${this.validateHandlers[i].validate(input)}`);
                return false;
            }
        }
        // 所有校验都通过,返回 true
        return true;
    }
}
// 用户名校验处理器类,实现 IValidateHandler 接口
class UsernameValidateHandler {
    validate({username}) {
        console.log(username,3333)
        // 使用传入的用户名进行校验逻辑
        const validUsernamePattern = /^[a-zA-Z0-9_]{3,20}$/;
        return validUsernamePattern.test(username)?true:'用户名格式错误,请监测';
    }
}
// 密码校验处理器类,实现 IValidateHandler 接口
class PwdValidateHandler {
    validate({password}) {
                console.log(password,222)
        // 使用传入的密码进行校验逻辑
        const validPwdPattern = /^[a-zA-Z0-9_\-\u4e00-\u9fa5]+$/;
        return validPwdPattern.test(password)?true:'密码校验错误,请检查';
    }
}
// 验证码校验处理器类,实现 IValidateHandler 接口
class CaptchaValidateHandler {
    validate({captcha}) {
                console.log(captcha,444)
        // 使用传入的验证码进行校验逻辑
        const expectedCaptcha = '1234';
        return captcha === expectedCaptcha?true:'验证码错误,请检查';
    }
}
// 测试代码
const validation = new Validation();
validation.addValidateHandler(new UsernameValidateHandler());
validation.addValidateHandler(new PwdValidateHandler());
validation.addValidateHandler(new CaptchaValidateHandler());
const username = 'validUsername';
const password = '123123';
const captcha = '12234';
if (validation.validate({ username, password, captcha })) {
    console.log('表单校验通过');
}
else {
    console.log('表单校验未通过');
}
依赖倒置原则(DIP:Dependence Inversion Principle)

依赖倒置原则指设计代码结构时,高层模块不应该依赖底层模块,二者应该依赖其抽象;抽象不应该依赖细节,细节应该依赖抽象;
通过依赖倒置原则可以降低类与类之间的耦合性,提高系统的稳定性,提高代码的可读性和可维护性,并降低修改程序带来的风险;
内部实现逻辑就是不断地将实现细节进行抽离提取,封装成每一个单独的基类(抽象类)和实体类,然后根据相互依赖关系进行相互继承与重写,后续有新功能需求添加时只需要新定义一个实体类继承自基类,然后进行实例化注入即可;这样可以实现后续的无风险拓展;
以抽象为基础比以细节为基础搭建起来的架构要稳定的多,因此拿到需求后要面向接口编程,按照先顶层再细节的顺序设计代码结构

class Bike {
  run() {
    console.log('Bike run')
  }
}
class Passenger {
  construct(Bike: bike) {
    this.tool = bike
  }
  public start() {
    this.tool.run()
  }
}

重构后

按照遵循依赖倒置原则,可以声明一个接口 ITransportation,让 Passenger 类的构造函数改为 ITransportation 类型,从而做到 Passenger 类和 Bike 类解耦,这样当 Passenger 需要支持 Car 类的时候,只需要新增 Car 类即可

interface ITransportation {
  run(): void
}
class Bike implements ITransportation {
  run() {
    console.log('Bike run')
  }
}
class Car implements ITransportation {
  run() {
    console.log('Car run')
  }
}
class Passenger {
  construct(ITransportation : transportation) {
    this.tool = transportation
  }
  public start() {
    this.tool.run()
  }
}
单一职责原则(SRP:Simple Responsibility Principle)

一个class、Interface、Methods只负责一项职责

指不要存在一个实体负责多个职责;这样一旦有一个发生变化,就有可能会影响到其他职责,需要将实体和职责进行解耦,做到后期的需求变更不会相互影响,同时也降低了类的复杂度、提高了类的可读性,系统的可维护性,降低变更引起的分险;

接口隔离原则(ISP:Interface Segregation Principle)

指用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口,因此在设计接口时需要注意以下几点:

  • 一个类对另一个类的依赖应该建立在最小接口上
  • 建立单一接口,不要建立在庞大臃肿的接口上
  • 尽量细化接口,接口中的方法尽量(适度)少

接口隔离原则符合高内聚,低耦合的设计思想,使得类具有很好的可读性、可拓展性和可维护性;次原则需要对抽象、业务模型有非常好的理解;

工厂模式

核心是将创建对象的过程单独封装,最终实现的效果是可以实现无脑传参
由一个工厂对象决定创建某一种产品对象的实例,主要用来创建同一类对象,打造高效的对象创建工厂
抽象工厂是一种创建型设计模式,可以通过提供一个接口来创建一系列相关或相互依赖的对象,而无需指定他们的具体类

在设计模式中,开发者主要做的是将变与不变找到,然后找到其中的共性和区别,有的甚至有变与变相互关联的,此时就需要将共性封装彻底,将共性与个性分离彻底,一般将共性封装到一个类中,然后将承载了共性和个性化的逻辑封装到同一个函数中,从而避免了多个共享和个性化的类过多和实例化逻辑过多的问题;

  • 应用场景
    • 有构造函数的地方
    • 写了大量构造函数,调用了大量的new 一般工厂模式
// 共性部分 → 有可能会更多  没有将共性部分封装彻底
function Coder(name , age) {
  this.name = name
  this.age = age
  this.career = 'coder' 
  this.work = ['写代码','写系分', '修Bug']
}
function ProductManager(name, age) {
  this.name = name 
  this.age = age
  this.career = 'product manager'
  this.work = ['订会议室', '写PRD', '催更']
}
......

// 变化部分 共性和个性化强耦合 没有彻底分离 且后续拓展需要实例化跟多的共性部分然后再根据变化进行耦合起来
function Factory(name, age, career) {
  switch(career) {
      case 'coder':
          return new Coder(name, age) 
          break
      case 'product manager':
          return new ProductManager(name, age)
          break
      ......
}

抽离解耦版 存在的问题 → 没有遵循开放封闭原则(对拓展开放,对修改封闭:即软件实体(类、模块、函数)可以拓展,但是不可以修改);该版本存在的问题是不是在拓展,而是在修改

// 共性部分 name , age, career, work
function User(name , age, career, work) {
  this.name = name
  this.age = age
  this.career = career 
  this.work = work
}

// 共性间的区别:
// 1、每个字段值不一样
// 2、work需要随着career字段的变化而变化
function Factory(name, age, career) {
  let work
  switch(career) {
      case 'coder':
          work =  ['写代码','写系分', '修Bug'] 
          break
      case 'product manager':
          work = ['订会议室', '写PRD', '催更']
          break
      case 'boss':
          work = ['喝茶', '看报', '见客户']
      case 'xxx':
          // 其它工种的职责分配
          ...
          
  return new User(name, age, career, work)
}
  • 抽象工厂和简单工厂对比
    抽象工厂模式是每个抽象产品派生多个具体的产品类,每个抽象工厂派生多个具体工厂类,每个具体工厂负责多个(一系列)具体产品的实例创建
    • 共同特点:都尝试去分离一个系统中变与不变的部分
    • 不同点:在于场景的复杂度
      • 简单工厂:处理的对象是类,且都比较简单,其共性都已抽离,逻辑简单,可以不苛求拓展性
      • 抽象工厂:处理的也是类,但是是比较复杂的类,其不同点较多,可以划分出很多不同点来,同时其可拓展的可能性也很大;主要包括四个关键角色
        • 包含抽象工厂具体工厂抽象产品具体产品
        • 实际案例分析:创建同一品牌的多个产品,就可以使用抽象工程模式
          • 抽象工厂:定义了所有所有电器品牌具有的共同接口
          • 具体工厂:定义了每一个自己的电器品牌,可以生产自己的电视机、冰箱、空调等 lena7-抽象工厂模式.png
        • 抽象工厂(抽象类,不能被用于生成具体的实例),用于声明最终目标的共性,在一个系统里,抽象工厂可以有多个,如手机抽象类、平板抽象类、游戏机抽象类等
        class MobilePhoneFactory {
            // 提供操作系统的接口
            createOS(){
                throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
            }
            // 提供硬件的接口
            createHardWare(){
                throw new Error("抽象工厂方法不允许直接调用,你需要将我重写!");
            }
        }
        
        • 具体工厂(用于生产具体的产品),继承自抽象工厂、实现了抽象工厂里声明的那些方法,用于创建具体的产品类
        // 具体工厂继承自抽象工厂
        class FakeStarFactory extends MobilePhoneFactory {
            // 像如下的采用new的方式new出具体对象的类叫做具体的产品类,不同的具体产品类往往有着共同的功能,如Android和iOS都是操作系统,都有着操作手机硬件系统的具体功能
            createOS() {
                // 提供安卓系统实例
                return new AndroidOS()
            }
            createHardWare() {
                // 提供高通硬件实例
                return new QualcommHardWare()
            }
        }
        
        • 抽象产品(抽象类,不能被用于生成具体实例)
        // 定义操作系统这类产品的抽象产品类
        class OS {
            controlHardWare() {
                throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
            }
        }
        // 定义手机硬件这类产品的抽象产品类
        class HardWare {
            // 手机硬件的共性方法,这里提取了“根据命令运转”这个共性
            operateByOrder() {
                throw new Error('抽象产品方法不允许直接调用,你需要将我重写!');
            }
        }
        
        • 具体产品(用于生成产品簇里的一个具体的产品所依赖的更细粒度的产品)
        // 定义具体操作系统的具体产品类
        class AndroidOS extends OS {
            controlHardWare() {
                console.log('我会用安卓的方式去操作硬件')
            }
        }
        class AppleOS extends OS {
            controlHardWare() {
                console.log('我会用🍎的方式去操作硬件')
            }
        }
        
        // 定义具体硬件的具体产品类
        class QualcommHardWare extends HardWare {
            operateByOrder() {
                console.log('我会用高通的方式去运转')
            }
        }
        
        class MiWare extends HardWare {
            operateByOrder() {
                console.log('我会用小米的方式去运转')
            }
        }
        ...
        
        // 这是我的手机
        const myPhone = new FakeStarFactory()
        // 让它拥有操作系统
        const myOS = myPhone.createOS()
        // 让它拥有硬件
        const myHardWare = myPhone.createHardWare()
        // 启动操作系统(输出‘我会用安卓的方式去操作硬件’)
        myOS.controlHardWare()
        // 唤醒硬件(输出‘我会用高通的方式去运转’)
        myHardWare.operateByOrder()
        
        • 后续的拓展就可以根据自己拓展的具体情况来判断是添加具体的产品还是具体的工厂类的新加,可以实现开放封闭原则(对拓展开放,对修改封闭)
        • 函数的方式实现抽象工厂
        // 创建抽象工厂函数
        function createButtonFactory() {
          // 创建对象字典
          const factories = {
            'primary': createPrimaryButton,
            'secondary': createSecondaryButton,
          };
        
          // 创建工厂选择器函数
          function createButton(type) {
            const factory = factories[type];
            if (!factory) {
              throw new Error(`Invalid button type: ${type}`);
            }
            return factory();
          }
        
          // 创建具体的工厂函数
          function createPrimaryButton() {
            const button = document.createElement('button');
            button.className = 'btn btn-primary';
            button.textContent = 'Primary Button';
            return button;
          }
        
          function createSecondaryButton() {
            const button = document.createElement('button');
            button.className = 'btn btn-secondary';
            button.textContent = 'Secondary Button';
            return button;
          }
        
          // 返回工厂选择器函数
          return createButton;
        }
        
        // 使用抽象工厂函数创建不同类型的按钮
        const createButton = createButtonFactory();
        const primaryButton = createButton('primary');
        const secondaryButton = createButton('secondary');
        
        昊桐_260c → JS设计模式之抽象工厂模式
        class FactoriesRule {//抽象工厂
            constructor(){
                if(new.target === FactoriesRule){
                    throw new Error("抽象工厂不能被实例化")
                }
            }
            get(type){
                throw new Error("抽象工厂的方法不能被调用")
            }
        }
        class Factories extends FactoriesRule{//总工厂
            constructor(){
                super();
            }
            get(type){
                var obj = {
                    student:new Student(),
                    teacher:new Teacher(),
                }
                if(obj[type]){
                    return obj[type];
                }else {
                    throw new Error("不存在该工厂")
                }
            }
        }
        class Rule {//抽象工厂
            constructor(){
                if(new.target === Rule){
                    throw new Error("抽象工厂不能被实例化")
                }
            }
            getList(){
                throw new Error("抽象工厂的方法不能被调用")
            }
        }
        class Student extends Rule{
            constructor(){
                super()
            }
            getList(){
                return ["小明","小刚","小美","小丽"];
            }
        }
        class Teacher extends Rule{
            constructor(){
                super()
            }
            getList(){
                return ["赵老师","钱老师","孙老师","李老师"];
            }
        }
        const factories = new Factories();//总工厂
        const student = factories.get("student");//学生工厂
        console.log(student.getList());//学生名单
        const teacher = factories.get("teacher");//老师工厂
        console.log(teacher.getList());//老师名单
        
        // 拓展
        class ShiYan extends FactoriesRule{//实验小学工厂
            constructor(){
                super();
            }
            get(type){
                var obj = {
                    student:new Student(),
                    teacher:new Teacher(),
                }
                if(obj[type]){
                    return obj[type];
                }else {
                    throw new Error("不存在该工厂")
                }
            }
        }
        const shiyan = new ShiYan();//总工厂
        const shiyanStudent = shiyan.get("student");//学生工厂
        console.log(shiyanStudent.getList());//学生名单
        const shiyanTeacher = shiyan.get("teacher");//老师工厂
        console.log(shiyanTeacher.getList());//老师名单
        

new.target浅析

new.target允许检测函数或构造方法是否是通过new 运算符调用的,若函数或构造方法是由new调用的,则new.target属性值指向该函数或构造函数,否则为undefined

  • 普通函数调用,new.target值为undefined,使用new运算符调用的函数其值为函数本身,可以用new.target来判断一个函数是否为new调用的
    function Person() {
      if(new.target) {
        //  new 方式调用
        this.name = 'Mike'
        console.log(this)
      }else {
        // 普通方式调用
        throw new Error('函数必须使用new调用')
      }
    }
    new Person() // Person { name: 'Mike' }
    Person() // Uncaught Error: 函数必须使用new调用
    
  • ES6中的class类创建的实例必须使用newnew.target的值为类定义本身
class Person {
  constructor() {
    console.log(new.target,222,new.target === Person)
  }
}

// sudo
// class Person {
//   constructor() {
//     console.log(new.target,222,new.target === Person)
//   }
// } 222 true
  • 在类的继承中,new.target的值指向初始化类的类定义(父类中的constructor的new.target指向子类而非父类)
class Person {
  constructor() {
    console.log(new.target === Person,3333,new.target === Male)
  }
}
class Male extends Person {
  constructor() {
    super()
      console.log(new.target === Person,2222,new.target === Male)
  }
}
new Person()
// true 3333 false
new Male()
// false 3333 true
// false 2222 true