Skip to content

设计原则

设计原则(Design Principles)指的是那些在软件开发中,特别是在设计软件组件时应该遵循的指导原则。这些原则旨在提高代码的可维护性、可扩展性和灵活性。

单一职责原则(SRP)

一个类(或者接口、方案)应该只做一件事,只有一个变化的原因。

单一职责原则的重点在于职责的划分,很多时候并不是一成不变的,需要根据实际情况而定。

我们在设计一个类的时候,可以先从粗粒度的类开始设计,等到业务发展到一定规模,如果发现这个粗粒度的类方法和属性太多,且经常修改的时候,我们就可以对这个类进行重构了,将这个类拆分成粒度更细的类。

开放封闭原则(OCP)

类应该是开放的,可以扩展,而不能修改(也就是说对扩展开放,对更改封闭;修改意味着更改现有类的代码,扩展意味着添加新功能)。

那么如何在不触及类的情况下添加新功能呢?它通常是在接口和抽象类的帮助下完成的。

例如,我们需要设计一个图形类库,里面有圆形、正方形等形状:

typescript
class Shape {
  draw() {
    // 绘制圆形、正方形等形状的代码
  }
}
class Shape {
  draw() {
    // 绘制圆形、正方形等形状的代码
  }
}

如果要添加新的形状,就需要修改这个类。我们可以将绘图方法定义为抽象方法,让子类去实现:

typescript
abstract class Shape {
  abstract draw();
}

class Circle extends Shape {
  draw() {
    console.log('画圆');
  }
}

class Square extends Shape {
  draw() {
    console.log('画正方形');
  }
}
abstract class Shape {
  abstract draw();
}

class Circle extends Shape {
  draw() {
    console.log('画圆');
  }
}

class Square extends Shape {
  draw() {
    console.log('画正方形');
  }
}

Liskov 替换原则(LSP)

里氏替换原则指出,子类应该可以替代其基类。

也就是说,所有父类能出现的地方,子类就可以出现,并且替换了也不会出现任何错误。

接口隔离原则(ISP)

原则表面客户端不应该强迫依赖它不需要的接口,类间的依赖关系应该建立在最小的接口上。

例如,如果一个接口提供了很多方法,但是一个类只需要用到其中的几个方法,那么可以将这个接口拆分成多个更小的接口。

我们经常会给别人提供服务,而服务调用方可能有很多个。很多时候我们会提供一个统一的接口给不同的调用方,但有些时候调用方 A 只使用 1、2、3 三个方法,调用方 B 只使用 4、5 两个方法。接口隔离原则的意思是,你应该把 1、2、3 拆为为一个接口,4、5 拆为一个接口,这样接口之间就隔离开来了。

依赖倒置原则(DIP)

高层模块不应该依赖底层模块,两者都应该依赖其抽象。抽象不应该依赖细节,即接口或抽象类不依赖于实现类。细节应该依赖抽象,即实现类不应该依赖于接口或抽象类。

如果一个类依赖于另一个类的具体实现,那么当这个依赖的具体实现发生变化时,就需要修改原来的类。我们可以将这两个类都抽象为接口或基类,让它们之间只依赖于它们的共同点(即共同的接口或基类)。

typescript
class B {
  doSomething() {
    console.log('B');
  }
}

class A {
  constructor(b: B) {
    this.b = b;
  }

  fun() {
    return this.b.doSomething();
  }
}
class B {
  doSomething() {
    console.log('B');
  }
}

class A {
  constructor(b: B) {
    this.b = b;
  }

  fun() {
    return this.b.doSomething();
  }
}

如果要替换 B 的实现,就需要修改 A 类。我们可以将 B 抽象为接口:

typescript
interface BInterface {
  doSomething(): void;
}

class B implements BInterface {
  doSomething() {
    console.log('B');
  }
}

class C implements BInterface {
  doSomething() {
    console.log('C');
  }
}

class A {
  constructor(b: BInterface) {
    this.b = b;
  }

