[封装01-设计模式] 设计原则 和 工厂模式(简单抽象方法) 适配器模式 装饰器模式

814 阅读15分钟

design_principle设计原则.png

Error.png

导航

[react] Hooks

[封装-设计模式01] 设计原则 和 工厂模式(简单抽象方法) 适配器模式 装饰器模式

[React 从零实践01-后台] 代码分割
[React 从零实践02-后台] 权限控制
[React 从零实践03-后台] 自定义hooks
[React 从零实践04-后台] docker-compose 部署react+egg+nginx+mysql
[React 从零实践05-后台] Gitlab-CI使用Docker自动化部署

[源码-webpack01-前置知识] AST抽象语法树
[源码-webpack02-前置知识] Tapable
[源码-webpack03] 手写webpack - compiler简单编译流程
[源码] Redux React-Redux01
[源码] axios
[源码] vuex
[源码-vue01] data响应式 和 初始化渲染
[源码-vue02] computed 响应式 - 初始化,访问,更新过程
[源码-vue03] watch 侦听属性 - 初始化和更新
[源码-vue04] Vue.set 和 vm.$set
[源码-vue05] Vue.extend

[源码-vue06] Vue.nextTick 和 vm.$nextTick
[部署01] Nginx
[部署02] Docker 部署vue项目
[部署03] gitlab-CI

[数据结构和算法01] 二分查找和排序

[深入01] 执行上下文
[深入02] 原型链
[深入03] 继承
[深入04] 事件循环
[深入05] 柯里化 偏函数 函数记忆
[深入06] 隐式转换 和 运算符
[深入07] 浏览器缓存机制(http缓存机制)
[深入08] 前端安全
[深入09] 深浅拷贝
[深入10] Debounce Throttle
[深入11] 前端路由
[深入12] 前端模块化
[深入13] 观察者模式 发布订阅模式 双向数据绑定
[深入14] canvas
[深入15] webSocket
[深入16] webpack
[深入17] http 和 https
[深入18] CSS-interview
[深入19] 手写Promise
[深入20] 手写函数
[深入21] 数据结构和算法 - 二分查找和排序
[深入22] js和v8垃圾回收机制
[深入23] JS设计模式 - 代理,策略,单例

[前端学java01-SpringBoot实战] 环境配置和HelloWorld服务
[前端学java02-SpringBoot实战] mybatis + mysql 实现歌曲增删改查
[前端学java03-SpringBoot实战] lombok,日志,部署
[前端学java04-SpringBoot实战] 静态资源 + 拦截器 + 前后端文件上传
[前端学java05-SpringBoot实战] 常用注解 + redis实现统计功能
[前端学java06-SpringBoot实战] 注入 + Swagger2 3.0 + 单元测试JUnit5
[前端学java07-SpringBoot实战] IOC扫描器 + 事务 + Jackson

(一) 前置知识

(1) 一些单词

principle 原则 原理
responsibility 职责 责任
substitution 代替 置换
// Single Responsibility Principle 单一职责原则

abstract 抽象的
simple 简单的
show up 出现,露面

advanced 高级的,先进的
configuration 配置
// advanced configuration 高级配置

constraint 约束
peek 偷看
shape 形状
anonymous 匿名的

