30-3 之软件架构设计原则 2 - OCP 开放封闭原则

上一章节我们有简单的在提一下软件架构设计的两个基本原则 :

  • 低耦合
  • 高内聚

这里我们在提一次。然後接下来我们这篇文章将要来谈谈 SOLID 中的 OCP 开发一封闭原则,也是为了达成上面这两件原则。

OCP ( Open-Closed Principle ) 开放封闭原则是什麽呢 ?

根据 《 Clean Architecture 》这本书里所书写的定义为 :

一个软件应该对於扩展是开放的,但对於修改是封闭的

这个比较白话文的来说我觉得是 :

当有新功能时,是可以用类式 plugin 的方式加新功能,而不是去修改原本的程序

这个违反这个原则,事实上在我们写的程序码中非常的常见,你想想 ~ 是不是在程序中,有很多 function 里会使用 if else 情境来判断要用什麽来处理,例如订单可能会根据不同产品来进行一些对同处理,这时我们是不是最直觉的写法就是加个 if 呢 ?

我个人是认为在开发初起,我们很难预测未来的变动,所以这时简单用个 if 来处理,就我个人是觉得可以接受,但是如果之後又来 ~ 那我就觉得真的可能考虑重构了呢。

这里只要记好一件事情 :

开放封闭原则在软件架构存在的目的,就是在於怕你要改时,会动到原本的程序码,而导致可能的出错

但我小声的说…如果有写测试,那事实上不太会影响…… 在某些小型专案上。

但是这个如果拉到大型专案上,一直用 if else 又或是一直在原本的地方加功能,会哭的不是你,而是後人,活生生的血泪屎。

范例

首先来来个基本版,就是一个专门用来处理订单的类别,然後他有个方法可以计算这个订单的费用。

// bad

class OrderFeeCalculator{
    constructor(order){
        this.order = order
    }

    calcFee(){
        let fee = 0
        fee = 0.1 * this.order.price
        return fee
    }
}

然後这时有个新需求,我们订单的类型为『 文章 ARTICLE 』的话,则费用只要价格的 5% 就好,那这时通常第一直觉会直接改成如下 :

// Bad 2
class OrderFeeCalculator{
    constructor(order){
        this.order = order
    }

    calcFee(){
        let fee = 0
        if(this.order.category === 'ARTICLE'){
            fee = 0.05 * this.order.price
        }else{
            fee = 0.1 * this.order.price
        }
        return fee
    }
}

这一题事实上可以用一些设计模式来解决。

策略模式 + 简单工厂解法

这样如果某一个产品需要特殊的计算,那就只要在对应的 strategy 里修改运算就 ok 罗,而不用怕会影响到其它产品的运算。

// Good
class OrderFeeCalculator{
    constructor(order){
        this.order = order
    }

    calcFee(){
        let fee = 0
        fee = _productFeeStrategyFactory[this.order.category].calcFee(this.order)
        return fee
    }

    _productOrderFeeFactor(category){
        switch (category) {
            case 'ARTICLE':
                return new ArticleFeeStrategy()
            case 'COURSE':
                return new CorseFeeStrategy()
        }

    }
}

class ArticleFeeStrategy{
    calcFee(order){
        return this.order.price * 0.05
    }
}
class CorseFeeStrategy{
    calcFee(order){
        return this.order.price * 0.1
    }
}

小总结

事实上我仔细想想,不少的设计模式都是为了来达到『 开放封闭原则 』,例如 :

  • 策略模式 : 如范例将每种产品的费用计算方法抽出,然後每当有新增产品时,只要新增策略就好,而不用修改原本的程序码
  • 配接器模式 ( adapter pattern ) : 这个我在第三方串接或接老系统时很常用到,就是通常会包一层起来,将 interface 形状用成符合我们内部用的, 这样就不同单心第三方一个修改,全世界都要修改的问题
  • 观查者模式 : 假设有个需求是订单完成後,需要做某件事情 A,然後假设又有 B → Z 个事情要做,那你就会发现,这个 orderComplate 方法超长,而且应该依赖了不少其它 moduel 来处理 A → Z 的事情。事实上可以反过来,让各地方监听订单完成事件,然後要处理什麽事情,由那个地方来处理,这样是不是在新增需求时,不必修改 orderComplate 呢 ?

老样子,来问一下三个问题,来加深记忆

这个知识点可以用来解释什麽现象

我觉得分层架构也有一定原因是因为这个原则而产生的,我们不希望 Presenters 层级的修改,影响到 Controller。

这个知识点可以和以前的什麽知识连结呢 ?

分层架构、与一些设计模式都有连结,这些都是可以让我们们更接近 OCP 的方法。

我要如何运用这个知识点 ?

  • 在开发任何单位的东西 ( function、class、module、layer ) 都需要思考未来是否有需求,会导致这个地方大量的修改,如果答案是,则可以考虑用一些设计模式或啥的来解决。

参考资料


<<:  Day 17:「我们,是好朋友哦~」- Vue 简介

>>:  Day 17:Layout Using Grid

EP25 - EKS 日志蒐集使用 Loki 和 Grafana(一)

前四天我们经历一番折腾, 终於把 Octopus Deploy 架起来, 从 Octopus Dep...

菸酒生的ARM之路-1

在介绍的时候有说到我是在爬文苹果电脑的时候注意到ARM的!不得不说,使用了两个多月下来,对於使用全新...

Day8-Go阵列Array

前言 阵列是一种资料结构,里面装载的资料必须是同性质的,不能同时装载着字串又装载着整数,且建立後阵列...

[全民疯AI系列2.0] 完赛总结

全民疯AI系列2.0完赛总结 不知不觉就参加了三届iT邦铁人赛,很高兴能够藉由此活动分享经验与知识。...

# Day32 写在Go繁之後

Day32 写在Go繁之後 这是我的第0010 0000篇文章。 为什麽系列标题要叫做Go繁不及备载...