前端设计模式与原则 | 豆包MarsCode AI刷题

52 阅读5分钟

设计模式

设计模式,是在实践过程中,逐渐提炼出来的一系列可复用的公共逻辑。
而如何提炼出合理,具备可维护性的公共逻辑,就要运用设计原则

单一职责原则

按类来区分,人和球分为两类,不能把球的属性放在人类中。
但是扩展性差,人类按性别分男女,按职业分工程师,产品经理等多个类别。在多对多的关系中扩展困难。

举例

假设我们有一个在线商店系统,其中包含用户(User)和产品(Product)两个类。根据单一职责原则,我们可能会有以下设计:

  • User 类:负责用户信息,如姓名、地址、联系方式等。
  • Product 类:负责产品信息,如名称、价格、库存等。

现在,我们需要添加一个新的功能,允许用户对产品进行评分。根据单一职责原则,我们可能会创建一个新的类 Rating 来处理评分逻辑。但是,这个设计在多对多的关系中(用户可以对多个产品评分,产品可以被多个用户评分)会导致问题:

  • 类的数量增加:我们需要为用户和产品之间的关系创建额外的类,如 UserRating 或 ProductRating,以存储用户对产品的评分。
  • 代码耦合UserProduct 和 Rating 类需要通过复杂的接口来交互,这增加了代码的耦合性。
  • 维护成本:每次需要修改评分逻辑时,可能需要修改多个类,增加了维护的复杂性。
  • 难以重用Rating 类可能只适用于这个特定的评分场景,难以在其他需要评分的上下文中重用。

里氏替换原则

该原则针对的是父类与子类的替换关系,核心是任何使用父类实例的地方,能够使用子类实例完美替换

基础类和子类的定义

首先,我们定义一个基础类 Animal 和两个子类 Dog 和 Bird

class Animal {
    makeSound() {
        console.log("Some generic animal sound");
    }
}

class Dog extends Animal {
    makeSound() {
        console.log("Bark");
    }
}

class Bird extends Animal {
    makeSound() {
        console.log("Chirp");
    }
}

使用父类和子类的实例

接下来,我们定义一个函数 makeAnimalSound,它接受一个 Animal 类型的参数,并调用 makeSound 方法。

function makeAnimalSound(animal) {
    animal.makeSound();
}

根据里氏替换原则,我们可以将 Dog 或 Bird 的实例传递给这个函数,而不会影响程序的正确性。

let myDog = new Dog();
makeAnimalSound(myDog); // 输出 "Bark"

let myBird = new Bird();
makeAnimalSound(myBird); // 输出 "Chirp"

在这个例子中,Dog 和 Bird 的实例可以完美替换 Animal 类型的参数,并且程序的行为符合预期。

违反里氏替换原则的例子

如果我们违反了里氏替换原则,可能会出现问题。例如,如果我们在 Bird 类中添加了一个 fly 方法,而 Animal 类没有这个方法,那么 makeAnimalSound 函数就不再是一个通用的函数,因为它不能处理 fly 方法:

class Bird extends Animal {
    makeSound() {
        console.log("Chirp");
    }

    fly() {
        console.log("Flying");
    }
}

如果 makeAnimalSound 函数被修改为调用 fly 方法,那么它就不能再接受 Dog 的实例,因为 Dog 类没有 fly 方法,这违反了里氏替换原则。

function makeAnimalMove(animal) {
    animal.fly(); // 这将导致错误,因为Dog类没有fly方法
}

依赖倒置原则

高层模块不应该依赖低层模块,两者都应该依赖其抽象
抽象不应该依赖细节,细节应该依赖抽象
概念理解

  1. 高层模块,依赖其他类来实现功能,人看书,人类是高层模块,需依靠 书类 来实现书的功能中展示内容,人类的方法是看
  2. 底层模块,简单的类,不依赖任何其他的类来实现自己的功能。给书类定义一个方法,返回书籍内容
  3. 抽象。人类不仅要书,还要看漫画,看杂志。就会大量地修改 人类。 那么我们抽象出 读物 概念。先定义抽象类,然后定义各自具体的读物,最后定义 人类
    要在现有的代码中新增报纸这种读物,你需要定义一个新的类 Newspaper,它也实现了 Reader 接口。这样,Newspaper 类将有一个 getContent 方法,就像 Book 类一样。然后,你可以在 Person 类的 read 方法中使用 Newspaper 的实例,就像使用 Book 的实例一样。

下面是如何添加 Newspaper 类的示例:

// 定义 Reader 接口
class Reader {
  // 由于 JavaScript 没有接口的概念,我们使用一个抽象类来模拟
  // 这里我们定义一个抽象方法 getContent,具体的子类需要实现这个方法
  getContent() {
    throw new Error("getContent method must be implemented.");
  }
}

class Book implements Reader {
  getContent() {
    return '这是书籍的具体内容';
  }
}

// 定义 Newspaper 类,也实现 Reader 接口
class Newspaper implements Reader {
  getContent() {
    return '这是报纸的具体内容';
  }
}

// 高层模块
class Person {
  // 此时需要传入一个 Reader 的实例,用于获取读物的内容
  read(reader) {
    console.log('我开始阅读了');
    console.log(`内容是:${reader.getContent()}`);
  }
}

// 使用
const book = new Book();
const newspaper = new Newspaper();
const person = new Person();

// 人阅读书籍
person.read(book); // 输出:我开始看书了,内容是:这是书籍的具体内容

// 人阅读报纸
person.read(newspaper); // 输出:我开始阅读了,内容是:这是报纸的具体内容

在这个例子中,Book 和 Newspaper 都实现了 Reader 接口,这意味着它们都有 getContent 方法。Person 类的 read 方法接受一个 Reader 类型的参数,这意味着它可以接收任何 Reader 实现的实例,无论是 Book 还是 Newspaper

这样就可以在代码中灵活地添加更多类型的读物,只要它们实现了 Reader 接口,不需要修改 Person 类就可以支持新的读物类型。