(2) Error

  • new Error(message)
  • 抛出Error实例对象后,整个程序就中断在错误发生的地方,不再往下执行
  • error实例属性
    • message 错误提示信息
    • name 错误名称 ( 非标准属性 )
    • stack 错误堆栈 ( 非标准属性 )
  • 六种错误对象
    • SyntaxError 语法错误
    • ReferenceError ( 引用一个不存在的变量时发生的错误 ) 或者 ( 将一个值分配给无法分配的对象 )
    • RangeError 一个值超出有效范围时发生的错误
    • TypeError ( 变量或者参数 ) 不是 ( 预期类型 ) 时候发生的错误
    • RUIError URI 相关函数的参数不正确时抛出的错误
    • EvalError eval函数没有被正确执行时抛出的错误
  • 第七种,是自定义错误对象
    • 原理
      • 1.就是普通的构造函数(具有name,message,stack等属性)
      • 2.将构造函数的prototype属性指向 new Error() 生成的实例
      • 3.将构造函数的prototype.contructor属性指向构造函数自己,防止引用出错
  • throw语句
    • throw语句的作用是:手动中断程序执行,并抛出错误,即手动抛出错误
    • 注意:throw可以抛出任意类型的值,不一定是错误实例
    try {
          // throw new Error('错误了')
          // ---------------------------- 注意:throw 可以抛出任何类型的值,不局限于错误对象实例
          throw '错误了2'
    } catch(e) {
          // console.log(e.message) ----- 注意:这里对应的是try抛出的错误对象,错误对象具有message属性
          // ---------------------------- 注意:catch() 的参数是try代码块中抛出的值,即也可以是任意类型
          console.log(e);
    }
    
  • try...catch(e)结构
    • try...catch主要作用:是对错误进行处理,选择是否往下执行
    • catch(e) 接受的参数表示try代码块抛出的值,任意类型,因为throw可以抛出任意类型
    • 注意:catch捕获错误后,程序不会中断,会按正常流程继续执行下去
      try {
        throw "出错了";
      } catch (e) {
        console.log(111);
      }
      console.log(222);
      // 111
      // 222 还是会正常执行
    
    • try...catch还可以嵌套try...catch
  • try...finally结构
    • try...catch结构允许在最后添加一个finally代码块,表示不管是否出现错误,都必需在最后运行的语句
    • 如果在try...catch中有return语句,会在finally执行完毕后才会去执行return后面的语句

Error.png

(二) 六大设计原则

  • 单一职责原则 Single Responsibility Principle
  • 开放封闭原则 Open Closed Principle
  • 里氏替换原则 Liskov Substitution Principle
  • 迪米特法则(最少知识原则) Law of Demeter
  • 接口隔离原则 Interface Segregation Principle
  • 依赖倒置原则 Dependence Inversion Principle

(2.1) 单一职责原则

  • 关键词
    • ( 单一 ) ( 职责 )
    • 如果我们有两个动机去改写一个方法,那么这个方法就具有两个职责
  • 单一职责原则
    • 一个方法只做一件事
    • 这样需求变迁,则只会影响到最小粒度的方法,其他方法不会受到影响
  • 如何划分职责???
    • 两个完全不一样的功能,不应放在一个类中
    • 一个类中应该是 ( 一组相关性很高的函数,或者数据的封装 )
    • 如果随着需求的变化,有两个职责总是同时变化,那就不必分离他们
    • 即使两个职责已经被耦合在一起,但它们还没有发生改变的征兆,那么也许没有必要主动分离它们
  • 单一职责原则的优优缺点
    • 优点
      • 降低了单个类或者对象的 ( 复杂度 )
      • 按照 ( 职责 ) 把类和对象分解成 ( 更小粒度 ),有利于 ( 代码复用 ) ( 单元测试 ) 等
      • 当一个职责变化时,不会影响到其他职责
    • 缺点
      • 增加了代码复杂度
      • 增加了对象之间相互联系的难度

(2.2) 开放封闭原则

  • 关键词
    • 开放-封闭原则,满足类,对象,模块,函数是 ( 可扩展的 ),但是是 ( 不可修改的 )
    • 即当需要给程序添加功能或者修改功能时,可以尝试添加代码,但是不能去修改源代码
  • 案例
    • 用对象的 ( 多态 ) 来消除 ( 条件分支 )
    • 将 ( 稳定不变的部分 ) 和 ( 容易变化的部分 ) 分开,则在系统演化迭代的过程中,只需要去修改经常变化的部分,变化的部分使用 ( 钩子函数 ) 和 ( 回调函数 )
  • 优点
    • 原来的代码不用修改
    • 可以复用原来没有修改的代码
  • 开放封闭原则总结
    • 不管是具体的各种设计模式,还是更抽象的面向对象设计原则,比如单一职责原则、最少知识原则、依赖倒置原则等,都是为了让程序遵守开放-封闭原则而出现的

