設(shè)計(jì)模式-過度設(shè)計(jì)

寫軟件和造樓房一樣需要設(shè)計(jì),但是和建筑行業(yè)嚴(yán)謹(jǐn)客觀的設(shè)計(jì)規(guī)范不同,軟件設(shè)計(jì)常常很主觀,且容易引發(fā)爭論。

設(shè)計(jì)模式被認(rèn)為是軟件設(shè)計(jì)的“規(guī)范”,但是在互聯(lián)網(wǎng)快速發(fā)展的過程中,也暴露了一些問題。相比過程式代碼的簡單與易于修改,設(shè)計(jì)模式常常導(dǎo)致代碼復(fù)雜,增加理解與修改的成本,我們稱之為 “過度設(shè)計(jì)”。因而很多人認(rèn)為,設(shè)計(jì)模式只是一種炫技,對系統(tǒng)沒有實(shí)質(zhì)作用,甚至有很大的挖坑風(fēng)險。這個觀點(diǎn)容易讓人因噎廢食,放棄日常編碼中的設(shè)計(jì)。

  1. 為什么長期來看,設(shè)計(jì)模式相比過程式代碼是更好的?
  2. 什么情況下設(shè)計(jì)模式是有益的,而什么情況下會成為累贅?
  3. 如何利用設(shè)計(jì)模式的益處,防止其腐化?

設(shè)計(jì)模式的缺陷

“過度設(shè)計(jì)” 這個詞也不是空穴來風(fēng),首先,互聯(lián)網(wǎng)軟件的迭代比傳統(tǒng)軟件快很多,傳統(tǒng)軟件,比如銀行系統(tǒng),可能一年只有兩個迭代,而網(wǎng)站的后臺可能每周都在發(fā)布更新,所以互聯(lián)網(wǎng)非常注重軟件修改的便捷性。其次,設(shè)計(jì)模式的 “分模塊”,“開閉原則” 等主張,天然地易于拓展而不利于修改,和互聯(lián)網(wǎng)軟件頻繁迭代產(chǎn)生了一定的沖突。

開閉原則的缺陷

開閉原則:軟件中對象應(yīng)該對擴(kuò)展開放,對修改關(guān)閉。

基于開閉原則,誕生了很多中臺系統(tǒng)。應(yīng)用通過插件的方式,可以在滿足自身定制業(yè)務(wù)需求的同時,復(fù)用中臺的能力。

當(dāng)業(yè)務(wù)需求滿足中臺的主體流程和規(guī)范時,一切看上去都很順利。一旦需求發(fā)生變更,不再符合中臺的規(guī)范了,往往需要中臺進(jìn)行傷筋動骨的改造,之前看到一篇文章吐嘈 “本來業(yè)務(wù)上一周就能搞定的需求,提給中臺需要8個月”。

所以基于中臺無法進(jìn)行深度的創(chuàng)新,深度創(chuàng)新在軟件上必然也會有深度的修改,而中臺所滿足的開閉原則是不利于修改的。

迪米特原則的缺陷

迪米特原則:一個實(shí)體應(yīng)當(dāng)盡量少地與其他實(shí)體之間發(fā)生相互作用,使得系統(tǒng)功能模塊相對獨(dú)立。

基于迪米特法則,我們會把軟件設(shè)計(jì)成一個個 “模塊”,然后對每個 “模塊” 只傳遞需要的參數(shù)。

在過程式編碼中,代碼片段是擁有上下文的全部信息的,比如下面的薪資計(jì)算代碼:

// 績效
int performance = 4;
// 職級
int level = 2;
String job = "engineer";
switch (job) {
    case "engineer":
        // 雖然計(jì)算薪資時只使用了 績效 作為參數(shù), 但是從上下文中都是很容易獲取的
        return 100 + 200 * performance;
    case "pm":
        // .... 其余代碼省略
}

而如果我們將代碼改造成策略模式,為了滿足迪米特法則,我們只傳遞需要的參數(shù):

