前言
设计模式七大原则是用来指导面向对象设计的。其中的前5大,大家应该都比较熟悉了
SOLID的是有关面向对象设计的5个原则的首字母缩略词。它包含
- Single-responsibility principle
- Open–closed principle
- Liskov substitution principle
- Interface segregation principle
- Dependency inversion principle
除了这些还有迪米特法则、合成复用原则。
遵循以上原则,有利于写出易懂,灵活,可维护性高的代码。下面来逐个介绍
Single-responsibility principle
单一职责原则:一个类只负责一件事,只有一个引起它变化的原因。
如果一个类型,负责多件事,那么它的每个职责在修改时,都会修改这个类。而修改,就有可能带来bug。
而且,如果其它的类只需要它的某个功能,但继承了它,就会连那些不需要的功能一起继承了,也可能带来bug。
Open–closed principle
开闭原则:软件实体(比如class)应该对扩展开放,对修改封闭
具体一点,它的意思是应该可以通过新增代码来给类实现新的功能,而且尽量避免对原有代码的修改。
这里有一个例子
class Screen {
drawShapes(shapes: Shape[]): void {
for (const shape of shapes) {
if (shape instanceof Circle) {
drawCircle(shape);
} else if (shape instanceof Rectangle) {
drawRectangle(shape);
} else {
throw new Error("Unknown shape!");
}
}
}
}
如果想给drawShapes
新增画三角形的功能,就得新增一个else if,违反了此原则。应该改成这样:
class Screen {
drawShapes(shapes: Shape[]): void {
for (const shape of shapes) {
shape.draw();
}
}
}
其中,每种shape的draw方法都由它自己实现。这样避免了Screen和具体shape的耦合。新增shape时,不需要修改这个Screen::drawShapes里的代码,它就能支持新的shape的绘制。
下面再举一个例子
blinker.readthedocs.io/en/stable/
这是python里的一个library。里面有个signal的使用,就遵循OCP。
开发者可以把每种事件,抽象成一个signal。然后给这个signal添加回调函数。
def log_on_event(event: Event):
logger.info("Clicker at {}, {}".format(event.x, event.y))
def get_file(event: Event):
requests.get(event.url)
on_click = signal("on_click")
on_click.connect(log_on_event)
on_click.connect(get_file)
on_click.send(Event(1, 2, "http://localhost:5000"))
如果想给on_click增加新的处理逻辑,只要新增on_click.connect(SOME_HANDLER)
就行了。
Liskov substitution principle
里式替换原则:父类应当可以用子类替换。
也就是,父类能够出现的地方,子类一定能够出现。
这个原则的一个作用是来帮助开发者判断一个继承关系是否合理的。
这里有一个著名的问题
Circle-ellipse problem
。理论上,圆是椭圆的一种,当椭圆的长轴和短轴相等时,这个椭圆就成了圆。
但其实在面向对象设计中,圆继承椭圆是不合适的。
class Ellipse {
constructor(public x: number, public y: number) {
}
setX(x: number) {
this.x = x;
}
setY(y: number) {
this.y = y;
}
}
如果一个Circle
类继承了Ellipse
,setX
和setY
,无论怎么实现都是不合适的。原本接受Ellipse
的代码,如果换成了Circle
,那么代码就可能出错。
Interface segregation principle
接口隔离原则:不应强制客户端依赖它们不需要的接口
这个原则有点像单一职责原则的“接口版本”。举个例子
interface ParkingLot {
parkCar();
doPayment(car: Car);
}
这个接口表示停车场,其中有两个方法,一个停车,一个支付。
这里的问题,有个停车场免费,所以没有doPayment
方法。
解决此问题的一个方法是拆成这两个接口。
interface ParkingLot {
parkCar();
}
interface ParkingLotPayment {
doPayment(car: Car);
}
免费停车场只需实现ParkingLot
,付费停车场需要实现以上2个接口。
Dependency inversion principle
依赖倒置原则:类应该依赖接口或者抽象类,而不是具体类和函数。
再次考虑我在Open–closed principle
中举的Screen
例子。其实,在这个例子中,我们是按照DIP来改造了Screen
类。同时也让这个类遵循了OCP。通过这个例子,我们可以发现,OCP和DIP有着紧密的联系。
DIP是一个实现OCP的方法,但不是唯一的方法。
Law of Demeteror (or principle of least knowledge)
迪米特法则,又称最小知道原则,它的概念是一个对象应该对其他对象保持最少的了解。它是一种让面向对象设计实现低耦合高内聚的方法。
举个例子
class Client {
orderTakeout(foodService: FoodService, foodName: string) {
const cook = foodService.getCook();
const food = cook.cook(foodName);
return food;
}
}
这段代码是没有遵循LoD的。因为client不应该与cook交互。如果foodService获取事务的逻辑不再是以上这个逻辑,那client也得改代码。
不如改成以下形式
class Client {
orderTakeout(foodService: FoodService, foodName: string) {
const food = foodService.getFood();
return food;
}
}
这种形式中,Client类保持了对FoodSerivice的最小了解,职责变得简单了。foodSerice获取食物的逻辑无论怎样更改,都不需要修改client的代码。
Composite Reuse Principle (or composition over inheritance)
合成复用原则(又称为组合优于继承):使用组合来实现代码复用和多态,而不是继承。
继承虽然很常用,但是也有一些问题。最近几年流行的新语言,有些不支持继承,比如go, rust。
这里以继承和组合两种方式来实现同一个逻辑,来展示组合比继承好在哪里。
这个例子有点像我在接口隔离原则里提到的。
abstract class IBird {
eat() {
console.log("Eat food");
};
fly(): void {
console.log("Flying.");
}
}
class Ostrich extends IBird {
fly() {
throw new Error("Not implemented");
}
}
这种方法的问题,子类Ostrich继承了它完全不需要的方法。这个问题,如果仍要使用继承来解决,也是能解决的,但是比较麻烦
如果把鸟会不会跑、会不会叫,等问题考虑进来,那继承关系更复杂了。假设鸟类拥有5种能力,这5种能力的组合高达2^5=32种,每一种都用1个类或者接口来表示就太麻烦了。
继承一个问题在于:继承层次过深、继承关系过于复杂时会影响到代码的可读性和可维护性。
下面,把以上的逻辑使用组合来表示。
把吃食物、飞翔的能力,作为对象,让各种鸟类引用这些对象,来表示它们拥有这些能力。
interface IFlyAbility {
fly(): void;
}
interface IEatAbility {
eat(): void;
}
class Pigeon {
flyAbility: IFlyAbility;
eatAbility: IEatAbility;
fly() {
this.flyAbility.fly();
}
eat() {
this.eatAbility.eat();
}
}
class Ostrich {
eatAbility: IEatAbility;
eat() {
this.eatAbility.eat();
}
}
以上代码有以下优势
- 继承关系简单。如果需要再增加一种能力,也只需要新增一个能力接口和一个能力实现即可。
- 灵活。类的继承关系在编译期就已经确定了,它的任意一种行为也是确定的。但组合关系中,一个对象可以随便改变它引用的对象,这样可以在运行时改变它的某个行为的实现。
总结
学习了以上几种原则,我对低耦合高内聚有了更深刻的认识。
SIP体现了高内聚。
OCP,DIP,LoD体现了降低高层模块与底层模块的耦合。
组合优于继承,体现了避免继承关系带来的耦合。
以上原则也不是完美的。例如在一个项目的实现方式(例如使用哪个数据库)很少改动的情况下,遵循DIP会使代码变得啰嗦。在明确一个类不会被继承的情况下,而且项目较小的情况下,遵守SRP也会使代码变得啰嗦。总之,应该灵活地去选择和使用。