Published on

设计模式六大原则:金庸群侠传

Authors
  • avatar
    Name
    青雲
    Twitter

在金庸武侠的世界里,江湖中的武林高手武艺各异,却又各有所长。他们的成功不仅仅在于招式的精巧,更在于他们对武学核心原则的深刻理解和运用。就像设计软件系统一样,掌握了核心的设计原则,才能构建出灵活、可扩展、易维护的系统。设计模式中的六大原则,就如同武侠小说中的武林秘籍,为我们在设计复杂系统时提供了宝贵的指导。

单一职责原则(Single Responsibility Principle, SRP)

单一职责就像令狐冲所学的独孤九剑。独孤九剑讲究一招一式皆有其特定的目标和用途,或破剑、或破刀、或破枪等,每一式都专注于应对一种兵器的攻击。单一职责原则也是如此,一个类只负责一项职责,如同独孤九剑的每一式都专注于破解一类兵器。 04021f07-765f-43d8-9098-db77f1d494e0.jpeg

定义

单一职责原则强调:一个类应该只有一个引起它变化的原因。即,一个类只负责一个职责。

示例:员工管理与报酬管理分离

实现

class Employee {
  private name: string;

  constructor(name: string) {
    this.name = name;
  }

  getName(): string {
    return this.name;
  }
}

// 职责1:员工管理
class EmployeeManager {
  private employees: Employee[] = [];

  addEmployee(employee: Employee): void {
    this.employees.push(employee);
  }

  getEmployees(): Employee[] {
    return this.employees;
  }
}

// 职责2:员工报酬管理
class EmployeePayroll {
  calculatePay(employee: Employee): number {
    // 计算员工报酬逻辑
    return 1000; // 示例
  }
}

// 使用示例
const emp1 = new Employee("Alice");
const empManager = new EmployeeManager();
empManager.addEmployee(emp1);
const payroll = new EmployeePayroll();
console.log(`Pay for ${emp1.getName()}: ${payroll.calculatePay(emp1)}`); // Pay for Alice: 1000

类图

  1. Employee 类负责处理员工的基本信息。
  2. EmployeeManager 类负责管理员工,如添加员工、获取员工信息等。
  3. EmployeePayroll 类负责计算员工的报酬。

体现单一职责原则的设计模式

  1. 策略模式(Strategy Pattern):通过将算法封装到具体的策略类中,使每个策略类只负责一种特定的算法逻辑,遵循单一职责原则。
  2. 装饰器模式(Decorator Pattern):通过将额外的职责封装到装饰器类中,使得每个装饰器类只负责一种特定的职责,符合单一职责原则。
  3. 观察者模式(Observer Pattern):通过将通知逻辑封装到观察者类中,使得每个观察者类只负责处理特定的通知事件,体现了单一职责原则。
  4. 命令模式(Command Pattern):通过将请求封装到具体的命令类中,使得每个命令类只负责一个特定的请求,符合单一职责原则。

典型应用场景

表单验证

在前端开发中,表单验证是一个常见的需求。可以将不同的验证逻辑封装到不同的类中,使得每个类只负责一种验证方式,如邮箱验证、手机号验证、密码强度验证等。 示例

class EmailValidator {
  validate(email: string): boolean {
    const regex = /^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/;
    return regex.test(email);
  }
}

class PhoneValidator {
  validate(phone: string): boolean {
    const regex = /^\d{10}$/;
    return regex.test(phone);
  }
}

class PasswordValidator {
  validate(password: string): boolean {
    return password.length >= 8;
  }
}

// 使用示例
const emailValidator = new EmailValidator();
console.log(emailValidator.validate("[email protected]")); // true

const phoneValidator = new PhoneValidator();
console.log(phoneValidator.validate("1234567890")); // true

const passwordValidator = new PasswordValidator();
console.log(passwordValidator.validate("strongpass")); // true

动画处理

动画处理在前端开发中非常常见,可以将不同的动画效果封装到不同的类中,使得每个类只负责一种动画效果,如淡入淡出动画、滑动动画等。 示例

class FadeInAnimation {
  animate(element: HTMLElement): void {
    element.style.transition = "opacity 1s";
    element.style.opacity = "1";
  }
}

class SlideInAnimation {
  animate(element: HTMLElement): void {
    element.style.transition = "transform 1s";
    element.style.transform = "translateX(0)";
  }
}