(2.3) 最少知识原则

  • 概念
    • 一个 ( 对象,类,模块,函数,变量 ) 等应该对其他对象有最少的了解,减少对象之间的耦合

(2.4) 里氏替换原则

  • 概念
    • 所有引用 ( 基类 ) 的地方,必须能够使用其 ( 子类 ) 直接替换
  • 特点
    • 子类必须实现父类的所有方法,即继承父类所有的方法和属性
    • 子类可以有自己的属性和方法 ( 重写或者添加新的属性 )
    • 覆盖或者实现父类的方法时,入参可以被放大,输出可以被缩小

(2.5) 依赖倒置原则

  • 模块间依赖通过抽象(比如:接口interface)发生,实现类(实现类可以去实现接口中的方法)之间不直接依赖
  • ( 实现类 ) 依赖于 ( 接口或抽象类 )
  • ( 接口或者抽象类 ) 不依赖于 ( 实现类 ) - 什么是抽象类类?
  • 什么是抽象类类?
    • 如果以一个类中没有足够的信息来描绘一个具体的对象,这样的类就是抽象类
    • 抽象类不能实例化对象,所以抽象类必须被继承才能使用

(2.6) 接口隔离原则

  • 客户端不应该依赖于它不需要的接口
  • 建立单一的接口

(三) 工厂模式

(3.1) 简单工厂模式

  • 在简单工厂模式中,是由工厂来创建产品的,即是老板也是服务员
abstract class Animal {  // 抽象类不需要自己实现方法
  constructor(public name: string) {
    // this.name = name
  }
}
class Cat extends Animal { } // 继承
class Dog extends Animal { } // 继承

// 简单工厂模式
// 通过传入 (类型) 在工厂类中根据传入的类型,分别调用给构造函数去生产对应的实例
class AnimalFactory {
  static create(name: string) { // static 静态方法可以通过类本身去调用
    switch (name) { // 根据传入的类型,调用不同的构造函数,创建不同的实例
      case 'cat':
        return new Cat('cat')
      case 'dog':
        return new Dog('dog')
      default:
        return new Error('出错了')
    }
  }
}

const dog = AnimalFactory.create('dog') // 生产dog
const cat = AnimalFactory.create('cat') // 生产cat
console.log(dog)
console.log(cat)

(3.2) 工厂方法模式

  • 在简单工厂模式中,是由工厂来创建产品的,即是老板也是服务员
  • 在工厂方法模式中,不再由工厂来创建产品,而是先创建具体的工厂,然后具体的工厂来创建产品
  • 即由原来的老板做,变成了发命令让别人来做
  • 即由工厂命令其他工厂来生产产品
abstract class Animal { // ----------------------------------------- 产品
  // 抽象类不需要自己实现方法
  constructor(public name: string) {
    // this.name = name
  }
}
class Cat extends Animal { } // ------------------------------------ 具体产品 cat
class Dog extends Animal { } // ------------------------------------ 具体产品 dog

abstract class AnimalFactory { // ---------------------------------- 具体类型的工厂抽象类
  abstract createAnimal(): Animal
}
class CatFactory extends AnimalFactory { // ------------------------ 创建cat的工厂,实现类
  // 实现类 实现 抽象类的方法
  createAnimal() {
    return new Cat('cat')
  }
}
class DogFactory extends AnimalFactory { // ------------------------ 创建dog的工厂,实现类
  // 实现类 实现 抽象类的方法
  createAnimal() {
    return new Dog('dog')
  }
}

class Factory { // ------------------------------------------------- 工厂类
  // --------------------------------------------------------------- 根据类型,让具体的工厂去生产产品
  static create(name: string) { // static 静态方法可以通过类本身去调用
    switch (name) { // 根据传入的类型,调用不同的构造函数,创建不同的实例
      case 'cat':
        return new CatFactory().createAnimal()
      case 'dog':
        return new DogFactory().createAnimal()
      default:
        return new Error('出错了')
    }
  }
}