// 績效
int performance = 4;
// 職級
int level = 2;
String job = "engineer";
// 只傳遞了需要 performance 參數(shù)
Context context = new Context();
context.setPerformance(performance);
strategyMap.get(job).eval(context);

需求一旦變成 “根據(jù)績效和職級計(jì)算薪資”,過程式代碼只需要直接取用上下文的參數(shù),而策略模式中需要分三步,首先在 Context 中增加該參數(shù),然后在策略入口處設(shè)置參數(shù),最后才能在業(yè)務(wù)代碼中使用增加的參數(shù)。

這個例子尚且比較簡單,互聯(lián)網(wǎng)的快速迭代會讓現(xiàn)實(shí)情況更加復(fù)雜化,比如多個串聯(lián)在一起模塊,每個模塊都需要增加參數(shù),修改成本成倍增加。

可理解性的缺陷

設(shè)計(jì)模式一般都會應(yīng)用比較高級的語言特性:

  • 策略模式在內(nèi)的幾乎所有設(shè)計(jì)模式都使用了多態(tài)
  • 訪問者模式需要理解動態(tài)分派和靜態(tài)分派
  • ...
    這些大大增加了設(shè)計(jì)模式代碼的理解成本。而過程式編碼只需要會基本語法就可以寫了,不需要理解這么多高級特性。

小結(jié)

這三點(diǎn)缺陷造成了設(shè)計(jì)模式和互聯(lián)網(wǎng)快速迭代之間的沖突,這也是應(yīng)用設(shè)計(jì)模式時難以避免的成本。

過程式編碼相比設(shè)計(jì)模式,雖然有著簡單,易于修改的優(yōu)點(diǎn),但是卻有永遠(yuǎn)無法回避的本質(zhì)缺陷。

過程式編碼的本質(zhì)缺陷

上文中分析,過程式編碼的優(yōu)點(diǎn)就是 “簡單,好理解,易于修改”。這些有點(diǎn)乍看之下挺對的,但是仔細(xì)想想都很值得懷疑:

  • “簡單”:業(yè)務(wù)邏輯不會因?yàn)檫^程式編碼而變得更加簡單,相反,越是大型的代碼庫越會大量使用設(shè)計(jì)模式(比如擁有 2400w 行代碼的 Chromium);
  • “好理解”:過程式編碼只是短期比較好理解,因?yàn)闆]有設(shè)計(jì)模式的學(xué)習(xí)成本,但是長期來看,因?yàn)樗鼪]有固定的模式,理解成本是更高的;
  • “易于修改”:這一點(diǎn)我相信是對的,但是設(shè)計(jì)模式同樣也可以是易于修改的,下一節(jié)將會進(jìn)行論述,本節(jié)主要論述前兩點(diǎn)。

軟件復(fù)雜度

軟件工程著作 《人月神話》 中認(rèn)為軟件復(fù)雜度包括本質(zhì)復(fù)雜度和偶然復(fù)雜度。

本質(zhì)復(fù)雜度是指業(yè)務(wù)本身的復(fù)雜度,而偶然復(fù)雜度一般是因?yàn)榉椒ú粚蛘呒夹g(shù)原因引入的復(fù)雜度,比如拆分服務(wù)導(dǎo)致的分布式事務(wù)問題,就是偶然復(fù)雜度。

如果一段業(yè)務(wù)邏輯本來就很復(fù)雜,即本質(zhì)復(fù)雜度很高,相關(guān)模塊的代碼必然是復(fù)雜難以理解的,無論是采用設(shè)計(jì)模式還是過程式編碼?!坝眠^程式編碼就會更簡單” 的想法在這種情況下顯然是荒謬的,相反,根據(jù)經(jīng)驗(yàn),很多一直在采用過程式編碼的復(fù)雜模塊,最后都會變得邏輯混亂,缺乏測試用例,想重構(gòu)時已經(jīng)積重難返。

