设计模式
设计模式,是在实践过程中,逐渐提炼出来的一系列可复用的公共逻辑。
而如何提炼出合理,具备可维护性的公共逻辑,就要运用设计原则
单一职责原则
按类来区分,人和球分为两类,不能把球的属性放在人类中。
但是扩展性差,人类按性别分男女,按职业分工程师,产品经理等多个类别。在多对多的关系中扩展困难。
举例
假设我们有一个在线商店系统,其中包含用户(User)和产品(Product)两个类。根据单一职责原则,我们可能会有以下设计:
User类:负责用户信息,如姓名、地址、联系方式等。Product类:负责产品信息,如名称、价格、库存等。
现在,我们需要添加一个新的功能,允许用户对产品进行评分。根据单一职责原则,我们可能会创建一个新的类 Rating 来处理评分逻辑。但是,这个设计在多对多的关系中(用户可以对多个产品评分,产品可以被多个用户评分)会导致问题:
- 类的数量增加:我们需要为用户和产品之间的关系创建额外的类,如
UserRating或ProductRating,以存储用户对产品的评分。 - 代码耦合:
User、Product和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方法
}
依赖倒置原则
高层模块不应该依赖低层模块,两者都应该依赖其抽象
抽象不应该依赖细节,细节应该依赖抽象
概念理解
- 高层模块,依赖其他类来实现功能,人看书,人类是高层模块,需依靠 书类 来实现看书的功能中展示内容,人类的方法是看
- 底层模块,简单的类,不依赖任何其他的类来实现自己的功能。给书类定义一个方法,返回书籍内容
- 抽象。人类不仅要看书,还要看漫画,看杂志。就会大量地修改 人类。 那么我们抽象出 读物 概念。先定义抽象类,然后定义各自具体的读物,最后定义 人类
要在现有的代码中新增报纸这种读物,你需要定义一个新的类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 类就可以支持新的读物类型。