const dog = Factory.create('dog')
const cat = Factory.create('cat')
console.log(dog)
console.log(cat)

(3.3) 抽象工厂模式

abstract class Cat {}
abstract class Dog {}
class ChinessCat extends Cat {} // 中国猫
class ChinessDog extends Dog {} // 中国狗
class EnglishCat extends Cat {} // 美国猫
class EnglishDog extends Dog {} // 美国狗

abstract class AnimalFactory { // ------------------------------- 抽象工厂类 => 抽象动物工厂
  abstract createCat(): Cat
  abstract createDog(): Dog
}

class ChineseAnimalFactory extends AnimalFactory { // ----------- 实现类 => 中国动物工厂
  createCat() {
    return new ChinessCat()
  }
  createDog() {
    return new ChinessDog()
  }
}

class EnglishAnimalFactory extends AnimalFactory { // ----------- 实现类 => 美国动物工厂
  createCat() {
    return new EnglishCat()
  }
  createDog() {
    return new EnglishDog()
  }
}

const chineseAnimal = new ChineseAnimalFactory() // ------------- 中国动物
const chineseCat = chineseAnimal.createCat() // ----------------- 中国猫
console.log(chineseCat)

(四) 适配器模式 adapter

  • 适配器模式又称包装器模式,将一个类的接口转化为用户需要的另一个接口,解决类或对象之间接口不兼容问题
  • 旧的接口和使用者不兼容
  • 中间加一个适配器转换接口
  • 总结
    • 适配器模式就是为了解决 ( 适配两个以上接口不兼容的问题 ) 和 ( 外观模式 ) 的核心思路保持一致
    • 适配器模式adapter,也被称作 ( 包装器模式wrapper )
      • 本质上是在原有逻辑上再包装一层
      • 类比 ( 高阶组件 )
2023-03-03 更新如下
- 适配器模式在前端中的应用
  - 接口适配
  - 函数参数适配
  - 数据的适配
  - vue 中的 filter 过滤器
---
1
接口适配
- 需求: 我们要根据支持的地图种类,来渲染,输入有可能是谷歌地图,也有可能是百度地图
---

const googleMap = {
  show() {
    console.log('谷歌地图')
  }
}
const baiduMap = {
  display() {
    console.log('百度地图')
  }
}
- 未使用适配器模式: 时就会报错,接口不兼容
function renderMap(map) {
  map.show();
}
- 使用适配器模式
function adapter(map) {
  if (!map.show) map.show = map.display
  return map
}
renderMap(adapter(googleMap)) // 适配器模式,将不同的接口统一
renderMap(adapter(baiduMap))




2
函数参数适配
- 场景: 比如当我们函数传入的参数个数较多时,一般会用一个对象,而有的参数是可选时,我们就需要使用 适配器模式来解决兼容问题
- 比如: 对于可选参数,如果不存在时,使用 默认参数
---

function sdk(config) {
  let defaultConfig = {
    name: '',
    age: 0
  }
  for(let key in config) {
    defaultConfig[key] =  defaultConfig[key] || config[key] // 适配器模式 - 函数参数适配 - 当有未传参数时使用默认值
  }

  return defaultConfig
}
sdk({name: 'woow_wu7'})




3
数据适配
- 需求: 将后端返回的 ( 数组数据 ) 处理成 ( tree型数据 )
---

后端返回的数据
 const arr = [
  { id: 1, parentId: -1 },
  { id: 2, parentId: 1 },
  { id: 3, parentId: 1 },
  { id: 4, parentId: 2 },
  { id: 5, parentId: 4 },
];

前端转换的数据
// array -> tree
function array2tree(arr) {
  const map = {};
  const res = [];

  arr.forEach((item) => {
    item.children = [];
    map[item.id] = item;
  });

  arr.forEach((item) => {
    if (item.parentId < 0) {
      res.push(item);
    } else {
      map[item.parentId]?.children?.push(item);
    }
  });

  return res;
}

const result = array2tree(arr);
console.log("result: ", result);
1
// Rmb 是需要被适配的类
class Rmb {
  output() {
    return '100rmb'
  }
}

