释义
初看这个原则时,很容易将其错误地理解为:函数或类只应该保持有一种能力。这固然也是一个重要的设计原则,但是并不是单一职责原则所想表达的意思。这个原则最初的描述是:任何一个软件模块都应该有且仅有一个被修改的原因。
这里可能有点难以理解,对于软件而言,修改是永无止境的。如果只有一个原因能修改它,那设计之初就应该避免这个原因,做到十全十美。所以,这里的原因并不是指某个更改的意图。而是指,该模块所服务的对象,才是修改模块的原因。更进一步的解释就是:任何一个软件模块都应该只对某一类行为者负责。这也是原则名称里的职责含义。这里的行为者就是该模块所服务的对象,可以是用户或者另一个模块。
如果一个模块或者类需要服务于多个行为者,那么随着软件的迭代,行为者需求的更改,模块的代码会无限的膨胀下去,代码耦合度会大大提高。对此我们很难保证针对于某一行为者的修改,能够不影响到其他行为者的行为。最终的结局就是,更改一个问题,引出无数个其他的问题,项目失去维护的可能。
错误例子
下面通过一个不遵守单一职责原则的例子来认识这个原则。该模块用于控制一艘宇宙飞船。
class SpaceStation {
public:
// 构造函数
SpaceStation();
void runSensor();
void useSupplies(string type, size_t quantity);
void reportSupplies();
void loadFuel(size_t quantity);
void reportFuel();
void activeThrusters();
};
在学习数据库范式的时候,会遇到一张违反范式的超级表。这种表将所有的字段都放在了一起,企图在一张表里放下所有的数据,然而这会给数据库带来冗余过大,插入异常,修改异常等问题。同样的,这个类也是一个超级类,设计者试图用一个类来解决关于SpaceStation
的所有需求。
按照单一职责原则,我们至少可以从这个类目前的实现中提取出四个行为者:sensors; supplies; fuel; thrusters
。不同的行为者会有自己不同的需求,科学家会希望管理sensors
,地面指挥中心希望管理好supplies
,工程师会希望管理fuel
,宇航员希望能够管理好thrusters
。
从这就大概就可以看出问题了,每个行为者都希望去更改SpaceStation
,如果不加以阻止,SpaceStation
会越来越膨胀,越来越复杂,这就是修改的“原因”太多了,违反单一职责原则所带来的后果。诚然,在初始阶段,这样去实现可以很快的完成任务,但是后续的维护会加倍地吞噬一开始所节省的时间。
解决办法就是把类负责不同行为者的部分单独拎出来,各自服务好各自的行为者,才可以从根源上避免上面所提到的问题。
简要修改
class Sensor {
public:
void runSensor();
}
class Supplies {
public:
void useSupplies(string type, size_t quantity);
void reportSupplies();
}
class Fuel {
public:
void loadFuel(size_t quantity);
void reportFuel();
}
class Thrusters {
public:
void activeThrusters();
}
class SpaceStation {
public:
SpaceStation();
private:
Sensor sensor_control_;
Supplies supplies_control_;
Fuel fuel_control_;
Thrusters thruster_control_;
};
既然已经归纳出了四个行为者,所以把SpaceStation
中相对应的功能拎出来,放到应该放的地方,也就是代码中所展示的四个类。这样不仅仅是对功能进行了封装,更重要的是保持了类的单一职责,每个类都有自己所属的服务对象。服务对象的需求可以无止境的变更,但不管怎样,这些变更都会被控制在一个类内,不会对外界产生任何影响。对于SpaceStation
而言,它也不需要关心Sensor
或是Fuel
内部是如何实现的,它需要关注的就是这些类所提供的能力,按需索取即可。
当然,这唯一的弊端可能就是看起来复杂了一些,需要耗费的管理精力多了一些,但是随着项目的不断发展,这些弊端都会被抹平。
这样的修改并不是最终的版本,比如我们可以抽象出一个Report
接口,Supplies
和Fuel
都继承这接口来实现对应的报告需求。但对于完成单一职责原则的讲解,目前的修改已经足够了,我们成功地将职责进行了划分,下放到了对应的类中。
总结
单一职责原则是SOLID
原则的第一个大原则,时刻遵循这一原则可以给各个模块划分出清晰的边界,这样可以防止修改代码所带来的风险扩散问题。并且能够实现信息的隐藏,将不必要的数据隐藏在内部,方便后续做替换和权限保护。更重要的是,单一的职责能够保证模块不会被各种各样的修改所破坏,有助于维护代码可维护性。