// 使用示例
const element = document.getElementById("myElement");
const fadeInAnimation = new FadeInAnimation();
fadeInAnimation.animate(element); // 应用淡入动画

const slideInAnimation = new SlideInAnimation();
slideInAnimation.animate(element); // 应用滑动动画

数据请求处理

在前端开发中,数据请求处理也是一个常见的应用场景。可以将不同的请求逻辑封装到不同的类中,使得每个类只负责一种请求方式,如 GET 请求、POST 请求等。 示例

class GetRequest {
  send(url: string): Promise<Response> {
    return fetch(url);
  }
}

class PostRequest {
  send(url: string, data: any): Promise<Response> {
    return fetch(url, {
      method: "POST",
      headers: {
        "Content-Type": "application/json"
      },
      body: JSON.stringify(data)
    });
  }
}

// 使用示例
const getRequest = new GetRequest();
getRequest.send("https://api.example.com/data")
  .then(response => response.json())
  .then(data => console.log(data));

const postRequest = new PostRequest();
postRequest.send("https://api.example.com/data", { key: "value" })
  .then(response => response.json())
  .then(data => console.log(data));

数据格式化

在前端开发中,数据格式化也是经常需要处理的,可以将不同的格式化逻辑封装到不同的类中,使得每个类只负责一种格式化方式,如日期格式化、货币格式化等。 示例

class DateFormatter {
  format(date: Date): string {
    return date.toLocaleDateString();
  }
}

class CurrencyFormatter {
  format(amount: number): string {
    return `$${amount.toFixed(2)}`;
  }
}

// 使用示例
const dateFormatter = new DateFormatter();
console.log(dateFormatter.format(new Date())); // 输出: 例如 12/31/2022

const currencyFormatter = new CurrencyFormatter();
console.log(currencyFormatter.format(123.456)); // 输出: $123.46

总结

单一职责原则强调每个类只负责一个职责,避免多个职责耦合在一起。通过结合各种设计模式和面向对象原则,单一职责原则在前端开发中有着广泛的应用。典型的应用场景包括表单验证、动画处理、数据请求和数据格式化等。合理运用单一职责原则,可以使代码更具可读性、可维护性和可扩展性。

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

张三丰的太极拳以柔克刚,圆转如意,对扩展开放,对修改关闭。开闭原则就如同太极拳的理念,在面对新的需求时,我们不是去修改已有的代码,而是通过扩展现有代码来实现新的功能。就像太极拳在应对不同的对手时,通过调整招式的变化来克敌制胜,而不是改变拳法的根本原理。这样可以保证代码的稳定性和可维护性,避免因为频繁修改代码而引入新的错误。

定义

开闭原则强调:软件实体(如类、模块和函数)应该对扩展开放,对修改封闭。这意味着我们应当能够添加新的功能,而不改动现有的代码。

示例:形状的扩展

定义抽象类和具体类

// 抽象类:形状
abstract class Shape {
  abstract area(): number;
}

// 具体类:圆形
class Circle extends Shape {
  private radius: number;

  constructor(radius: number) {
    super();
    this.radius = radius;
  }

  area(): number {
    return Math.PI * this.radius * this.radius;
  }
}

// 具体类:矩形
class Rectangle extends Shape {
  private width: number;
  private height: number;

  constructor(width: number, height: number) {
    super();
    this.width = width;
    this.height = height;
  }

  area(): number {
    return this.width * this.height;
  }
}

扩展新的形状类

当我们需要增加新的形状,例如三角形时,可以通过扩展现有代码,而无需修改 Shape 类及其现有的子类。

// 具体类:三角形
class Triangle extends Shape {
  private base: number;
  private height: number;

  constructor(base: number, height: number) {
    super();
    this.base = base;
    this.height = height;
  }

  area(): number {
    return 0.5 * this.base * this.height;
  }
}

使用示例

const shapes: Shape[] = [new Circle(10), new Rectangle(5, 6), new Triangle(4, 3)];
shapes.forEach(shape => console.log(shape.area()));
// 输出:314.1592653589793, 30, 6

这种设计方式使得系统具有很好的扩展性,我们可以随时添加新的形状类型,而不会影响到已有的形状类或系统的其他部分。

类图