那么設(shè)計(jì)模式會增加偶然復(fù)雜度嗎?閱讀有設(shè)計(jì)模式的代碼,除了要理解業(yè)務(wù)外,還要理解設(shè)計(jì)模式,看起來是增加了偶然復(fù)雜度,但是下文中我們會討論,從長期的角度來看,這不完全正確。

理解單一問題 vs 理解一類問題

開頭提到,設(shè)計(jì)模式是軟件設(shè)計(jì)的“規(guī)范”,和建筑業(yè)的設(shè)計(jì)規(guī)范類似,規(guī)范能夠幫助不同背景的人們理解工程師的設(shè)計(jì),比如,當(dāng)工人們看到三角形的結(jié)構(gòu)時,就知道這是建筑師設(shè)計(jì)的支撐框架。

過程式代碼一般都是針對當(dāng)前問題的某個特殊解決方法,不包含任何的 “模式”,雖然表面上減少了 “模式”的學(xué)習(xí)成本,但是每個維護(hù)者/調(diào)用者都要去理解一遍這段代碼的特殊寫法,特殊調(diào)用方式,無形中反而增加了成本。

以數(shù)據(jù)結(jié)構(gòu)的遍歷為例,如果全部采用過程式編碼,比如二叉樹打印的代碼是:

public void printTree(TreeNode root) {
    if (root != null) {
        System.out.println(root.getVal());
        preOrderTraverse1(root.getLeft());
        preOrderTraverse1(root.getRight);
    }
}

圖的節(jié)點(diǎn)計(jì)數(shù)代碼是:

public int countNode(GraphNode root) {
    int sum = 0;
    Queue<Node> queue = new LinkedList<>();
    queue.offer(root);
    root.setMarked(true);

    while(!queue.isEmpty()){
        Node o = queue.poll();
        sum++;

        List<Node> list = g.getAdj(o);
        for (Node n : list) {
            if (!n.isMarked()) {
                queue.add(n);
                n.setMarked(true);
            }
        }
    }
    return sum;
}

這些代碼本質(zhì)上都是在做數(shù)據(jù)結(jié)構(gòu)的遍歷,但是每次讀到這樣的代碼片段時,你都要將它讀到底才發(fā)現(xiàn)它其實(shí)就是一個遍歷邏輯。幸好這里的業(yè)務(wù)邏輯還比較簡單,就是一個打印或者計(jì)數(shù),在實(shí)際工作中往往和更復(fù)雜的業(yè)務(wù)邏輯耦合在一起,更難發(fā)現(xiàn)其中的遍歷邏輯。

而如果我們使用迭代器模式,二叉樹的打印代碼就變成:

public void printTree(TreeNode root) {
    Iterator<TreeNode> iterator = root.iterator();
    while (iterator.hasNext()) {
        TreeNode node = iterator.next();
        System.out.println(node);
    }
}

圖的節(jié)點(diǎn)計(jì)數(shù)代碼變成:

public int countNode(GraphNode root) {
    int sum = 0;
    Iterator<TreeNode> iterator = root.iterator();
    while (iterator.hasNext()) {
        iterator.next();
        sum++;
    }
    return sum;
}

這兩段代碼雖然有區(qū)別,但是它們滿足一樣的 ”模式“,即 “迭代器模式”,看到 Iterator 我們就知道是在進(jìn)行遍歷,甚至都不需要關(guān)心不同數(shù)據(jù)結(jié)構(gòu)具體實(shí)現(xiàn)上的區(qū)別,這是所有遍歷統(tǒng)一的解決方案。雖然在第一次閱讀這個模式的代碼時需要付出點(diǎn)成本學(xué)習(xí) Iterator,但是之后類似代碼的理解成本卻會大幅度降低。