abstract class Money {
  abstract transform(): string
}

class MoneyAdaptor extends Money { // 适配器实现类
  rmb: Rmb
  constructor(rmb: Rmb) {
    super()
    this.rmb = rmb
  }
  transform() {
    return this.rmb.output() + '转成美元'
  }
}

const dollar = new MoneyAdaptor(new Rmb())
console.log(dollar.transform());
2
适配器模式在javascript中的运用 
- 概念:适配器就是将不适配东西适配起来,比如:手机转接头,转换器等
- 案例:将 ( 小写字符串 ) 适配成 ( 大写字符串 )
---

class Lower {
  getSize = () => "size";
}

class Adapter {
  lower = new Lower().getSize();
  getSize = () => this.lower.toUpperCase(); // 转成大写
}

const upper = new Adapter().getSize();

(4.1) 适配器模式有哪些运用

  • axios中区分浏览器和node端时的adaptor函数
  • vue中的computed计算属性

(4.2) 适配器模式应用案例 - axios

  • axios中的adapter函数就会根据系统的类型去调用不同的请求方法,比如浏览器中使用XMLHttpRequest,而在node环境中使用http模块,从而抹平差异化
  • axios源码中的adapter
1. var adapter = config.adapter || defaults.adapter;

2. adapter(config).then() // 不管是什么环境,浏览器或者node都返回一个promise

3. defaultes.adapter 如下
var defaults = {
  adapter: getDefaultAdapter()
}
function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    adapter = require('./adapters/xhr'); // 浏览器环境
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    adapter = require('./adapters/http'); // node环境
  }
  return adapter;
}
  • 手写一个axios中的adapter函数
axios({
  method: "GET",
  url: "www.baidu.com",
}).then(
  (value) => console.log(value),
  (reason) => console.error(reason)
);
// 设计模式yuanze
// 1. 单一职责原则 => 一个函数只实现一个逻辑
// 2. 开放封闭原则 => 可扩展,不可修改
// 3. 最少知识原则 => 模块,函数,方法,变量都要尽量依赖其他类和对象
// 4. 里氏替换原则 => 基类可以用子类直接替换
// 5. 接口隔离原则
// 6. 依赖倒置原则 => 实现类 依赖 接口或者抽象类

// 浏览器端
function xhrAdaptor(config) {
  const { method, url } = config;
  return new Promise((resolve, reject) => {
    const xhr = new XMLHttpRequest();
    xhr.open(method, url, true);
    xhr.setRequestHeader("Content-Type", "application/json"); // setRequestHeader 必须是在 open之后 send之前
    xhr.responseType = "json";
    xhr.onreadystatechange = function () {
      if (xhr.readyState !== 4) {
        return;
        // readyState
        // 0 UNSENT ------------- xhr实例已经被创建,open()方法未被调用
        // 1 OPEND -------------- open()方法被调用,send()方法未被调用,setRequestHeader() 可以被调用
        // 2 HEADERS_RECEIVED --- send()方法被调用,响应头和响应状态已经返回
        // 3 LOADING ------------ 响应体(response entity body)正在下载中,此状态下通过 xhr.response 可能已经有了响应数据
        // 4 DONE --------------- 整个数据传输过程已经完成,无论本次请求是成功还是失败
      }
      if (xhr.status === 200) {
        // readyState === 4 && status === 200
        return resolve(xhr.responseText);
      } else {
        return reject(new Error(xhr.statusText));
      }
    };
    xhr.onerror = function () {
      console.error("error");
    };
    // xhr.onload = function() {
    //     if (xhr.status > 200 && xhr.status < 300 || xhr.status === 304) {
    //         return resolve(xhr.responseText)
    //     }
    // }
    xhr.send();
  });
}

