JavaScript 前端设计模式之设计原则 — 设计模式入门篇 《一》

设计原则

前言:在了解设计模式之前,一定要先理解什么是设计原则,只有这样才能悟透设计模式的根本以及来源。

1.何为设计?

  • 按哪一种思路或者标准来实现的功能;
  • 功能相同,可以有不同设计的方式;
  • 需求如果不断变化,设计的作用才能体现出来;

2. SOLID 五大设计原则

  1. 单一职责原则(S) 单一功能原则认为对象应该仅具有一种 单一功能 的概念;
  2. 开放封闭原则(O)开闭原则认为 软件体应该是对于扩展开放的,但是对于修改封闭的 概念;
  3. 里式替换原则 (L)里式替换原则认为 程序中的对象应该是可以在不改变程序正确性的前提下被它的子类所替换的概念;
  4. 接口隔离原则 (I) 接口隔离原则认为 多个特定客户端接口要好于一个宽泛用途的接口 的概念;
  5. 依赖反转原则 (D)依赖反转原则认为 应该遵从依赖于抽象而不是一个实例 的概念 ,依赖注入是该原则的一种实现方式;

2.1 单一职责原则 (Single Responsibility Principle)

  • 一个类或者模块只负责一个职责,如果功能特别复杂就进行拆分;
  • 单一职责可以降低类的复杂性,提高代码可读性、可维护性;
  • 当类代码函数过多、方法过多、功能太多、职责太杂的时候就要对类进行拆分了;
  • 拆分不能过度,如果拆分过度会损失内聚性和维护性;

一个商品类,有name,price属性,以及可以修改商品名,和修改价格,那么就可细分

/**
 * 商品类
 */
class Commodity {
  public name: string;
  public price: number;
  // 更新商品名
  public updateName() { }
  // 更新商品价格
  public updatePrice() { }
}

一个类中的方法尽量维持在10个方法以内,这样可以做到高内聚和维护性,但是也不能过分拆分,取决于业务的功能。

2.2 开放封闭原则 (Open Closed Principle)

  • 对扩展开放,对修改关闭

  • 增加需求时,扩展新代码,而非修改已有代码;

  • 开闭原则是设计模式中的总原则;同时也是软件设计的终极目标;

  • 对近期可能会变化并且如果有变化但改动量巨大的地方要增加扩展点,扩展点过多会降低可读性;

例如顾客去商场购物,普通用户不打折,VIP用户打9折,那么我们现在用代码来实现一下

/**
 * 会员类
 * @param {string} rank 会员等级
 */
class User {
  constructor(public rank: string) {
    this.rank = rank;
  }
}

/**
 * 超市类
 * @param name 商品
 * @param price 价格
 */
class Shopping {
  constructor(public name: string, public price: number) {

  }
  cashier(user: User) {
    switch (user.rank) {
      case 'general':
        console.log(`您是普通会员,折扣后的价格是:${this.price * 1}`);
        break;
      case 'vip':
        console.log(`您是vip会员,折扣后的价格是:${this.price * .9}`);
        break;
      default:
        console.log(`您的价格是:${this.price}`);
        break;

    }
  }
}

let goods = new Shopping('MacBook Pro', 10000);
let general = new User('general');
let vip = new User('vip');
console.log(goods.cashier(general)); // 您是普通会员,给您的折扣是:10000
console.log(goods.cashier(vip)); // 您是普通会员,给您的折扣是:9000

假如现在超市又新增了一个黄金VIP,那么现在是不是得修改 Shopping 类中的 cashier 方法,这样就违背了 开放封闭原则 ,所以上面就不是一个完美的设计。

我们现在改进一下上面的代码, 让它符合 开放封闭原则

/**
 * 会员类
 * @param {string} rank 会员等级
 */
class User {
  constructor(public rank: string, public discount: number = 1) {
    this.rank = rank;
    this.discount = discount;
  }
}

/**
 * 超市类
 * @param name 商品
 * @param price 价格
 */
class Shopping {
  constructor(public name: string, public price: number) { }
  cashier(user: User) {
    return this.price * user.discount;
  }
}