設(shè)計(jì)模式中類似上面的例子還有很多:

  • 看到 XxxObserver,XxxSubject 就知道這個模塊是用的是觀察者模式,其功能大概率是通過注冊觀察者實(shí)現(xiàn)的
  • 看到 XxxStrategy 策略模式,就知道這個模塊會按照某種規(guī)則將業(yè)務(wù)路由到不同的策略
  • 看到 XxxVisitor 訪問者模式 就知道這個模塊解決的是嵌套結(jié)構(gòu)訪問的問題
  • ...

是面對具體問題 case by case 的學(xué)習(xí),還是掌握一個通用原理理解一類問題?肯定是學(xué)習(xí)后者更有效率。

“過程式代碼更加好理解”往往只是針對某個代碼片段的,當(dāng)我們將范圍擴(kuò)大到一個模塊,甚至整個系統(tǒng)時,其中會包含大量的代碼片段,如果這些代碼片段全部是無模式的過程代碼,理解成本會成倍增加,相似的模式則能大大降低理解成本,越大的代碼庫從中的收益也就越大。

新人學(xué)習(xí)過程式編碼和設(shè)計(jì)模式的學(xué)習(xí)曲線如下圖:

學(xué)習(xí)曲線圖.png

設(shè)計(jì)模式防腐

前文中提到,互聯(lián)網(wǎng)軟件非常注重修改的便捷性,而這是過程式編碼的長處,設(shè)計(jì)模式天然是不利于修改的。但是過程式編碼又有著很多致命的問題,不宜大規(guī)模使用。我們?nèi)绾尾拍茉诎l(fā)揮設(shè)計(jì)模式長處的同時,揚(yáng)長補(bǔ)短,跟上業(yè)務(wù)的快速演進(jìn)呢?

腐敗的設(shè)計(jì)模式

有一條惡龍,每年要求村莊獻(xiàn)祭一個少女,每年這個村莊都會有一個少年英雄去與惡龍搏斗,但無人生還。
又一個英雄出發(fā)時,有人悄悄尾隨,龍穴鋪滿金銀財(cái)寶,英雄用劍刺死惡龍。然后英雄坐在尸身上,看著閃爍的珠寶,慢慢地長出鱗片、尾巴和觸角,最終變成惡龍。

以上是緬甸著名的 “屠龍少年變成惡龍” 的傳說。見過很多系統(tǒng),最初引入設(shè)計(jì)模式是為了提高可維護(hù)性,當(dāng)時或許實(shí)現(xiàn)了這個目標(biāo),但是隨著時間推移,變成了系統(tǒng)中沒人敢修改,“不可維護(hù)” 的部分,最終成為一個 “過度設(shè)計(jì)”,主要原因有以下兩點(diǎn):

  1. 無法調(diào)試: 新的維護(hù)者無法通過調(diào)試快速學(xué)習(xí)模塊中的 “模式”,或者說因?yàn)閷W(xué)習(xí)成本太高,人們常在沒有弄清楚“模式”的情況下就著手改代碼,越改越離譜,最終覆水難收
  2. 沒有演進(jìn): 系統(tǒng)中的設(shè)計(jì)模式也是要跟隨業(yè)務(wù)不斷演進(jìn)的。但是現(xiàn)實(shí)中很多系統(tǒng)發(fā)展了好幾年,只在剛開始創(chuàng)建的時候進(jìn)行過一次設(shè)計(jì),后來因?yàn)闀r間緊或者懶惰等其他原因,再也沒有人改過模式,最終自然跟不上業(yè)務(wù),變成系統(tǒng)中的累贅。

可調(diào)試的模塊

“模塊” 是軟件調(diào)試的基本單位,一個模塊中可能會應(yīng)用多種 “設(shè)計(jì)模式” 來輔助設(shè)計(jì)。設(shè)計(jì)模式相比過程式編碼,邏輯不是線性的,無法通過逐行閱讀來確認(rèn)邏輯,調(diào)試就是后來人學(xué)習(xí)理解設(shè)計(jì)的重要途徑。在理解的基礎(chǔ)上,后人才能進(jìn)行正確的模式演進(jìn)。

