美文网首页
设计原则之开闭原则

设计原则之开闭原则

作者: CaryaLiu | 来源:发表于2021-08-26 10:43 被阅读0次

    本文是极客时间里王争专栏《设计模式之美》的学习笔记,你可以通过链接阅读原文获取更加详尽的描述,也可以通过该链接进行订阅和购买获取优惠。

    开闭原则

    在 23 种经典设计模式中,大部分设计模式都是为了解决代码的扩展性问题而存在的,主要遵从的设计原则就是开闭原则。

    如何理解“对扩展开放、修改关闭”?

    开闭原则(Open Closed Principle),简写为OCP。其定义:

    software entities (modules, classes, functions, etc.) should be open for extension , but closed for modification。

    软件实体(模块、类、方法等)应该“对扩展开放、对修改关闭”。

    添加一个新的功能应该是,在已有代码基础上扩展代码(新增模块、类、方法等),而非修改已有代码(修改模块、类、方法等)。

    为了便于理解该原则,这里举一个例子。下面是一个Web容器中的一段代码,意在根据操作类型判断处理是否导航到新的内容。

    class HybridWebController: UIViewController {
        lazy var wkWebView: WKWebView = createWkWebView()
        var url = "https://taobao.com"
        override func viewDidLoad() {
            super.viewDidLoad()
            setupWkWebView()
            wkWebView.load(URLRequest(url: URL(string: url)!))
        }
        func setupWkWebView() {
            view.addSubview(wkWebView)
            wkWebView.snp.makeConstraints { make in
                make.edges.equalToSuperview()
            }
            wkWebView.navigationDelegate = self
        }
        func createWkWebView() -> WKWebView {
            let configuration = WKWebViewConfiguration()
            configuration.allowsInlineMediaPlayback = true
            configuration.dataDetectorTypes = []
            let wkWebView = WKWebView(frame: CGRect.zero, configuration: configuration)
            return wkWebView
        }
    }
    extension HybridWebController: WKNavigationDelegate {
        func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
            // 处理特殊scheme的事件
            let hasHandledNavigation = handleNavigation(navigationAction)
            if hasHandledNavigation {
                decisionHandler(.cancel)
                return
            }
            decisionHandler(.allow)
        }
    }
    

    假如我们现在有一种新的业务场景,比如需要对接快电,我们需要判断navigationAction中的url是否包含快电的域名,然后做出对应的处理。主要的改动有:

    1. 增加判断是否包含快电的域名,并做出处理快电业务的逻辑
    2. 根据判断结果,进行对应的回调处理

    修改的代码如下:

    extension HybridWebController: WKNavigationDelegate {
        func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
            // 处理特殊scheme的事件
            let hasHandledNavigation = handleNavigation(navigationAction)
            if hasHandledNavigation {
                decisionHandler(.cancel)
                return
            }
            // 改动点1:针对处理快电的逻辑进行回调
            // 处理第三方对接
            if handleFleetingPower(navigationAction) {
                decisionHandler(.cancel)
                return
            }
            // ...
            decisionHandler(.allow)
        }
        // 改动点2:处理快电的业务逻辑
        func handleFleetingPower(_ navigationAction: WKNavigationAction) -> Bool {
            if let url = navigationAction.request.url?.absoluteString,
               url.contains("fleetingpower.com") {
                // 处理快电业务
                return true
            }
            return false
        }
    }
    

    上面的代码改动是基于“修改”的方式来实现新功能的。如果我们遵循开闭原则,也就是“对扩展开放、对修改关闭”。那如何通过“扩展”的方式,来实现同样的功能呢?

    我们先重构一下之前的 代码,让它的扩展性更好一些。重构的内容主要包含两部分:

    • 引入HybridWebNavigationPolicy协议,用于抽象在上述代理方法中进行处理的各种判断策略
    • 引入WebNavigationPolicyManager管理类,用户处理各种策略的优先级以及策略集合的组装

    具体代码实现如下所示:

    protocol HybridWebNavigationPolicy {
        func decidePolicy(for navigationAction: WKNavigationAction, webController: HybridWebController?) -> Bool
    }
    
    /// 处理特殊scheme的事件
    struct CommonNavigationPolicy: HybridWebNavigationPolicy {
        func decidePolicy(for navigationAction: WKNavigationAction, webController: HybridWebController?) -> Bool {
            let url = navigationAction.request.url as NSURL?
            let scheme = url?.scheme
            if let url = url, let scheme = scheme {
                if scheme == "tel" {
                    let resourceSpecifier = url.resourceSpecifier
                    DispatchQueue.main.async {
                        // 拨打电话
                    }
                    return false
                }
                if scheme == "itms-apps" {
                    // 跳转到AppStore
                    return false
                }
            }
            return true
        }
    }
    
    class WebNavigationPolicyManager {
        static func navigationPolicics() -> [HybridWebNavigationPolicy] {
            let common = CommonNavigationPolicy()
            return [common]
        }
    }
    
    class HybridWebController: UIViewController {
        /// 依赖注入的方式
            var policies = WebNavigationPolicyManager.navigationPolicics()
      
    }
    
    extension HybridWebController: WKNavigationDelegate {
        func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
            for policy in policies {
                if !policy.decidePolicy(for: navigationAction, webController: owner) {
                    decisionHandler(.cancel)
                    return
                }
            }
            decisionHandler(.allow)
        }
    }
    
    

    现在,我们再来看下,基于重构之后的代码,如果再添加上面讲到的那个新功能,对接快电的业务,我们又该如何改动代码呢?主要的改动有下面四处。

    • 改动点1,新增FleetingPowerNavigationPolicy,遵循HybridWebNavigationPolicy协议,用于处理快电业务
    • 改动点2,在WebNavigationPolicyManagerstatic func navigationPolicics() -> [HybridWebNavigationPolicy]函数返回数组中增加FleetingPowerNavigationPolicy的实例

    具体代码实现如下:

    protocol HybridWebNavigationPolicy {
            // 代码未改动
    }
    
    /// 处理特殊scheme的事件
    struct CommonNavigationPolicy: HybridWebNavigationPolicy {
        // 代码未改动
    }
    
    /// 改动点1:处理快电业务
    struct FleetingPowerNavigationPolicy: HybridWebNavigationPolicy {
        func decidePolicy(for navigationAction: WKNavigationAction, webController: HybridWebController?) -> Bool {
            if let url = navigationAction.request.url?.absoluteString,
               url.contains("fleetingpower.com") {
                return true
            }
            return false
        }
    }
    
    class WebNavigationPolicyManager {
        static func navigationPolicics() -> [HybridWebNavigationPolicy] {
            let common = CommonNavigationPolicy()
            // 改动点2:新增`FleetingPowerNavigationPolicy`的实例
            let fleetingPower = FleetingPowerNavigationPolicy()
            return [common, fleetingPower]
        }
    }
    
    class HybridWebController: UIViewController {
            // 代码未改动
    }
    
    extension HybridWebController: WKNavigationDelegate {
        func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
            // 代码未改动
        }
    }
    
    
    

    重构之后的代码更加灵活和易扩展。如果我们要想添加新的基于操作的导航判断,只需要基于扩展的方式创建新的Policy类即可,不需要改动原来的webView(_:decidePolicyFor:decisionHandler:)函数的逻辑。

    修改代码就意味着违背开闭原则吗?

    从开闭原则的定义中,我们可以看出,开闭原则可以应用在不同粒度的代码中,可以是模块,也可以类,还可以是方法(及其属性)。同样一个代码改动,在粗代码粒度下,被认定为“修改”,在细代码粒度下,又可以被认定为“扩展”。比如,添加属性和方法相当于修改类,在类这个层面,这个代码改动可以被认定为“修改”;但这个代码改动并没有修改已有的属性和方法,在方法(及其属性)这一层面,它又可以被认定为“扩展”。

    看了上面重构之后的代码,你可能还会有疑问:在添加新的判断处理逻辑的时候,尽管改动点1(添加新的Policy类)是基于扩展而非修改的方式来完成,但是改动点2貌似不是基于扩展而是基于修改的方式来完成的,那改动点1不就违背开闭原则了吗?

    实际上,我们也没必要纠结某个代码改动是“修改”还是“扩展”,更没必要太纠结它是否违反“开闭原则”。我们回到这条原则的设计初衷:只要它没有破坏原有的代码的正常运行,没有破坏原有的单元测试,我们就可以说,这是一个合格的代码改动。

    在重构之后的webView(_:decidePolicyFor:decisionHandler:)代码实现中,我们的核心逻辑集中在该方法中以及各个Policy中,当我们在添加新的判断处理逻辑的时候,该方法完全不需要修改,而只需要扩展一个新的Policy类。如果我们把该方法及各个Policy类合起来看做一个“模块”,那模块本身在添加新的功能的时候,完全满足开闭原则。

    添加一个新功能,不可能任何模块、类、方法的代码都不“修改”,这个是做不到的。类需要创建、组装、并且做一些初始化操作,才能构建成可运行的的程序,这部分代码的修改是在所难免的。我们要做的是尽量让修改操作更集中、更少、更上层,尽量让最核心、最复杂的那部分逻辑代码满足开闭原则。

    如何做到“对扩展开放、修改关闭”?

    开闭原则讲的就是代码的扩展性问题,是判断一段代码是否易扩展的“金标准”。如果某段代码在应对未来需求变化的时候,能够做到“对扩展开放、对修改关闭”,那就说明这段代码的扩展性比较好。所以,问如何才能做到“对扩展开放、对修改关闭”,也就粗略地等同于在问,如何才能写出扩展性好的代码。

    在讲具体的方法论之前,我们先来看一些更加偏向顶层的指导思想。为了尽量写出扩展性好的代码,我们要时刻具备扩展意识、抽象意识、封装意识。这些“潜意识”可能比任何开发技巧都重要。

    在写代码的时候后,我们要多花点时间往前多思考一下,这段代码未来可能有哪些需求变更、如何设计代码结构,事先留好扩展点,以便在未来需求变更的时候,不需要改动代码整体结构、做到最小代码改动的情况下,新的代码能够很灵活地插入到扩展点上,做到“对扩展开放、对修改关闭”。

    还有,在识别出代码可变部分和不可变部分之后,我们要将可变部分封装起来,隔离变化,提供抽象化的不可变接口,给上层系统使用。当具体的实现发生变化的时候,我们只需要基于相同的抽象接口,扩展一个新的实现,替换掉老的实现即可,上游系统的代码几乎不需要修改。刚刚我们讲了实现开闭原则的一些偏向顶层的指导思想,现在我们再来看下,支持开闭原则的一些更加具体的方法论。

    在众多的设计原则、思想、模式中,最常用来提高代码扩展性的方法有:多态、依赖注入、基于接口而非实现编程,以及大部分的设计模式(比如,装饰、策略、模板、职责链、状态等)。

    如何在项目中灵活应用开闭原则?

    写出支持“对扩展开放、对修改关闭”的代码的关键是预留扩展点。那问题是如何才能识别出所有可能的扩展点呢?

    如果你开发的是一个业务导向的系统,比如金融系统、电商系统、物流系统等,要想识别出尽可能多的扩展点,就要对业务有足够的了解,能够知道当下以及未来可能要支持的业务需求。如果你开发的是跟业务无关的、通用的、偏底层的系统,比如,框架、组件、类库,你需要了解“它们会被如何使用?今后你打算添加哪些功能?使用者未来会有哪些更多的功能需求?”等问题。

    即便我们对业务、对系统有足够的了解,那也不可能识别出所有的扩展点,即便你能识别出所有的扩展点,为这些地方都预留扩展点,这样做的成本也是不可接受的。我们没必要为一些遥远的、不一定发生的需求去提前买单,做过度设计。

    最合理的做法是,对于一些比较确定的、短期内可能就会扩展,或者需求改动对代码结构影响比较大的情况,或者实现成本不高的扩展点,在编写代码的时候之后,我们就可以事先做些扩展性设计。但对于一些不确定未来是否要支持的需求,或者实现起来比较复杂的扩展点,我们可以等到有需求驱动的时候,再通过重构代码的方式来支持扩展的需求。

    开闭原则也并不是免费的。有些情况下,代码的扩展性会跟可读性相冲突。很多时候,我们都需要在扩展性和可读性之间做权衡。在某些场景下,代码的扩展性很重要,我们就可以适当地牺牲一些代码的可读性;在另一些场景下,代码的可读性更加重要,那我们就适当地牺牲一些代码的可扩展性。

    相关文章

      网友评论

          本文标题:设计原则之开闭原则

          本文链接:https://www.haomeiwen.com/subject/wtnyiltx.html