软件设计的一些原则

开闭原则

释义

  对扩展开放,对修改封闭。开闭原则(Open-Closed Principle)强调的是用抽象构建框架,用实现扩展细节,它的核心是面向抽象编程,遵守开闭原则可以提高软件系统的可复用性以及维护性。

例子

  SpringMVC使用HandlerArgumentResolver来解析@RequestMapping标注的方法的参数,常用的@RequestParam@RequestBody@PathVariable等注解均是由其背后的参数解析器提供支持。如果有比较特殊的解析规则,或者想支持自定义的注解,只需要新增一个策略类实现HandlerArgumentResolver接口即可,而不需要修改SpringMVC的源码。

依赖倒置原则

释义

  高层模块(稳定)不应该依赖低层模块(变化),二者都依赖抽象(稳定);抽象(稳定)不应该依赖于实现细节(变化),实现细节应该依赖于抽象(稳定)。依赖倒置原则(Dependency Inversion Principle)强调面向接口编程,不要面向实现编程,遵守依赖倒置原则可以减少类间的耦合、提高系统稳定性,同时提高代码可读性和可维护性。

例子

  Spring中的ApplicationContextBeanFactoryApplicationContextBeanFactory的基础上提供了事件发布、国际化等功能。实现细节上,ApplicationContext并不依赖DefaultListableBeanFactory这个具体实现,而是依赖于BeanFactory这个共同的契约。

单一职责原则

释义

   不要存在多于一个导致类变更的理由。单一职责原则(Single Responsibility Principle)是说一个类应该只有一个发生变化的原因,即一个类只负责一项职责。如果一个类拥有多于一个的职责,则这些职责就耦合到了一起,那么就会有多于一个原因来导致这个类发生变化。对于某一职责的更改可能会损害类满足其他耦合职责的能力。这样职责的耦合会导致设计的脆弱,以至于当职责发生更改时产生无法预期的破坏,因此单一职责原则的核心是解耦和增强内聚性。

例子

  Spring的核心概念BeanFactory的继承体系。注意这里虽然最终都耦合到了DefaultListableBeanFactory这个实现上,但所有其他需要使用DefaultListableBeanFactory类的地方已经可以被接口进行隔离,我们仅需依赖所定义的单一职责的接口。而DefaultListableBeanFactory仅在被实例化的位置才会出现。我们将丑陋的代码限制在一定的范围内,而不会泄露或污染应用程序的其他部分。

接口隔离原则

释义

  用多个专门的接口,而不使用单一的总接口,客户端不应该依赖它不需要的接口。接口分离原则(Interface Segregation Principle)用于处理胖接口(fat interface)所带来的问题。如果类的接口定义暴露了过多的行为,则说明这个类的接口定义内聚程度不够好。换句话说,类的接口可以被分解为多组功能函数的组合,每一组都服务于不同的客户类,而不同的客户类可以选择使用不同的功能分组。

例子

  Spring-Context提供的事件发布、国际化和资源加载等功能虽然可以统一通过ApplicationContext来暴露,更好的做法是通过ApplicaitonEventPublisherAwareMessageSourceAwareResourcePatternResolverAware等一系列*Aware接口来获取。

迪米特法则/最少知识原则

释义

  类应该与其协作类进行交互但无需了解它们的内部结构。具体点就是,对象应尽可能地避免调用由另一个方法返回的对象的方法。迪米特法则(Law of Demeter)或者说最少知识原则(Least Knowledge Principle),关注点在降低类与类之间的耦合,从而提升代码的可读性和可维护性。

例子

  不符合迪米特法则。

1
2
3
4
5
6
7
8
9
10
/**
* 不仅需要了解 Server,还需要了解 EmailSystem 以及 SenderSubsystem
* 这几个 API 任意有一个发生变化,都会导致 sendSupportEmail(...) 需要
* 修改
*/
public void sendSupportEmail(Server server, String message, String toAddress) {
EmailSystem emailSystem = server.getEmailSystem();
String fromAddress = emailSystem.getFromAddress();
emailSystem.getSenderSubsystem().send(fromAddress, toAddress, message);
}

调整后使其符合迪米特法则。

1
2
3
4
5
6
/**
* 只需要了解 SenderSubsystem 即可,对应到代码里就是只需要使用一次 . 语法
*/
public void sendSupportEmail(SenderSubsystem sender, String message, String fromAddress, String toAddress) {
sender.send(fromAddress, toAddress, message);
}

里氏替换原则

释义

  任何父类可以出现的地方,都可以安全地使用其子类进行替换;只有当子类能够完全替代它们的基类时,使用基类的函数才能够被安全的重用,然后子类也可以被放心的修改了。里氏替换原则(The Liskov Substitution Principle)对继承进行了约束,是实现开闭原则的重要方式。

例子

  不满足里氏替换原则。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
class Rectangle {
private double width;
private double height;

public double getWidth() {
return width;
}

public void setWidth(double width) {
this.width = width;
}

public double getHeight() {
return height;
}

public void setHeight(double height) {
this.height = height;
}
}

class Square extends Rectangle {

@Override
public void setWidth(double width) {
super.setWidth(width);
super.setHeight(width);
}

@Override
public void setHeight(double height) {
super.setHeight(height);
super.setWidth(height);
}
}

class Main {
public static void main(String[] args) {
Rectangle rect = new Square();
rect.setWidth(4);
rect.setHeight(5);
// 断言失败
// 对长方形来说,它的宽高之间是没有依赖的
// 而正方形不是,它的宽高是有依赖关系的
Assert.isTrue(rect.getWidth() * rect.getHeight() == 20);
}
}

当通过基类接口使用对象时,客户类仅知道基类的前置条件和后置条件。因此,子类对象不能期待客户类服从强于基类中的前置条件。也就是说,它们必须接受任何基类可以接受的条件。而且,子类必须符合基类中所定义的后置条件。也就是说,它们的行为和输出不能违背任何已经与基类建立的限制。

Reference


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!