“模塊” 在軟件工程中的概念比較含糊:

  • 模塊可以是一個獨(dú)立的系統(tǒng)。由多個微服務(wù)構(gòu)成的一個系統(tǒng),每個微服務(wù)可以認(rèn)為是一個 “模塊”;
  • 在同一個應(yīng)用中 和一個功能相關(guān)的對象集合也可以認(rèn)為是一個模塊。

隨著微服務(wù)概念的興起,很多人誤認(rèn)為只有將代碼拆成單獨(dú)的系統(tǒng),才能叫 “模塊”,其實(shí)不然,我們應(yīng)該先在同一個應(yīng)用中將模塊拆分開來,然后再演化成另一個單獨(dú)的應(yīng)用,如果一上來就強(qiáng)行拆的話,只會得到兩個像量子糾纏一樣耦合在一起的應(yīng)用。

關(guān)于軟件調(diào)試。有的人傾向于每做一點(diǎn)修改就從應(yīng)用的入口處(點(diǎn)擊圖形界面或者調(diào)用 http 接口)進(jìn)行測試,對他來說應(yīng)用內(nèi)部的分模塊就是一種負(fù)擔(dān),因?yàn)橐坏y試不通過,他需要理解模塊之間復(fù)雜的交互,然后確認(rèn)傳入被修改模塊的參數(shù)是什么。對他來說,肯定是全部使用過程式編碼更好理解一些,然后抱怨系統(tǒng) ”過度設(shè)計(jì)“,雖然可能設(shè)計(jì)并沒有過度。

有經(jīng)驗(yàn)的工程師在修改完代碼后,會先測試被修改模塊的正確性,沒有問題后,應(yīng)用入口處的測試只是走個流程,大多可以一遍通過。但是如果一個模塊沒有辦法獨(dú)立調(diào)試的話,那么它所有人來說都是一個累贅。

對于獨(dú)立系統(tǒng)的模塊,它的接口應(yīng)該在脫離整個應(yīng)用后也明確的含義的,接口參數(shù)也應(yīng)該盡量簡單且容易構(gòu)造。

對于同一應(yīng)用中的代碼模塊,它還應(yīng)該具備完善的單元測試,維護(hù)者通過單元測試就可以理解模塊的特性和限制,通過本地 debug 就可以理解模塊的整體設(shè)計(jì)。

John Ousterhout 教授(Raft 的發(fā)明者)的著作 《軟件設(shè)計(jì)哲學(xué)》中提到 深模塊 的概念,給我們設(shè)計(jì)模塊提供了非常好的指導(dǎo)。

深模塊是指接口簡單,但是實(shí)現(xiàn)復(fù)雜的模塊,就像我們的電腦,它看上去只是一塊簡單的板,卻隱藏了內(nèi)部復(fù)雜的功能實(shí)現(xiàn)。John 認(rèn)為設(shè)計(jì)良好的模塊都應(yīng)該是深的,設(shè)計(jì)良好的應(yīng)用應(yīng)該由深模塊組成。

從軟件調(diào)試的角度來說,接口簡單意味著它易于調(diào)試和理解,實(shí)現(xiàn)復(fù)雜意味著它能夠幫助我們屏蔽掉很多的業(yè)務(wù)復(fù)雜性,分模塊的代價是值得的。

可調(diào)試的模塊能夠讓我們修改設(shè)計(jì)模式的心理壓力大大降低,因?yàn)橛腥魏螁栴}我們都可以很快發(fā)現(xiàn)。有了這個基礎(chǔ),我們才能跟著業(yè)務(wù)去演進(jìn)我們的模式。

模式演進(jìn)