let goods = new Shopping('MacBook Pro', 10000);
let general = new User('general');
let vip = new User('vip', .9);
let goldVip = new User('goldVip', .8);
console.log('折扣后的价格是:' + goods.cashier(general)); // 10000
console.log('折扣后的价格是:' + goods.cashier(vip)); // 9000
console.log('折扣后的价格是:' + goods.cashier(goldVip)); // 8000

前端实际应用中符合该原则的较多,几乎大部分库都符合该原则,例如 Axios 中的拦截器;

2.3 里式替换原则 (Liskov Substitution Principle)

  • 所有引用基类的地方必须能透明地使用其子类的对象;
  • 子类能替换掉父类,使用者可能根本就不需要知道是父类还是子类,反之则不行;
  • 里式替换原则是开闭原则的实现基础,程序设计的时候尽量使用基类定义及引用,运行时再决定使用哪个子类;
  • 里式替换原则可以提高代码的复用性,提高代码的可扩展性,也增加了耦合性;
  • 相对于多态,这个原则是讲的是类如何设计,子类如果违反了父类的功能则表示违反了里式替换原则

一个用去买咖啡喝,那么想要知道当前购买咖啡的价格

	/**
 * 尽可能使用父类或者抽象类
 * 任何再能使用父类的地方都要可以使用子类
 * 类似多态
 */

abstract class AbstractCoffe {
  abstract getPrice(): number;
}

class Mocha extends AbstractCoffe {
  getPrice() {
    return 28;
  }
}

class Americano extends AbstractCoffe {
  getPrice() {
    return 26;
  }
}

class Cappuccino extends AbstractCoffe {
  getPrice() {
    return 30;
  }
}

class Customer {
  // 这里可以传父类,也可以传子类,所以就可以替换
  // 这就是里式替换原则
  // 但是这里的子类不能违反父类的约定,也就是 子类中的 getPrice 不能反悔其它的类型,只能返回 Number
  drink(abstractCoffe: AbstractCoffe) {
    console.log("这个咖啡价格是:" + abstractCoffe.getPrice());
  }
}

let c1 = new Customer();
c1.drink(new Mocha())
c1.drink(new Americano())
c1.drink(new Cappuccino())

2.4 依赖倒置原则(Dependence Inversion Principle)

  • 面向接口编程,依赖于抽象而不依赖于具体实现;
  • 要求我们在程序代码中传递参数时或在光联关系中,尽量引用层次高的抽象层类;
  • 使用方只关注接口而不关注具体类的实现;
/**
 * 依赖抽象,而非依赖具体的实现
 */

interface GrilFriend {
  age: number;
  height: number;
  weight: number;
  beautiful(): Boolean
}

class TongLiYa implements GrilFriend {
  age: 25;
  height: 168;
  weight: 105;
  beautiful() {
    return true
  }
}

class XiaoHone implements GrilFriend {
  age: 25;
  height: 168;
  weight: 105;
  beautiful() {
    return true
  }
}

class SingleDog {
  constructor(public grilFriend: GrilFriend) { }
}

let dog1 = new SingleDog(new TongLiYa());

// 不需要是TongLiYa ,但是可以与她类似就行了,依赖抽象GrilFriend,而非具体的 TongLiYa
let dog2 = new SingleDog(new XiaoHone());

2.5 接口隔离原则(Interface Segregation Principle)

  • 保持接口的单一独立,避免出现胖接口;
  • 客户端不应该依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上;
  • 接口尽量细化,而且接口中的方法尽量的少;
  • 类似于单一职责原则,更关注接口;
interface IUerManager {
  updateUserInfo(): void; // 更新用户信息
  updatePassword(): void; // 更新密码
}

interface IProductManager {
  updateProduct(): void;
  updatePrice(): void;
}

/************************* 举例说明 ****************************/
/**
 * 可以吃饭的接口
 */
interface IEating {
  eat(): void
}

/**
 * 可以吃唱歌的接口
 */
interface ISinging {
  singing(): void
}

/**
 * 可以说话的接口
 */
interface ISpeaking {
  speaking(): void
}

// 我可以唱歌,可以吃饭,会讲话
// 拆分成三个,1. 为了复用,2. 为了低耦合,3. 为了单一职责
class Me implements IEating, ISinging, ISpeaking {
  eat() { }
  singing() { }
  speaking() { }
}

