进行面向对象程序设计的时候,我们需要面对很多问题,比如:
- 什么时候需要一个类
- 一个类应该包含哪些功能,不包含哪些功能
- 类之间的依赖关系设计
还有很多其它的问题,如果不遵照一些原则,而一切按照“本能”来的话,你的程序很快就会成为一坨不可靠,不容易扩展,不容易复用的“屎山”。面向对象设计原则就是为了保证你的代码可靠,易扩展,易复用而人为规定的一系列方法,它们是由大量资深的前辈们总结出来的经验。
这里先介绍一个经典的设计原则:SOLID
SRP | The Single Responsibility Principle | ''A class should have one, and only one, reason to change.'' |
OCP | The Open Closed Principle | You should be able to extend a classes behavior, without modifying it. |
LSP | The Liskov Substitution Principle | Derived classes must be substitutable for their base classes. |
ISP | The Interface Segregation Principle | Make fine grained interfaces that are client specific. |
DIP | The Dependency Inversion Principle | Depend on abstractions, not on concretions. |
这里先说一下SRP,参考文档,假设我们在实现一个游戏,需要计算矩形的面积,以及在GUI中显示矩形这两个功能,那么如果你很直接地实现一个Rectangle
类,并同时让它实现计算面积跟GUI显示,那么就违反了SRP原则。
首先,如果我们用的是C++(或者java之类,Python不会有这个问题,它是通过解释器运行的,不用link)那么这个Rectangle
类需要link GUI的库,相对于单纯只进行几何计算,需要额外的link和compile时间以及内存开销(java中需要加入GUI库的class文件)。其次,如果我们需要修改Rectangle
的渲染部分代码,由于它跟计算部分写在同一个类里面,这会导致我们需要对计算部分也进行重编译以及测试等。
所以,一个遵循SRP的设计,应该是把计算逻辑跟渲染逻辑分别做在两个类里面,也就是“每个类只有一个职责”。那么问题来了,我们该如何去定义一个“职责”,也就是功能应该细分到什么粒度?无限细分显然是错误的,我们没有必要给每一个小功能定义一个类,而有时候看似合理的划分其实又是包含了多个职责的,原文给出的说明是:“A class should have one, and only one, reason to change.”看似很好理解,其实有时候也很具有迷惑性,这里有一个例子:
如果我们定义一个Modem
类,它具有以下函数:
class Modem
{
public void dial(String pno);
public void hangup();
public void send(char c);
public char recv();
}
看上去似乎一切完美,但是它事实上是违背了SRP原则的,dial
和hangup
属于连接管理功能,而send
和recv
则是数据管理,也就是对于Modem
接口,有超过一个以上的理由去修改。
我们可以把Modem
拆分成DataChannel
和Connection
两个接口,这样做似乎有点过于繁琐,并且这两个部分经常是需要同时使用的,所以我们可以使用一个ModemImplementation
类继承这两个接口。
class DataChannel
{
public virtual void send(char c) = 0;
public virtual char recv() = 0;
}
class Connection
{
public virtual void dial(String pno) = 0;
public virtual void hangup() = 0;
}
class ModemImplementation : public DataChannel, public Connection
{
public void send(char c);
public char recv();
public void dial(String pno);
public void hangup();
}
也许看上去这很丑陋且冗余,但这经常是必要的,细分必然会导致繁琐不便(很多场合经常会需要同时使用多个细分功能),接口上细分,类实现再组合保留了便利性和SRP原则的益处,比如系统中有的模块如果需要data部分,那么它可以仅引入DataChannel
,通过interface去调用ModemImplementation
内部的数据部分功能,没有任何模块需要去依赖ModemImplementation
,只有主函数需要知道ModemImplementation
的存在。
总的来说,应用SRP原则时需要正确地去区分所谓的“responsibility”,这个没那么容易,同时出于方便考虑,在把功能细分以后重组有时也是必要的。