互聯(lián)網(wǎng)應(yīng)用更新迭代頻繁,因?yàn)樵O(shè)計(jì)模式不易于修改,外加模塊不好調(diào)試,很多團(tuán)隊(duì)就懶得對模式進(jìn)行演進(jìn),而是各種繞過的 “黑科技”。很多應(yīng)用都已經(jīng)發(fā)展了好幾年,用的還是系統(tǒng)剛創(chuàng)建時的模式,怎么可能還跟得上業(yè)務(wù)發(fā)展,于是就變成了人們眼中的 “過度設(shè)計(jì)”。

設(shè)計(jì)模式也是需要跟著業(yè)務(wù)演進(jìn)的。當(dāng)對未來的業(yè)務(wù)進(jìn)行規(guī)劃,也要同時對系統(tǒng)模式進(jìn)行思考,系統(tǒng)的模式是否還能跟上未來業(yè)務(wù)的規(guī)劃?在迭代中不斷探索最符合業(yè)務(wù)的設(shè)計(jì)模式。

Java8 引入的很多新特性可以幫助我們降低業(yè)務(wù)頻繁演進(jìn)時,模式的遷移成本。當(dāng)我們對是否要應(yīng)用某個模式猶豫不絕的時候,可以考慮使用函數(shù)式設(shè)計(jì)模式,以策略模式為例,在面向?qū)ο笾?,策略模式必須采用如下編碼:


interface Strategy {
    void doSomething();
}

class AStrategy implements Strategy {
    //... 代碼省略
}
class BStrategy implements Strategy {
    //... 代碼省略
}
及
// 業(yè)務(wù)代碼
class AService {
    private Map<String, Strategy> strategyMap;
    
    public void doSomething(String strategy) {
        strategyMap.get(strategy).doSomething();
    }
}

我們新建了好多類,一旦日后反悔,遷移的成本非常高。而使用函數(shù)式策略模式,我們可以將他們暫且全部寫在一起:


class AService {
    private Map<String, Runnable> strategyMap;

    static {
        strategyMap.put("a", this::aStrategy);
        strategyMap.put("b", this::bStrategy);
    }

    public void doSomething(String strategy) {
        strategyMap.get(strategy).run();
    } 
    
    private void aStrategy() {
        //...
    }
    
    private void bStrategy() {
        //...
    }
}

可以看到設(shè)計(jì)模式的函數(shù)式版本,相比面向?qū)ο蟀姹?,在隔離和封裝上相對差些,但是便捷性好一些。

所以我們可以在業(yè)務(wù)不穩(wěn)定的初期先使用函數(shù)式設(shè)計(jì)模式,利用它的便捷性快速演進(jìn),等到業(yè)務(wù)逐漸成熟,模式確定之后,再改成封裝性更好的面向?qū)ο笤O(shè)計(jì)模式。

小結(jié)

“設(shè)計(jì)模式” 作為對抗 “軟件復(fù)雜度” 惡龍的少年,可能業(yè)務(wù)發(fā)展,缺乏演進(jìn)等原因,最終自己腐壞成了新的 “惡龍”。

為了對抗設(shè)計(jì)模式的腐壞:

  • 構(gòu)造可調(diào)試的模塊,保證后來的維護(hù)者能夠通過調(diào)試快速理解設(shè)計(jì)。
  • 在業(yè)務(wù)發(fā)展中不斷探索最合適的模式。

開發(fā)效率與系統(tǒng)的成長性

在思考業(yè)務(wù)的同時,還要思考模式的演進(jìn),開發(fā)效率似乎變低了。但是這額外的時間并沒有被浪費(fèi),在設(shè)計(jì)過程也是對業(yè)務(wù)的重新思考,進(jìn)一步加深對業(yè)務(wù)的理解,編碼和業(yè)務(wù)之間必然是存在巨大的鴻溝,設(shè)計(jì)模式能夠幫助我們彌補(bǔ)這條鴻溝,演進(jìn)出和業(yè)務(wù)更加貼合的模塊,從而提升長期的效率。