体现开闭原则的设计模式

  1. 策略模式(Strategy Pattern):策略模式通过将算法封装到独立的策略类中,实现了对扩展开放、对修改封闭的要求。客户端可以在运行时选择具体的策略,而不需要修改已有代码。
  2. 装饰器模式(Decorator Pattern):装饰器模式通过将附加功能封装到装饰器对象中,使得可以在不修改具体组件代码的情况下,动态扩展对象的功能。
  3. 观察者模式(Observer Pattern):观察者模式通过将通知逻辑封装到观察者类中,可以在不修改被观察对象的情况下,动态添加新的观察者,扩展系统功能。
  4. 命令模式(Command Pattern):命令模式通过将请求封装到独立的命令类中,使得可以通过扩展新的命令类,增加新的请求处理功能,无需修改已有的代码。
  5. 工厂模式(Factory Pattern):工厂模式通过使用工厂方法创建对象,使得可以通过扩展工厂类或方法来创建新类型的对象,而不需要修改客户端代码。

典型应用场景

组件渲染

在组件渲染场景中,可以通过扩展新的渲染策略类,实现不同的渲染方案,例如根据设备或浏览器环境选择不同的渲染策略。

class Renderer {
  render(strategy: RenderStrategy, element: HTMLElement): void {
    strategy.render(element);
  }
}

// 抽象策略类
interface RenderStrategy {
  render(element: HTMLElement): void;
}

// 具体策略类:现代渲染
class ModernRender implements RenderStrategy {
  render(element: HTMLElement): void {
    element.innerHTML = "<div class='modern'>Modern Rendering</div>";
  }
}

// 具体策略类:兼容渲染
class LegacyRender implements RenderStrategy {
  render(element: HTMLElement): void {
    element.innerHTML = "<div class='legacy'>Legacy Rendering</div>";
  }
}

// 使用示例
const element = document.getElementById("myElement");
const renderer = new Renderer();
renderer.render(new ModernRender(), element); // 应用现代渲染
renderer.render(new LegacyRender(), element); // 应用兼容渲染

功能模块插拔

在功能模块插拔场景中,可以通过扩展和替换功能模块,实现动态切换功能,如换肤功能、多种排序算法等。

class Sorter {
  sort(strategy: SortStrategy, data: number[]): number[] {
    return strategy.sort(data);
  }
}

// 抽象策略类
interface SortStrategy {
  sort(data: number[]): number[];
}

// 具体策略类:快速排序
class QuickSort implements SortStrategy {
  sort(data: number[]): number[] {
    if (data.length <= 1) return data;
    const pivot = data[Math.floor(data.length / 2)];
    const left = data.filter(x => x < pivot);
    const middle = data.filter(x => x === pivot);
    const right = data.filter(x => x > pivot);
    return [...this.sort(left), ...middle, ...this.sort(right)];
  }
}

// 具体策略类:冒泡排序
class BubbleSort implements SortStrategy {
  sort(data: number[]): number[] {
    const n = data.length;
    for (let i = 0; i < n - 1; i++) {
      for (let j = 0; j < n - 1 - i; j++) {
        if (data[j] > data[j + 1]) {
          [data[j], data[j + 1]] = [data[j + 1], data[j]];
        }
      }
    }
    return data;
  }
}

// 使用示例
const sorter = new Sorter();
const data = [5, 3, 8, 2, 1];

console.log(sorter.sort(new QuickSort(), data)); // 使用快速排序
console.log(sorter.sort(new BubbleSort(), data)); // 使用冒泡排序

第三方库

1. Validator.js

Validator.js 是一个字符串验证库,通过策略模式实现了多种验证算法,如邮箱验证、手机号验证、URL 验证等,体现了开闭原则。

const validator = require('validator');

// 验证示例
console.log(validator.isEmail("[email protected]")); // true
console.log(validator.isMobilePhone("1234567890", "en-US")); // true
console.log(validator.isURL("https://example.com")); // true

2. Day.js

Day.js 是一个轻量级的日期处理库,通过策略模式实现了各种日期格式化和操作算法,可以根据需求灵活使用不同的日期操作,体现了开闭原则。

const dayjs = require('dayjs');

