开闭原则是指“对扩展开放,对修改关闭”,意思是添加一个新的功能应该是在已有代码基础上扩展代码(新增模块、类、方法等),而非修改代码。
更具体点说就是尽量让扩展可以通过增加类或者增加方法实现,而不是在方法中增加逻辑(如if else、switch case)以及在方法上增加参数的方式(因为增加参数意味着所有的调用方要做出改变,以及子类的该方法也要做出改变)实现。
一个具有良好扩展性的方法是不会因为持续的功能扩展而让该方法的入参和方法体内的代码持续增加的,这就是评判扩展性好坏和遵循开闭原则与否的直接判断方式。
开闭原则是所有设计原则中最重要的原则,没有之一,就如同可扩展性是可读性、复用性、可扩展性、可维护性中最重要的一个一样,而开闭原则直接影响可扩展性的大小。
要做到开闭原则,首先我们要判断代码的各个地方,哪个地方是之后可能会发生扩展的。对于可能发生扩展的地方编码时问自己是否有在这个地方预留好扩展点,预留到了哪,新代码是否能轻松的插入到这个扩展点上。
另外一点是扩展的功能尽可能是可封装的,可封装意味着可复用,你改一个地方就能让所有调用者生效,而不是要改多个相同的地方。if else、switch else的扩展就是不可封装的,因此增加了if else和switch case之后,可读性和复用性都会变差。而类和方法是就有封装性的,因此上面才说让扩展尽可能是通过增加类或者增加方法实现。
下面我们看作者举的一个例子:
有一段 API 接口监控告警的代码。其中,AlertRule 存储告警规则,可以自由设置。Notification 是告警通知类,支持邮件、短信、微信、手机等多种通知渠道。NotificationEmergencyLevel 表示通知的紧急程度,包括 SEVERE(严重)、URGENCY(紧急)、NORMAL(普通)、TRIVIAL(无关紧要),不同的紧急程度对应不同的发送渠道。
代码的初始设计如下:
<?php
class Alert{
private $rule;
private $notification;
public function __construct(AlertRule $rule, Notification $notification){
$this->rule = $rule;
$this->notification = $notification;
}
public function check($api, $requestCount, $errorCount, $durationOfSeconds) {
$tps = $requestCount / $durationOfSeconds;
// 单位时间内请求并发数超过指定值就告警,级别URGENCY
if ($tps > $this->rule->getMatchedRule($api)->getMaxTps()) {
$this->notification->notify(NotificationEmergencyLevel::URGENCY, "...");
}
// 错误数量超过指定值就告警,级别SEVERE
if ($errorCount > $this->rule->getMatchedRule($api)->getMaxErrorCount()) {
$this->notification->notify(NotificationEmergencyLevel::SEVERE, "...");
}
}
}
如果我们需要添加一个功能:当每秒钟接口超时请求个数 timeoutCount超过某个预先设置的最大阈值时,我们也要触发告警发送通知。
主要的改动有两处:第一处是修改 check() 函数的入参,添加一个新的统计数据,表示超时接口请求数;第二处是在 check() 函数中添加新的告警逻辑。
<?php
class Alert{
// 省略属性的构造函数
public function check($api, $requestCount, $errorCount, $durationOfSeconds,$timeoutCount) { // 改动1
$tps = $requestCount / $durationOfSeconds;
if ($tps > $this->rule->getMatchedRule($api)->getMaxTps()) {
$this->notification->notify(NotificationEmergencyLevel::URGENCY, "...");
}
if ($errorCount > $this->rule->getMatchedRule($api)->getMaxErrorCount()) {
$this->notification->notify(NotificationEmergencyLevel::SEVERE, "...");
}
// 改动2
$timeoutTps = $timeoutCount / $durationOfSeconds;
if ($timeoutTps > $this->rule->getMatchedRule($api)->getMaxTimeoutTps()) {
$this->notification->notify(NotificationEmergencyLevel::URGENCY, "...");
}
}
}
这样改的话明显不遵循开闭原则,问题有两点:
a. check方法的参数改变,所有调用check的地方都要改动;
b. 随着功能的继续扩展,if会越写越多,check方法会越来越长,逻辑越堆越多;
这两点问题使得这个Alert类的扩展性很低。
如何改造成遵循开闭原则的样子,提高扩展性?
首先思考哪些地方可能会扩展(直白点哪些地方可能会加东西)。既然我是要加多一种告警方式和统计对象(错误数量,并发量,时间等),那么check的方法体肯定会加东西,而统计对象通过传参传入,所以参数也会加。
但参数并不是一个可扩展的扩展点,而类才是。因此我们需要将参数收归成一个类,而刚好$api, $requestCount, $errorCount, $durationOfSeconds, $timeoutCount都能看做是接口状态这个实体对象的属性,因此将这些入参封装成一个 ApiStatInfo 类。
这样就解决了问题1。
这里顺便提一嘴:将参数中关联性强的参数收归为一个实体对象类是优化方法或函数参数过多以及参数扩展的最佳方式,大家以后遇到了这种情况就可以用这个方式优化。
对于问题2,我多扩展一种告警就在check方法内多写一段代码,这是明显的基于实现编程。如果不希望check中代码随扩展不断的增加,就需要收归告警的行为到一个接口中,用多态涵盖所有可能的告警方式,通过面向接口编程取代面向实现编程。因此我们引入一个 AlertHandler接口。
具体优化如下:
<?php
class Alert{
private $alertHandlers;
public function __construct(){
$this->alertHandlers = [];
}
public function registerAlertHandler(AlertHandler $handler){
$this->alertHandlers[] = $handler;
}
public function check(ApiStatusInfo $apiInfo) {
/** @var AlertHandler $handler */
foreach($this->alertHandlers as $handler){
$handler->check($apiInfo);
}
}
}
abstract class AlertHandler {
protected $notification;
protected $rule;
public function __construct(AlertRule $rule, Notification $notification)
{
$this->rule = $rule;
$this->notification = $notification;
}
abstract public function check(ApiStatusInfo $apiInfo);
}
class ApiStatusInfo{
protected $api;
protected $requestCount;
protected $durationOfSeconds;
protected $timeoutCount;
protected $errorCount;
public function getApi(){
return $this->api;
}
public function getRequestCount(){
return $this->requestCount;
}
public function getDurationOfSeconds(){
return $this->durationOfSeconds;
}
public function getTimeoutCount(){
return $this->timeoutCount;
}
public function getErrorCount(){
return $this->errorCount;
}
// 对应的setter方法省略
}
class ApiTpsHandler extends AlertHandler{
public function check(ApiStatusInfo $apiInfo){
$tps = $apiInfo->getRequestCount() / $apiInfo->getDurationOfSeconds();
if ($tps > $this->rule->getMatchedRule($apiInfo->getApi())->getMaxTps()) {
$this->notification->notify(NotificationEmergencyLevel::URGENCY, "...");
}
}
}
class ApiErrorHandler extends AlertHandler{
public function check(ApiStatusInfo $apiInfo){
if ($apiInfo->getErrorCount() > $this->rule->getMatchedRule($apiInfo->getApi())->getMaxErrorCount()) {
$this->notification->notify(NotificationEmergencyLevel::SEVERE, "...");
}
}
}
class ApiTimeoutHandler extends AlertHandler{
public function check(ApiStatusInfo $apiInfo){
$timeoutTps = $apiInfo->getTimeoutCount() / $apiInfo->getDurationOfSeconds();
if ($timeoutTps > $this->rule->getMatchedRule($apiInfo->getApi())->getMaxTimeoutTps()) {
$this->notification->notify(NotificationEmergencyLevel::URGENCY, "...");
}
}
}
使用示例demo:
<?php
class ApplicationContext {
private $alertRule;
private $notification;
private $alert;
public function initializeBeans() {
$this->alertRule = new AlertRule(/*.省略参数.*/); //省略一些初始化代码
$this->notification = new Notification(/*.省略参数.*/); //省略一些初始化代码
$this->alert = new Alert();
$this->alert->registerAlertHandler(new ApiTpstHandler($this->alertRule, $this->notification));
$this->alert->registerAlertHandler(new ApiErrorHandler($this->alertRule, $this->notification));
$this->alert->registerAlertHandler(new ApiTimeoutAlertHandler($this->alertRule, $this->notification));
}
public function getAlert():Alert { return $this->alert; }
// 饿汉式单例
private static $instance;
private function __construct()
{
$this->initializeBeans();
}
public static function getInstance():ApplicationContext {
if(is_null(self::$instance)){
self::$instance = new ApplicationContext();
}
return self::$instance;
}
}
class Demo {
public static function main() {
$apiStatInfo = new ApiStatInfo();
// ...省略设置apiStatInfo数据值的代码
ApplicationContext::getInstance()->getAlert()->check($apiStatInfo);
}
}
这样我们的代码就变得更容易扩展,如果我们要添加新的告警逻辑,只基于扩展的方式创建新的 handler 类即可,不用改动原来的 check() 函数的逻辑。当然,提升扩展性的优化作出了一点点牺牲,那就是代码在可读性上稍微变差了一些。
从这里我们可以知道:对于需要扩展的逻辑,将这些逻辑从方法中拆出来,拆成一个个类并通过多态实现是提升扩展性且遵循开闭原则的一个常规做法。特别是对if else 和 switch case的优化适用。
所以我们要扩展的时候,想想能不能将现有逻辑拆开成类,怎么拆。
上面我们通过一个例子介绍了如何做到开闭原则。实际开发中,还有很多可以实现开闭原则的做法,如:
多态 + 依赖注入 + 基于接口而非实现编程;
装饰器模式;
策略模式;
模板模式;
职责链模式;
等等。
下面我们介绍使用 ”多态 + 依赖注入 + 基于接口而非实现编程“的方式实现开闭原则,提升扩展性。
a.多态是指使用接口制定规范,而扩展出来的逻辑则需要封装为类实现接口,而不是一堆零散的逻辑代码;
b.依赖注入是指将依赖对象传入到调用者中,而非在调用者的内部直接实例化所依赖的对象,调用者委托所依赖对象调用方法;
c. 基于接口而非实现编程是指对传入调用者内的依赖对象的类型声明,最好是声明成它的接口或者抽象类,而不要声明成具体类(让调用者只依赖抽象和规范而非依赖具体实现),否则下次你想替换成另一个依赖类的话就要改调用者的方法参数了。
例子:
有个需求要通过 Kafka 来发送异步消息。但是在实现的时候,我们要学会将发消息行为抽象成跟具体消息队列(Kafka)无关的接口。所有上层系统不依赖具体的哪一种消息队列,只依赖发消息这个行为。这就是所谓的依赖抽象而非依赖实现。
并且将具有”发消息行为“的对象注入到上层系统(即调用者)中。当我们要替换新的消息队列的时候,比如将 Kafka 替换成 RocketMQ,可以很方便地拔掉老的消息队列实现,插入新的消息队列实现。
<?php
// 这一部分体现了抽象意识和多态
interface MessageQueue { //... }
class KafkaMessageQueue implements MessageQueue { //... }
class RocketMQMessageQueue implements MessageQueue {//...}
interface MessageFromatter { //... }
class JsonMessageFromatter implements MessageFromatter {//...}
class ProtoBufMessageFromatter implements MessageFromatter {//...}
class Demo { // 调用者
private $msgQueue;
// 如果下面一行的 __construct() 中声明的是 KafkaMessageQueue 就基于实现编程,而声明 MessageQueue就是基于接口而非实现编程
public function __construct(MessageQueue $msgQueue) { // 依赖注入
$this->msgQueue = $msgQueue;
}
// msgFormatter:多态、依赖注入
public function sendNotification(Notification $notification, MessageFormatter $msgFormatter) {
//...
}
}
最后还是要说一句,开闭原则不是免费的,为了实现扩展性,有时候会牺牲可读性,因为开闭原则要求将零散逻辑封装到类中,扩展的次数多了,类的数量会变多,耦合关系会逐渐复杂,这些都会导致可读性降低。
如果您需要转载,可以点击下方按钮可以进行复制粘贴;本站博客文章为原创,请转载时注明以下信息
张柏沛IT技术博客 > 面向对象和设计模式(七)设计原则之开闭原则 OCP(Open Closed Principle)