// 服务器端
function httpAdaptor(config) {
  const { method, url } = config
  const http = requrie('http')
  const {hostname, port, path} = url.parse(url)
  return new Promise((resolve, reject) => {
    // http模块
    const options = {
      method,
      hostname,
      port,
      path
    };
    let req = http.request(options, function (response) {
      let chunks = [];
      response.on("data", function (chunk) {
        chunks.push(chunk);
      });
      response.on("end", function () {
        const result = Buffer.concat(chunks).toString();
        return resolve(result);
      });
    });
    req.on("error", function (error) {
      return reject(error);
    });
  });
}


function getDefaultAdaptor() {
  // ---------------------- 适配器模式,根据环境调用不同的方法
  let adaptor;
  if (typeof XMLHttpRequest !== "undefined") {
    // ------ 浏览器环境
    adaptor = xhrAdaptor;
  }
  if (typeof process !== "undefined") {
    // ------------- node环境
    adaptor = httpAdaptor;
  }
  return adaptor;
}

function axios(config) {
  const adaptor = getDefaultAdaptor();
  return adaptor(config);
  // axios返回值就是一个promise对象,无论是浏览器端还是node端
}

(五) 装饰器模式

  • 装饰器模式是为已有功能更多功能的一种方式,是一种结构型设计模式,是对原有类进行扩展
  • 是一种依靠 ( 组合 ) 来实现 ( 类的功能扩展 ),并且支 ( 持多层嵌套 ) 的设计模式
  • 如果直接添加逻辑会违反 ( 单一职责原则 ) 和 ( 开放封闭原则 )
  • 常用的装饰器有 ( 类装饰器 ) ( 属性装饰器 ) ( 方法装饰器 ) ( 参数装饰器 )
abstract class Shape { // 形状抽象类
  abstract draw(): void;
}

class Circle extends Shape { // 形状子类 圆
  draw() {
    console.log("绘制圆");
  }
}

class Rectagle extends Shape { // 形状子类 矩形
  draw() {
    console.log("绘制矩形");
  }
}

abstract class Color extends Shape { // Color 也继承 Shape
  constructor(public shape: Shape) {
    super();
  }
  abstract draw(): void;
}

class Red extends Color {
  draw() {
    this.shape.draw(); // 调用传入的参数实例的draw方法,在new Red()时传入的参数是new Circle()
    console.log("绘制红色");
  }
}
class Yellow extends Color {
  draw() {
    this.shape.draw();
    console.log("绘制黄色");
  }
}

const redCircle = new Red(new Circle());
redCircle.draw() // 调用实例上的draw方法

image.png

(5.1) 装饰器模式在前端中的运用

  • 2021/09/22 更新
  • 表单验证
    • 在 ( 表单提交时,先做表单验证 ) 的工作
    • 核心函数:submit submit.before validate
/**
 * 借助装饰者模式,很容易衍生出 AOP 面向切面编程的概念
 * - 场景:典型场景就是对表单的验证,我们将把表单输入逻辑校验的 validata 函数融入到 before 逻辑当中
 * - 具体:
 *   1. 在提交表单时,执行 ( submit ) 函数,因为在 ( Function.prototype.before ) 挂载了 ( before ) 函数,被所有实例函数所继承
 *   2. 我们并不直接submit函数,而是调用 ( submit.before ) 从而在 sumbit 之前执行验证函数 ( validate ) 从而在执行sumbit之前做表单验证功能
 */
Function.prototype.before = function(beforefn) {
  const self = this
  return function() {
    if (beforefn.apply(this, arguments) === false) return // 表示执行验证逻辑时,验证未通过
    return self.apply(this, arguments) // this是调用before方法时所在的对象,是 submitBtn 在调用before,所以在了的 self 就是 submitBtn 函数
  }
}

const validate = function() {
  // 表单验证逻辑
}

const formSubmit = function() {
  // 表达提交逻辑
  ajax('http:// xxx.com/login', param)
}

submitBtn.onclick = function() {
  formSubmit.before(validate)
}

资料

设计原则1 juejin.cn/post/684490…
设计原则2 juejin.cn/post/684790…
装饰器模式1 juejin.cn/post/691423…
装饰器模式2 juejin.cn/post/688189…

vscode=>codeRunner=>ts-node出现乱码解决方案如下
github.com/formulahend…