合成复用设计原则(Composition over Inheritance)
鼓励使用对象组合而不是继承来实现代码重用和灵活性。
特点
-
该原则强调利用对象之间的组合关系,而不是通过继承类的方式来实现代码的重用。
-
通过将功能模块化为独立的组件,并通过组合这些组件来创建更复杂的功能,可以提供更大的灵活性和可维护性。
-
相比于继承,组合更加灵活,因为它允许在运行时动态地改变对象之间的关系。
优点
-
降低耦合度:使用组合可以减少类之间的紧密耦合,使得系统更加灵活、可扩展和易于维护。
-
提高代码复用性:通过组合已有的组件来构建新的功能,可以避免继承层次过深导致的复杂性和代码重复。
-
更好的模块化:将功能分解为独立的组件可以更好地组织和管理代码,使得系统更容易理解和修改。
-
动态变化:通过组合,可以在运行时动态地改变对象之间的关系,从而实现更灵活的行为。
总而言之,合成复用设计原则鼓励使用组合而不是继承来实现代码重用和灵活性,从而提高软件系统的可维护性和可扩展性。
示例
下面的代码展示了遵守和不遵守合成复用设计原则;它们实现的是相同的功能:
不遵守合成复用设计原则的代码:
class Vehicle {
startEngine() {
console.log("Starting engine...");
}
}
class Car extends Vehicle {
drive() {
this.startEngine();
console.log("Driving the car...");
}
}
class Motorcycle extends Vehicle {
ride() {
this.startEngine();
console.log("Riding the motorcycle...");
}
}
为什么没有遵守合成复用设计原则:
在这个例子中,Car 和 Motorcycle 类都通过继承 Vehicle 类来获得 startEngine() 方法。然而,这种设计方式存在一些问题:
-
Car 和 Motorcycle 类与 Vehicle 类之间具有紧密的耦合关系,因为它们直接依赖于 Vehicle 类的实现细节。
-
如果需要改变 startEngine() 方法的实现,那么所有子类都会受到影响。
-
继承关系限制了代码的灵活性,例如无法将其他类型的引擎组件用于 Car 或 Motorcycle。
遵守合成复用设计原则的代码:
class Engine {
start() {
console.log("Starting engine...");
}
}
class Car {
private engine: Engine;
constructor(engine: Engine) {
this.engine = engine;
}
drive() {
this.engine.start();
console.log("Driving the car...");
}
}
class Motorcycle {
private engine: Engine;
constructor(engine: Engine) {
this.engine = engine;
}
ride() {
this.engine.start();
console.log("Riding the motorcycle...");
}
}
为什么遵守了合成复用设计原则:
-
在这个例子中,Car 和 Motorcycle 类都通过组合的方式使用了 Engine 类。
-
它们不再继承 Vehicle 类,而是将引擎作为构造函数的参数,并在需要时调用引擎的 start() 方法。
迪米特设计原则(Law of Demeter,也称为最少知识原则)
面向对象设计中的一个原则,旨在降低对象之间的耦合度,提高系统的灵活性和可维护性。
特点:一个对象应该尽可能少地了解其他对象的内部结构和实现细节
- 一个对象应该只与其直接的朋友进行交互,而不与非直接的类进行通信。直接的朋友包括以下几种情况:
- 当前对象本身;
- 作为方法参数传递进来的对象;
- 当前对象的成员变量所引用的其他对象。
-
避免通过链式调用过多层次的方法来访问另一个对象的内部状态。
目标
保持对象的独立性和封装性,使得系统的各个对象之间松耦合,减少对其他对象的依赖关系。
作用
-
可以提高代码的可维护性和可复用性,同时降低系统的风险和复杂性。
-
遵循迪米特设计原则有助于提高代码的可读性和可理解性,因为每个对象只需要关注与自己相关的部分,而不需要了解整个系统的内部结构。
-
当系统需要进行修改时,减少了对象之间的依赖关系,可以降低变更带来的影响范围,使得系统更易于维护和扩展。
示例
下面的代码展示了遵守和不遵守迪米特设计原则;它们实现的是相同的功能:
不遵守迪米特设计原则的代码:
class Company {
private employees: Employee[];
constructor() {
this.employees = [];
}
hireEmployee(employee: Employee) {
this.employees.push(employee);
// 其他与员工相关的操作
}
getEmployees(): Employee[] {
return this.employees;
}
}
class Employee {
private name: string;
constructor(name: string) {
this.name = name;
}
getName(): string {
return this.name;
}
}
class Client {
private company: Company;
constructor(company: Company) {
this.company = company;
}
printAllEmployees() {
const employees = this.company.getEmployees();
for (const employee of employees) {
console.log(employee.getName());
}
}
}
为什么没有遵守迪米特设计原则:
-
在这个例子中,Client 类需要打印所有公司的员工姓名。
-
然而,Client 类通过 Company 类来获取所有员工,并直接访问员工的名称。这种设计方式存在一些问题:
- Client 类知道太多关于 Company 类的内部结构,包括如何获取员工和员工的名称。
- 如果 Company 类的内部结构发生变化,例如使用不同的数据结构存储员工信息,那么 Client 类也需要相应地进行修改。
遵守迪米特设计原则的代码:
class Company {
private employees: Employee[];
constructor() {
this.employees = [];
}
hireEmployee(employee: Employee) {
this.employees.push(employee);
// 其他与员工相关的操作
}
getEmployees(): Employee[] {
return this.employees;
}
}
class Employee {
private name: string;
constructor(name: string) {
this.name = name;
}
getName(): string {
return this.name;
}
}
class Client {
private company: Company;
constructor(company: Company) {
this.company = company;
}
printAllEmployees() {
const employees = this.company.getEmployees();
for (const employee of employees) {
this.printEmployeeName(employee);
}
}
private printEmployeeName(employee: Employee) {
console.log(employee.getName());
}
}
为什么遵守了迪米特设计原则:
-
在这个例子中,Client 类不再直接访问 Company 类的内部结构。
-
而是通过调用 Company 类提供的 getEmployees() 方法获取所有员工,然后将打印姓名的逻辑封装到私有方法 printEmployeeName() 中。
- Client 类减少了对 Company 类的依赖,只与其直接的朋友进行交互。
- 如果 Company 类的内部结构发生变化,Client 类无需修改,因为它只依赖于 Company 类提供的公共接口。
- Client 类可以更加专注于自身的职责,而不需要了解和处理过多的内部细节。