復(fù)雜軟件是需要長期成長演化的。JetBrains 花了十幾年時間才讓 Idea 形成優(yōu)勢,清掃免費(fèi) IDE 占據(jù)的市場; 米哈游也用了接近十年的時間才形成足夠的技術(shù)優(yōu)勢,在市場上碾壓了同時期的競爭對手。

而設(shè)計(jì)模式就是在幫助我們對業(yè)務(wù)進(jìn)行合理的抽象,盡可能地復(fù)用,這樣系統(tǒng)可以從每個模塊地成長中收益,而不是像過程式編碼,每次都重頭開始,重復(fù)解決那些已經(jīng)解決過的問題。

舉一個我工作中的例子,釘釘審批的表單有著復(fù)雜的嵌套結(jié)構(gòu),它由控件和明細(xì)組成,而明細(xì)中又有子控件(有的控件中還有子控件,甚至還有關(guān)聯(lián)其他表單的控件,總之很復(fù)雜就對了),最初我們采用過程式編碼,每當(dāng)需要處理控件時,就手寫一遍遍歷:

// 統(tǒng)計(jì) a 控件的總數(shù)
public int countComponentAB(Form form) {
    int sum = 0;
    for (Component c: form.getComponents()) {
        if (c.getType() == "A") {
            sum++;
        } else if (c.getType == "Table") {
            // 明細(xì)控件含有子控件
            for (Component d: c.getChildren()) {
                if (d.getType() == "A") {
                    sum++;
                }
            }
        }
    }
    return sum;
}
// 返回表單中所有的 A 控件和 B 控件
public List<Component> getComponentAB(Form form) {
    List<Component> result = new ArrayList<>();
    getComponentABInner(result, form.getItems());
    return result;
}

private getComponentABInner(List<Component> result, List<Component> items) {
    for (Component c: items) {
        if (c.getType() == "A" || c.getType() == "B") {
            result.add(c);
        } else if (!c.getChildren().isEmtpy()) {
            // 遞歸訪問子控件
            getComponentABInner(result, c.getChildren());
        }
    }
}

這兩段代碼各自有點(diǎn) “小 bug”:

  • 第一段代碼只展開了一層子控件,但是審批表單是支持多層子控件的
  • 第二段代碼雖然用遞歸支持了多層子控件,但是并不是所有的子控件都屬于當(dāng)前表單(前面提到過,審批支持關(guān)聯(lián)其他比表單的控件)
    兩段代碼風(fēng)格都不一樣,因此只能分別在上面修修補(bǔ)補(bǔ),新同學(xué)來大概率還會犯相同的錯誤,此時,系統(tǒng)也就談不上 “成長”。
    但是 Visitor 模式可以幫助我們將嵌套結(jié)構(gòu)的遍歷邏輯統(tǒng)一抽象出來,使用 Visitor 模式重新編碼后的兩段代碼看起來如下:
// 統(tǒng)計(jì) a 控件的總數(shù)
class CountAVisitor extends Visitor {
    public int sum;
    @Override
    public void visitA(ComponentA a) {
        sum++;
    }
}

public int countComponentAB(Form form) {
    CountAVisitor aVisitor = new CountAVisitor();
    // 遍歷邏輯統(tǒng)一到了 accept 中
    form.accept(aVisitor);
    return aVisitor.sum;
}
// 返回表單中所有的 A 控件和 B 控件
class GetComponentABVisitor extends Visitor {
    public List<Component> result;
    @Override
    public void visitA(ComponentA a) {
        result.add(a);
    }
    @Override
    public void visitB(ComponentB b) {
        result.add(b);
    }
}

public List<Component> getComponentAB(Form form) {
    GetComponentABVisitor abVisitor = new GetComponentABVisitor();
    form.accept(abVisitor);
    return abVisitor.result;
}

對于使用者來說,雖然第一次看到這種寫法時,需要花點(diǎn)時間學(xué)習(xí)模式,和理解其中的特性,但是一旦理解之后,不僅可以快速理解所有類似代碼,還可以利用這個模塊解決所有遍歷問題,而且這個模塊是經(jīng)過驗(yàn)證,能夠健壯地解決問題。