// 示例
console.log(dayjs().format('YYYY-MM-DD')); // 输出当前日期
console.log(dayjs('2022-01-01').add(1, 'month').format('YYYY-MM-DD')); // 2022-02-01
console.log(dayjs('2022-01-01').isBefore('2022-12-31')); // true

总结

开闭原则强调对扩展开放,对修改封闭,通过扩展现有代码来实现新的功能,而不改动已有的代码结构。这种设计原则保证了系统的稳定性和可维护性,避免因为频繁修改代码而引入新的错误。在实际开发中,合理应用开闭原则,可以提高系统的灵活性和可扩展性,适应不断变化的业务需求。

里氏替换原则(Liskov Substitution Principle, LSP)

里氏替换原则指出,子类对象应该能够替换父类对象,并且程序行为不会改变。郭靖的成长历程很好地体现了这一原则。

郭靖最初跟随江南七怪学习武功,江南七怪的武功可以看作是一种 “父类”。随着郭靖的成长,他又先后学习了洪七公的降龙十八掌等更高深的武功,这些可以视为 “子类”。但无论郭靖使用哪种武功,他的侠义之心和为人处世的原则始终不变,就如同在程序中,子类在替换父类后,整体的行为和功能不应发生改变。郭靖在战斗中,时而使用江南七怪所教的武功,时而施展降龙十八掌等绝学,但他所代表的正义、勇敢和坚韧的品质始终如一。

定义

里氏替换原则强调:子类对象应该能够替换父类对象,并且程序行为不会改变。这意味着子类必须能够替代父类,并且能够保持父类的行为和属性。

示例:动物类和其子类

定义抽象类和具体子类

// 基类:动物
class Animal {
  makeSound(): void {
    console.log("Some generic animal sound");
  }
}

// 子类:狗
class Dog extends Animal {
  makeSound(): void {
    console.log("Barking");
  }
}

// 子类:猫
class Cat extends Animal {
  makeSound(): void {
    console.log("Meowing");
  }
}

实现和使用示例

在遵循里氏替换原则的情况下,无论将基类 Animal 替换为什么子类(例如Dog、Cat),程序的行为都应该保持一致。

const animals: Animal[] = [new Dog(), new Cat()];

animals.forEach(animal => animal.makeSound());
// 输出:
// Barking
// Meowing

类图

体现里氏替换原则的设计模式

  1. 策略模式(Strategy Pattern):策略模式通过将算法定义在独立的策略类中,使得可以在运行时互换这些算法。这些策略类通常是一个接口的不同实现,这样可以保证使用不同策略时的行为一致,体现了里氏替换原则。
  2. **装饰器模式(Decorator Pattern):**装饰器模式通过将附加功能封装到装饰器类中,使得可以在不修改具体组件代码的情况下,动态扩展对象的功能。这些装饰器类通常是具体组件类的子类,并且可以相互替换。
  3. **工厂模式(Factory Pattern):**工厂模式通过使用工厂方法创建对象,使得可以通过扩展工厂类或方法来创建新类型的对象,而不需要修改客户端代码。通过提供不同的具体产品类,工厂模式体现了里氏替换原则。
  4. **模板方法模式(Template Method Pattern):**模板方法模式通过将算法的步骤定义在基类中,而将具体实现延迟到子类中,使得子类可以替换父类以实现不同的算法步骤。
  5. **观察者模式(Observer Pattern):**观察者模式通过定义一个观察者接口,使得不同的观察者实现可以相互替换,从而保证在主题通知改变时,不同的观察者能够一致地响应,符合里氏替换原则。

第三方库

RxJS

RxJS 是一个用于异步编程的库,通过 Observable 模式实现了许多功能,其中 Observable 接口和不同的操作符类都能够互相替换,实现了一致的行为,完全遵循里氏替换原则。 示例

import { of } from 'rxjs';
import { map } from 'rxjs/operators';

const obs$ = of(1, 2, 3);
obs$.pipe(map(x => x * 2)).subscribe(console.log); // 输出: 2, 4, 6

Redux

Redux 是一个状态管理库,遵循里氏替换原则,其 Store 和 Reducer 接口可以通过不同的实现方式互相替换,而不会影响系统的整体行为。 示例

import { createStore } from 'redux';

interface Action {
  type: string;
}

const reducer = (state = 0, action: Action) => {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
};

const store = createStore(reducer);

store.dispatch({ type: 'INCREMENT' });
console.log(store.getState()); // 输出: 1