// zhangsan 只会吃
class Zhangsan implements IEating {
  eat() { }
}

3. 迪米特法则 (Law of Demter, LOD)

  • 有时候也叫做最少知识原则;
  • 一个软件实体应当尽可能少地与其它实体发生相互作用;
  • 迪米特法则的初衷在于降低类之间的耦合;
  • 类定义时尽量要实现内聚,少使用 public 修饰符,尽量使用 private、protected 等;
  • PS:现实中得你自己的对象,你只需要了解自己的对象的习惯爱好,你对别人的对象尽可能少的了解;

老板需要知道公司产品的一个名字,那么他就问了经理,经理就问了员工A

/**
 * 员工A
 */
class StaffA {
  getProductName() {
    console.log("这个产品的名字是:XXX");
  }
}

/**
 * 经理
 */
class Manager {
  private staffA: StaffA = new StaffA();
  getProductName() {
    this.staffA.getProductName()
  }
}

/**
 * Boss
 */

class Boss {
  private manager: Manager = new Manager();
  getProductName() {
    this.manager.getProductName()
  }
}

let boss = new Boss();
boss.getProductName();

4. 合成复用原则

2.1 类的关系

  • 类之间有三种基本关系,分别是关联(聚合和组合)、泛化和依赖
  • 如果一个类单向依赖另一个类,那么它们之间就是 单向关联。如果彼此依赖,则为相互依赖,即 双向关联
  • 关联关系包括两种特例:聚合和组合
    • 聚合,用来表示整体与部分的关系或者拥有关系,代表部分的对象可能会被整体拥有,但并不一定会随着整体的消亡而销毁,比如班级和学生
    • 合成或者说组合要比聚合关系强的多,部分和整体的生命周期是一致的,比如人和器官之间;
// 一个班级里面会有多个学生,还有老师,这就是聚合的关系
class Class {
  public students: Array<Student>
  public teacher: Teacher = new Teacher()
}

// 学生和老师都有书,这是组合的关系
class Student {
  public book: Book;
}

// 学生和老师都有书,这是组合的关系
class Teacher {
  public book: Book;
}

class Book { }

// 学生和老师就是人的泛化,泛化关系
class Person { }

// 老师教书依赖黑板,依赖关系
class Block { }

2.2 合成复用原则

  • 合成复用原则是通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的;
  • 新对象可以调用已有对象的功能,从而达到复用;
  • 原则是尽量首先使用组合/聚合的方式,而不是使用继承;
  • 也就是说,专业的人做专业的事;
// 尽量使用组合或者聚合原则,而不是使用继承,因为继承的耦合性太强了
class Cooker {
  cook() { }
}

// class Person2 extends Cooker{ 尽量不使用继承
class Person2 {
  private cooker: Cooker;
  cook() {
    this.cooker.cook();
  }
}

5. 总结

  • 开闭原则是核心,对修改关闭对扩展开放是软件设计的基石;
  • 单一职责要求我们设计接口和模块功能的时候尽量保证单一性,修改一条不影响全局和其它模块;
  • 里式替换原则和依赖倒置原则要求面向接口和抽象编程,不要依赖具体实现,否则实现一改,上层调用者就要对应修改;

6. 如何写出好的代码?

  • 可维护性 BUG是否好改?
  • 可读性 是否容易看懂?
  • 可扩展性 是否可以添加新功能?
  • 灵活性 添加新功能是否容易?老方法和接口是否容易复用?
  • 简洁性 代码是否简单清晰?
  • 可复用性 相同的代码不要写多遍?
  • 可测试性 是否方便写单元测试和集成测试?

7. 23 种设计模式

创建性(5):

  • 工厂模式(简单工厂模式、工厂方法模式)、抽象工厂模式、建造者模式、单例模式
  • 原型模式

结构性模式(7):

  • 代理模式、桥接模式、装饰器模式、适配器模式
  • 外观模式、组合模式、享元模式

行为型(11):

  • 观察者模式、模板方法模式、策略模式、职责链模式、迭代器模式、状态模式
  • 访问者模式、备忘录模式、命令模式、解释器模式、中介者模式
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值