相比之下,過程式編碼,盡管都是遍歷邏輯,每一段風(fēng)格都不一樣,每一次都要重新理解,每一段都有不一樣的特性和 bug,明明知道邏輯就在那里,但是卻無法復(fù)用,每一任維護(hù)者只能繼續(xù)踩前人踩過的坑,重復(fù)地解決問題。對于系統(tǒng)的長期成長是不利的。


成長曲線.png

因噎廢食的陷阱

軟件工程師的成長

在工程師成長的路上,有很多坎坷,“不要過度設(shè)計(jì)” 就是其中無比甜蜜的陷阱,因?yàn)樗o我們偷懶一個很好的理由,讓我們可以安然地停在五十步,反而去嘲笑已經(jīng)跑了一百步的人。

如果有兩位工程師,前者因?yàn)檫^度設(shè)計(jì)而犯錯;后者則是不進(jìn)行設(shè)計(jì),安于系統(tǒng)現(xiàn)狀,認(rèn)為 “代碼無錯就是優(yōu)”。

我認(rèn)為前者更有成長性,因?yàn)樗辽偈怯写a和技術(shù)上的追求的,只要有正確的指導(dǎo),遲早會成為一名優(yōu)秀的工程師。

最怕的是團(tuán)隊(duì)沒有人指導(dǎo),任由其自由發(fā)展,或者批評阻礙其發(fā)展。這正是CR以及評審機(jī)制的意義。

互聯(lián)網(wǎng)精耕細(xì)作的新時代

設(shè)計(jì)模式能夠幫助我們大幅度提升復(fù)雜軟件的開發(fā)與維護(hù)效率,也本文圍繞的主要命題。

但是人們總是能找出反例,“很多公司工程做得很糟糕,業(yè)務(wù)也十分成功”。

之前看紅學(xué)會的直播,對抗軟件復(fù)雜度的戰(zhàn)爭,也有人問了曉斌類似的問題,曉斌的回答是 “如果你有一片田,種啥長啥,那么你不需要耕作,只要撒種子就可以了”。

在互聯(lián)網(wǎng)野蠻發(fā)展時期,大量的人才和熱錢涌入,軟件快速上線比一切都重要,開發(fā)效率的問題,只要招聘更多的人就能解決,哪怕在一個公司開發(fā)好幾套功能一樣的系統(tǒng)。

但是隨著互聯(lián)網(wǎng)人口紅利的消失,不再有充足的資源去承接業(yè)務(wù),我們就不得不做好精耕細(xì)作的準(zhǔn)備,扎實(shí)地累積自己的產(chǎn)品和技術(shù)優(yōu)勢,繼續(xù)創(chuàng)造下一個十年的輝煌。

本文的邊界情況

真理是有條件的。

本文并非走極端地認(rèn)為所有代碼都應(yīng)該應(yīng)用模式。至少在以下情況下,是不適合用模式的:

  • 一次性腳本,沒有多次閱讀和修改的可能。
  • 真的很簡單的模塊。前文提到過 ”模塊應(yīng)該是深“,如果這個模塊真的很簡單,它或許抽象不足,我們應(yīng)該將它和其他模塊整合一下,變得更加豐滿。如果應(yīng)用中抽不出復(fù)雜模塊,那可能不是事實(shí),只是我們的實(shí)現(xiàn)方式太簡單了(比如全是過程式編碼),反過來又對外宣稱 ”我們的業(yè)務(wù)很復(fù)雜“。
  • 團(tuán)隊(duì)內(nèi)都是喜歡攀比代碼設(shè)計(jì)的瘋子,需要告誡警醒一下。真的有團(tuán)隊(duì)達(dá)到這個程度了嗎?如果到了這個程度,才可以 “反對設(shè)計(jì)”。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容