Immutable.js

Immutable.js 提供不可变的数据集合,这些集合类(如 ListMap 等)都遵循里氏替换原则,能够互相替换而不改变系统行为。 示例

import { List } from 'immutable';

const list = List([1, 2, 3, 4, 5]);

list.forEach(value => console.log(value)); // 输出: 1, 2, 3, 4, 5

总结

里氏替换原则强调子类应该能够替换父类,并且程序行为不会改变。这种设计原则保证了系统的稳定性、扩展性和可靠性。通过合理运用里氏替换原则,可以在面向对象设计中构建出灵活、可扩展、易维护的系统。

依赖倒置原则(Dependency Inversion Principle, DIP)

张无忌修炼九阳神功后,他不再依赖于具体的武功招式去克敌制胜,而是依赖于自身强大的内力根基。九阳神功赋予了张无忌深厚的内力,让他在面对各种敌人和复杂情况时,不是直接依赖某种特定的招数,而是通过调动内力,结合对战斗形势的理解,灵活运用各种武功的原理来应对。张无忌深厚内力即为高层模块,所学的乾坤大挪移、太极等武功是底层模块,而他对武学本质的理解即为抽象,能够指导张无忌在不同情况下选择合适的武功来运用内力。

定义

依赖倒置原则强调:

  1. 高层模块不应该依赖于低层模块,两者都应该依赖于抽象。
  2. 抽象不应该依赖于细节,细节应该依赖于抽象。

示例:数据存储

通过将数据存储逻辑定义在抽象接口中,使得高层模块可以依赖于抽象数据存储接口,而不依赖于具体的存储实现类。这样,在需要添加新的存储方式时,只需实现新的存储类,而无须修改高层模块的代码。

接口和实现类

// 接口:数据存储
interface DataStore {
  save(data: string): void;
}

// 实现类:数据库存储
class DatabaseStore implements DataStore {
  save(data: string): void {
    console.log(`Saving ${data} to database`);
  }
}

// 实现类:文件存储
class FileStore implements DataStore {
  save(data: string): void {
    console.log(`Saving ${data} to file`);
  }
}

高层模块

// 高层模块:数据处理器,依赖于抽象的 DataStore
class DataProcessor {
  private store: DataStore;

  constructor(store: DataStore) {
    this.store = store;
  }

  process(data: string): void {
    // 处理数据
    this.store.save(data);
  }
}

使用示例

const databaseStore = new DatabaseStore();
const dataProcessor1 = new DataProcessor(databaseStore);
dataProcessor1.process("data1"); // Saving data1 to database

const fileStore = new FileStore();
const dataProcessor2 = new DataProcessor(fileStore);
dataProcessor2.process("data2"); // Saving data2 to file

类图

体现依赖倒置原则的设计模式

  1. 工厂模式(Factory Pattern):通过使用工厂类来创建实例,而不是在代码中直接 new 一个对象,这样就把对象的创建和使用分离开了,高层模块(使用对象的模块)不依赖于低层模块(对象的具体实现),两者都依赖于抽象(产品接口)。
  2. 策略模式(Strategy Pattern):定义了一系列算法,并将每一种算法封装起来,让它们可以互相替换,此模式使得算法的变化独立于使用算法的客户。
  3. 依赖注入(Dependency Injection,是一种机制,而非严格意义上的设计模式):依赖注入是实现依赖倒置原则的重要方式,它允许创建依赖关系的外部实体,并通过构造函数、属性或方法将对象传递给依赖对象。

前端应用场景

  1. 组件库与框架的解偶:在使用React、Vue等框架时,经常会使用到第三方UI组件库,如Ant Design、Vuetify等。通过抽象组件接口而不是直接依赖具体实现,可以在不改动业务代码的前提下更换组件库。
  2. 数据请求与处理的解耦:前端应用通常需要从服务器获取数据。通过定义抽象的数据服务接口,高层的UI组件不直接依赖于Axios、Fetch等具体实现,便于日后替换请求库或者改动请求逻辑。
  3. 插件系统:在一些复杂的SPA(单页应用)中,可采用插件系统对功能进行拓展。插件和应用核心之间通过定义好的接口交互,确保了应用的核心逻辑与插件的实现之间的独立性。