  fun() {
    return this.b.doSomething();
  }
}
interface BInterface {
  doSomething(): void;
}

class B implements BInterface {
  doSomething() {
    console.log('B');
  }
}

class C implements BInterface {
  doSomething() {
    console.log('C');
  }
}

class A {
  constructor(b: BInterface) {
    this.b = b;
  }

  fun() {
    return this.b.doSomething();
  }
}

这样,A 类就不需要依赖于 B 类的具体实现了。如果要替换 B 的实现,只需要将 A 构造函数的参数改为 C 即可。

迪米特法则(LoD)

迪米特法则也叫最少知识原则,就是说一个对象应该对其他对象有尽可能少的了解。

该原则要求对象只与其直接的关系对象进行交互,避免暴露过多的信息给外部对象,从而减少对象之间的依赖关系。旨在降低软件模块之间的耦合度,增强模块的独立性和可复用性。

在迪米特法则中,对于一个对象,其朋友包括以下几类:

  • 当前对象本身(this);
  • 以参数形式传入到当前对象方法中的对象;
  • 当前对象的成员对象;
  • 如果当前对象的成员对象是一个集合,那么集合中的元素也都是朋友;
  • 当前对象所创建的对象。

任何一个对象,如果满足上面的条件之一,就是当前对象的“朋友”,否则就是“陌生人”。在应用迪米特法则时,一个对象只能与直接朋友发生交互,不要与“陌生人”发生直接交互。

在买房子这件事情上,中介者(中介公司)就是一个迪米特法则的典型应用。中介者充当了两个对象之间的中介,使得这两个对象不需要直接交互。如果中介者不介入,买家和卖家就需要直接交互,这样买家就知道了卖家的很多信息,违背了迪米特法则。

合成复用原则(CRP)

优先使用对象组合,而不是继承来达到复用的目的。

在面向对象的编程中,继承是用来表示类之间的“is-a”关系,而合成(或称为聚合)是用来表示类之间的“has-a”关系。如果一个类可以重用另一个类的代码,那么应该优先考虑使用合成而不是继承。

  • 类继承通常为“白箱复用”,对象组合通常为“黑箱复用”;
  • 继承在某种程度上破坏了封装性,子类父类耦合度高;
  • 而对象组合只要求被组合的对象具有良好定义的接口,耦合度低。

例如,如果要设计一个图形编辑器,可以使用合成原则来实现:

typescript
interface Shape {
  draw(): void;
}

class Rectangle implements Shape {
  draw() {
    console.log('绘制矩形');
  }
}

class Circle implements Shape {
  draw() {
    console.log('绘制圆形');
  }
}

class GraphicEditor {
  shapes: Shape[] = [];

  addShape(shape: Shape) {
    this.shapes.push(shape);
  }

  drawAllShapes() {
    for (const shape of this.shapes) {
      shape.draw();
    }
  }
}
interface Shape {
  draw(): void;
}

class Rectangle implements Shape {
  draw() {
    console.log('绘制矩形');
  }
}

class Circle implements Shape {
  draw() {
    console.log('绘制圆形');
  }
}

class GraphicEditor {
  shapes: Shape[] = [];

  addShape(shape: Shape) {
    this.shapes.push(shape);
  }

  drawAllShapes() {
    for (const shape of this.shapes) {
      shape.draw();
    }
  }
}

这样,GraphicEditor 类就可以在不修改的情况下添加新的图形类了。

其它建议

高内聚松耦合

  • 使用封装来创建对象之间的分界层,让设计者可以在分界层的一侧进行修改,而不会对另一侧产生不良的影响,从而实现层次间的松耦合

针对接口编程,而不是针对实现编程

  • 不将变量类型声明为某个特定的具体类,而是声明为某个接口
  • 客户程序无需获知对象的具体类型,只需要知道对象所具有的接口
  • 减少系统中各部分的依赖关系,从而实现,“高内聚,松耦合”的类型设计方案

参考

Developed by Kisstar & Powered by VitePress.