文章内容
说到面向对象设计的 SOLID 原则,相信大部分的程序员都有个大概的印象,但具体是做什么的,要怎么做可能不太清楚,就带大家一起来看看SOLID原则以及它们之间的联系。
一、基本概念
面向对象设计领域,SOLID是由罗伯特·C·马丁在21世纪早期引入的记忆术首字母缩略字,代表面向对象编程和面向对象设计的五个基本原则。当这些原则被一起应用时,它们让设计和开发一个容易维护和可扩展的系统变得更加容易。
缩写 | 简称 | 全称 |
---|---|---|
S | 单一责任原则(SRP) | Single Responsibility Principle |
O | 开放封闭原则(OCP) | Open-Closed Principle |
L | 里式替换原则(LSP) | Liskov Substitution Principle |
I | 接口分离原则(ISP) | Interface Segregation Principle |
D | 依赖倒置原则(DIP) | Dependency Inversion Principle |
二、单一责任原则(S)
A class should have one and only one reason to change
单一职责原则(Single Responsibility Principle, SRP):一个类应该有且只有一个引起它变化的原因。简单地说:接口职责应该单一,不要承担过多的职责。
职责,可以理解为”引起变化的原因”。往往我们需要修改代码以应对需求变更,如果职责越多,职责间存在耦合越大,意味着可能潜在的变化点越多,变化的概率和风险越大,导致实现改变的需求难度越大。职责过多怎么办?拆!拆!拆! 如果在设计过程中发现一个类承担的职责太多,最直接有效的解决方式就是按职责 “拆分”。
上图是不是很形象,接下来通过代码的方式举例说明:
public class Users {
// 下单
public void order(){}
// 支付
public void pay(){}
// 日志
public void logger(){}
}
在这个用户类中包含这三个功能职责:1.下单逻辑 2.支付逻辑 3.日志操作。若将这三个功能结合在一个类中,可能会出现修改部分代码时会破坏其他部分。多个功能也使这个用户类难以理解,降低了内聚性,所以最好就是将这个类分离为三个分离的类,每个类仅仅有一个功能。拆分后的结果如下:
// 下单
public class UserOrder {}
// 支付
public class UserPay {}
// 日志
public class UserLogger {}
三、开放封闭原则(O)
Objects or entities should be open for extension, but closed for modification.
开放封闭原则(Open Closed Principle,OCP)的定义是:一个软件实体,如类、模块和函数应该对扩展开放,对修改关闭。允许通过扩展的方式增加系统行为,禁止通过直接修改源码的方式增加系统行为。简单地说:就是当别人要修改软件功能的时候,他不能修改我们原有代码,只能新增代码实现软件功能修改的目的。
接下来通过代码的方式举例说明:
if (type == apple) {
//deal with apple
} else if (type == banana) {
//deal with banana
} else if (type == ......) {
//......
}
这段代码模拟的是不同水果的处理程序,每种水果有不同的处理方式,如果后续还需要处理其他水果,那么就会在后面加上很多的 if else 语句,最终会让整个方法变得又臭又长。如果恰好这个水果中的不同品种有不同的剥皮方法,那么这里面又会有很多层嵌套。
可以看出并没有满足「对扩展开放,对修改封闭」的原则。如果我们对处理水果动作做一个抽象,每种水果分别单独有自己具体的实现,那么写出的代码就是这样的:
public interface PeelOff {
void peelOff();
}
public class ApplePeelOff implement PeelOff {
void peelOff() {}
}
public class BananaPeelOff implement PeelOff {
void peelOff() {}
}
public class PeelOffFactory {
private Map<String, PeelOff> map = new HashMap();
private init() {}
}
public static void main(){
String type = "apple";
PeelOff peelOff = PeelOffFactory.getPeelOff(type);
peelOff.pealOff();
}
四、里式替换原则(L)
Let q(x) be a property provable about objects of x of type T. Then q(y) should be provable for objects y of type S where S is a subtype of T.
里氏替换原则(Liskov Substitution Principle,LSP)的定义是:所有引用基类的地方必须能透明地使用其子类的对象。简单地说:所有父类能出现的地方,子类就可以出现,并且替换了也不会出现任何错误。
接下来通过代码的方式举例说明:例如下面Parent类出现的地方,可以替换成Son类,其中Son是Parent的子类。这就要求子类的所有相同方法,都必须遵循父类的约定,否则当父类替换为子类时就会出错。这里父类的约定,不仅仅指的是语法层面上的约定,还包括实现上的约定。
public class Parent {
public void hello throw NullPointerException() {
}
}
public class Son extends Parent {
public void hello throw NullPointerException() {
}
}
Parent obj = new Son();
// 等价于
Son son = new Son();
五、接口分离原则(I)
A client should never be forced to implement an interface that it doesn't use or clients shouldn't be forced to depend on methods they do not use.
接口分离原则(Interface Segregation Principle,ISP)的定义是:类间的依赖关系应该建立在最小的接口上。简单地说:接口的内容一定要尽可能地小,能有多小就多小。
这个原则比较简单,举个例子来说,我们对外提供API接口服务,调用方可能有很多个,很多时候我们会提供一个大而全的接口给不同的调用方,但有些时候调用方A只使用1、2、3 这三个方法,调用方B只使用4、5两个方法,其他都不用。接口隔离原则的意思是,你应该把 1、2、3 抽离出来作为一个接口,4、5 抽离出来作为一个接口,这样接口之间就隔离开来了。
这样做也是为了更好地隔离变化,降低内部改动导致的风险。在软件设计中,ISP提倡不要将一个大而全的接口扔给使用者,而是将每个使用者关注的接口进行隔离,分别提供不同的接口服务。
六、依赖倒置原则(D)
Entities must depend on abstractions not on concretions. It states that the high level module must not depend on the low level module, but they should depend on abstractions.
依赖倒置原则(Dependency Inversion Principle,DIP)的定义:高层模块不应该依赖底层模块,两者都应该依赖其抽象。抽象不应该依赖细节,即接口或抽象类不依赖于实现类,细节应该依赖抽象,即实现类不应该依赖于接口或抽象类。简单地说,就是说我们应该面向接口编程,通过抽象成接口,使各个类的实现彼此独立,实现类之间的松耦合。
为了遵循这一原则,我们需要使用一种设计模式称为依赖注入,典型的依赖注入通过类的构造函数作为输入参数。在Post类中通过使用依赖注入,我们不再依赖Post类自己来定义指定类型的日志。
public class Post {
private Logger _logger;
public Post(Logger injectedLogger) {
_logger = injectedLogger;
}
public void CreatePost(Database db, string postMessage) {
try {
db.Add(postMessage);
}
catch (Exception ex) {
_logger.log(ex.ToString());
}
}
}
七、SOLID原则的关系
讲完每个原则后,是不是好像懂了,但好像又什么都没记住?来看看这几个原则之间的关系。ThoughtWorks上有一篇文章说得挺不错:
单一职责是所有设计原则的基础,开闭原则是设计的终极目标。里氏替换原则强调的是子类替换父类后程序运行时的正确性,它用来帮助实现开闭原则。而接口隔离原则用来帮助实现里氏替换原则,同时它也体现了单一职责。依赖倒置原则是过程式编程与面向对象编程的分水岭,同时它也被用来指导接口隔离原则。
简单地说:依赖倒置原则告诉我们要面向接口编程,当面向接口编程之后,接口隔离原则和单一职责原则又告诉我们要注意职责的划分,不要什么东西都混杂在一起,当我们职责划分清楚后,里氏替换原则告诉我们在使用继承时,要注意遵守父类的约定,而依赖倒置、接口隔离、单一职责、里氏替换的最终目标都是为了实现开闭原则。
八、总结
面向对象设计的原则有很多,在实际的设计过程中完全遵循所有的原则是不太切实际的,也不现实,始终是一个取舍平衡的过程。设计也不是一个一蹴而就的过程,而是不断发现设计的 “坏味道”,然后持续地改进。