第三方库

  1. InversifyJS:是一个轻量级的依赖注入容器,用于TypeScript和JavaScript应用,它通过注解来实现依赖的注入,遵循了依赖倒置原则。
  2. Angular的依赖注入(DI)系统:Angular框架内建了一个依赖注入系统,它允许开发者将依赖项作为参数传递给组件或服务的构造函数中,从而实现高低层模块之间的解耦。

总结

依赖倒置原则通过强调高层模块依赖于抽象而不是具体实现,使得系统具有更高的灵活性和可维护性。像张无忌的九阳神功,他通过这门高级内功心法,可以自如地运用各派武功,不依赖于某个门派的基础武学,而是通过九阳神功与各派功夫间接相通。通过合理运用依赖倒置原则,可以在面向对象设计中构建出灵活、可扩展、易维护的系统。

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

接口隔离原则强调客户端不应该依赖它不需要的接口,一个类对另一个类的依赖应该建立在最小的接口上。她知晓天下各派武学理论,能够辨别不同门派的独特功夫,这相当于她拥有访问众多“接口”的能力。虽然精通武林各派理论,但她并不依赖某一门派的功夫,其角色核心在于理论和知识的掌握,并指导他人使用恰当的武功。虽然精通武林各派理论,但她并不依赖某一门派的功夫,其角色核心在于理论和知识的掌握,并指导他人使用恰当的武功。虽然精通武林各派理论,但她并不依赖某一门派的功夫,其角色核心在于理论和知识的掌握,并指导他人使用恰当的武功。

定义

接口隔离原则强调:客户端不应该被迫依赖于它不需要的接口。意思是,接口应该尽量小化,使得类只依赖于它们用到的方法。

示例:工作和吃饭接口

通过定义多个小接口,使得实现类可以选择实现自己真正需要的接口,而不是实现不必要的方法。这个原则帮助我们避免臃肿的接口,提供更细粒度的接口定义。

定义接口和实现类

// 接口:工作接口
interface Workable {
  work(): void;
}

// 接口:可吃饭接口
interface Eatable {
  eat(): void;
}

// 类:人类,实现工作接口和可吃饭接口
class Human implements Workable, Eatable {
  work(): void {
    console.log("Human is working");
  }

  eat(): void {
    console.log("Human is eating");
  }
}

// 类:机器人,只实现工作接口
class Robot implements Workable {
  work(): void {
    console.log("Robot is working");
  }
}

使用示例

const human: Human = new Human();
human.work(); // 输出: Human is working
human.eat(); // 输出: Human is eating

const robot: Robot = new Robot();
robot.work(); // 输出: Robot is working

类图

体现接口隔离原则的设计模式

  1. 策略模式(Strategy Pattern):通过定义一系列的算法,把它们一一封装起来,并使它们可以相互替换。此模式体现了接口隔离原则,因为客户端可以选择用哪种策略而不必关心策略的内部实现,不同策略的实现被隔离在各自的类中。
  2. 命令模式(Command Pattern):命令模式将请求封装为一个对象,从而允许我们根据不同的请求将客户端参数化和传递可调用的方法,并将操作排队或记录操作日志。因为每个命令对应一个执行操作,遵循了接口隔离原则,易于扩展。
  3. 适配器模式(Adapter Pattern):通过包装一个已有的类提供一个新的接口,使原本接口不兼容的类可以一起工作。这里的“新的接口”遵循接口隔离原则,专门为某个客户端服务。

应用场景

  1. 组件化开发:在React、Vue等现代前端框架中,组件化是核心概念之一。每个组件都应该有一个明确、专一的接口,通过Props(React)或Props/Events(Vue)与外界交互,这样做避免了组件依赖于它们不需要的属性或方法。
  2. 服务层设计:在复杂的前端应用中,通常会有一个服务层用于处理数据获取、存储等逻辑。这个服务层会暴露出多个服务,每个服务负责一个单一的职责,而不是创建一个巨大的API类使其变得臃肿不堪。
  3. 插件和中间件机制:在前端框架中使用插件和中间件允许开发者扩展框架的功能。每个插件或中间件只关心它需要实现的功能,而不需要了解系统的其他部分。

第三方库

  1. Lodash:作为一个JavaScript的实用工具库,Lodash提供了许多功能性API,这些API分门别类,每个函数负责一个单一的功能,开发者可以根据需要引入使用,而不是加载整个库。
  2. Redux Middleware:Redux是一个用于JavaScript应用的状态容器,它的中间件系统允许开发者插入自定义逻辑来处理actions或对dispatch的调用,每个中间件负责处理它关心的逻辑部分。

总结

接口隔离原则通过将不同的职责分离成独立的接口,确保客户端只依赖于其真正需要的接口,减少了接口的污染和不必要的依赖。通过合理运用接口隔离原则,可以构建出灵活、易维护且高内聚低耦合的系统。

迪米特法则(Law of Demeter, LoD)

黄药师以神秘莫测著称,他很少与外界过多接触,保持着一种神秘的距离感。迪米特法则就如同黄药师的性格,一个对象应该对其他对象保持最少的了解,减少对象之间的交互。这就像黄药师在江湖中独来独往,不会轻易与他人产生过多的联系。通过迪米特法则,我们可以降低对象之间的耦合度,提高代码的可维护性和可扩展性。

定义

迪米特法则强调:一个对象应该对其他对象有最少的了解。即每一个单位对其他单位的信息只限于那些与本单位密切相关的单位。这意味着对象之间的交互应尽可能少,并且只限于直接的朋友。

示例:汽车和引擎的交互

通过将汽车和引擎的交互封装在汽车类内部,使得客户端只需与汽车类交互,而不直接与引擎类交互,降低了客户端与引擎类的耦合。

类和实现

// 类:引擎
class Engine {
  start(): void {
    console.log("Engine started");
  }
}

// 类:汽车,只与自己直接工作的对象通信
class Car {
  private engine: Engine;

  constructor() {
    this.engine = new Engine();
  }

  startCar(): void {
    this.engine.start();
    console.log("Car started");
  }
}

使用示例

const car = new Car();
car.startCar();
// 输出:
// Engine started
// Car started

类图

体现迪米特法则的设计模式

  1. 外观模式(Facade Pattern):它提供了一个统一的接口,用来访问子系统中的一群接口。外观模式定义了一个高层接口,让子系统更容易使用,客户端通过外观类与子系统的内部复杂性隔离。
  2. 中介者模式(Mediator Pattern):通过创建一个中介对象,减少了各个对象之间的相互作用,使得对象之间紧密耦合变为松散耦合,中介者知道所有的具体类,并负责转发调用,从而实现了迪米特法则。
  3. 观察者模式(Observer Pattern):在此模式中,对象(被观察者)维持一系列依赖于它们的对象(观察者),将有关状态的任何变更自动通知给观察者对象。观察者和被观察者之间的耦合被最小化,观察者不需要了解被观察者内部的细节。

前端应用场景:

  1. 组件与组件库:在使用Vue.js、React等现代前端框架时,组件内部逻辑被封装,外界通过Props(React)或Props/Events(Vue)与组件交互。开发者在使用组件时不需要了解其内部实现,体现了迪米特法则原则。
  2. 状态管理库(Redux/Vuex):在大型前端应用中,状态管理变得复杂。通过使用像Redux这样的库,可以将状态管理逻辑集中在一处,组件只需要知道如何发送动作(actions)和订阅到状态(state),而不需要了解状态是如何被更新和管理的。
  3. Ajax请求封装:前端应用中发起网络请求是常见需求,通过封装HTTP请求的逻辑(例如使用Axios库),组件或者页面只需要调用封装好的函数即可发起请求,无需了解底层网络请求的细节。

第三方库

  1. Axios:作为一个基于Promise的HTTP客户端,为浏览器和node.js提供简单的API,让上层代码不需要直接处理XMLHttpRequest或是Node的http模块,屏蔽了底层细节。
  2. Redux(及类似的Vuex):这些状态管理库让状态的操作和变更逻辑集中管理,组件只需要通过简单的接口进行交互,比如触发action或订阅状态变更,而不需要关心状态是如何变更的,减少了组件对状态变更逻辑的依赖。

总结

迪米特法则通过限制对象之间的直接交互,降低了对象之间的耦合度,提高了系统的可维护性和可扩展性。就像黄药师在江湖中保持神秘的距离感一样,尽量避免与他人产生过多的联系,这保证了他的独立和灵活性。通过迪米特法则,我们可以构建出低耦合、高内聚、易维护的系统。