「論道架構師」優雅解決歷史代碼中的新需求

⚠️本文爲掘金社區首發簽約文章,未獲受權禁止轉載java


事件原由

6月中旬,可愛的產品大大給我提了一個臨時需求,須要我對商品建立/更新業務中由開放平臺對接而來的請求作一個Check,若是符合要求,則再作一段稍微複雜的邏輯處理編程

這種Easy程度的需求怎麼攔得住我,不到半天我就Coding,Push一鼓作氣,正當我準備點一杯喜茶開始摸魚的時候,我卻收到了一封郵件。設計模式

郵件裏有一堆的漢字和英文,但有幾個字赫然在目:緩存

您的代碼已被駁回。markdown

當我經歷了茫然、震驚、不敢相信、最後無奈接受的情緒轉變後,問了評審的同事,爲何要駁回個人代碼,他說:「歷史代碼通常業務都很完整(跟屎山同樣了...),那若是有新的需求不得不依賴它的話,怎麼架構

作纔是最佳方案,讓代碼有更好的拓展性,你有想過嗎?」。app

我確定是沒有想的,因而乎,我懷着些許愧疚的心情,找到了架構師,但願他能爲我指點迷津。異步


找一個看起來合適的位置塞進去

亮架構:Kerwin,這段代碼是否是偷懶了?ide

try {
   // 忽略歷史業務代碼,如下爲新增內容 
} catch (Exception) {
   // TODO
} finally {
    SkuMainBean retVal = skuMainBridgeService.updateSkuBridgeMainBean(skuMainBean);
    if(retVal != null){
        // 商品建立/修改異步處理邏輯
        SimpleThreadPool.executeRunnable(SimpleThreadPool.ThreadPoolName.openSkuHandle, () -> {
            skuOperateBusinessService.checkOpenSkuReview(retVal);
        });
    }
}
複製代碼

我(雖然我以爲不妥,但仍是強裝鎮定):沒偷懶啊,你看這塊業務代碼既沒有影響原功能,又用線程池的方式異步處理,不會影響總體接口效率,並且還把複雜邏輯都封裝到了Business層裏,這還叫偷懶嗎?函數

亮架構:你以爲這個商品建立/修改流程重要嗎?是否是我們的最核心的流程?下次產品再提新的需求,繼續 if 而後疊羅漢嗎?我咋記得你說過你最討厭在代碼裏看到 if 呢?

我(小聲):我討厭看到別人的 if,可是本身的仍是能夠接受的...

亮架構(氣笑):不跟你耍貧嘴了,一塊兒想一想怎麼改吧。

PS:【找一個看起來合適的位置塞進去】這種方式是咱們使用最頻繁,影響面相對較小,開發效率最高的方式了,但它帶來的問題就是後期很差維護,並且隨着需求變多,它就會變得和疊羅漢同樣,原本一個很簡單的方法函數,會變成百上千行的 「屎山」,所以須要酌情使用。


優先校驗終止

我(開始思考):若是需求是不知足某種狀況便可終止執行,那這種狀況可太簡單了,就不絮叨了。

亮架構:其實仍是有一點可說的,好比你須要在不知足時返回標識符結果加細節緣由,你怎麼處理?

:直接定義一個字符串而後返回,後續判斷字符串是否爲NULL便可。

亮架構:若是就是失敗了,且緣由也爲NULL或空字符串呢?其實咱們利用泛型有更優雅的解決方案,好比這樣定義一個元組:

public class ValueMsgReturn<A, B> {
    /** 結果 **/
    private final A value;

    /** 緣由 **/
    private final B msg;

    public ValueMsgReturn(A value, B msg) {
        this.value = value;
        this.msg = msg;
    }

    // 省略Get方法
}
複製代碼

這樣作的好處是,通用,簡單,沒必要定義重複的對象,你本身在代碼中試試就能明白它有多香,總體代碼就以下所示:

// 省略干擾代碼
ValueMsgReturn<Boolean, String> check = check();
if (check.getValue()) {
    return check.getValue();
}
複製代碼

PS:此種狀況較爲簡單,但仍然有技巧優化代碼,詳情請見歷史文章:

「奇淫技巧」如何寫最少的代碼


簡單觀察者模式

我(繼續思考):你剛那種狀況太簡單了,迴歸正題,我們這個需求可使用觀察者模式解耦啊!

亮架構(猶豫道):不是不能夠,但你想一下咱們須要改動哪些代碼吧。

:觀察者的核心即通知方 + 處理方,若是咱們使用JDK自帶的觀察者模式的話,改動以下:

  1. 須要將歷史代碼中的類繼承Observable
  2. 新的處理方法基於單一原則抽象成單獨的類,實現Observer接口
  3. 在類初始化時把兩者構建好通知關係

亮架構:若是一段邏輯在設計之初就採用觀察者模式的話,那還不錯,但歷史代碼則不適合,由於它一個類裏面包含大量的其餘方法,若是將來需求中有第二種須要通知的狀況,代碼就會更難維護,畢竟JDK觀察者模式是須要繼承Observable類的,固然了,做爲一個備選方案也不是不行。

PS:以上描述的JDK觀察者模式對應的是JDK1.8版本,關於觀察者模式的詳情,請見歷史文章

【一塊兒學系列】之觀察者模式:我沒有在監控你啊


AOP

我(忽然想起來):亮架構,你說用AOP來處理合適嗎?

亮架構:通常狀況下咱們用AOP來作什麼動做呢?

:個人話,通常會用做權限處理、日誌打印、緩存同步、特殊場景計數等等。

亮架構:是的,你想象一下若是咱們把這些業務邏輯都堆在切面裏會是什麼樣子?一個切點還好,兩個呢,十個呢?你們拿到新項目的時候都會參考前人的代碼風格,若是你開了一個壞的頭,其餘人就會跟着作一樣的事,很快代碼就會變成如同蜘蛛網通常,因此這種方式必定是要杜絕的。


MQ 解耦

我(忽然想起來):對了,我們的商品新建/修改都會有MQ的,我只用監聽MQ而後作業務處理就行了。

亮架構:這個確定是可行的,就是有點殺雞焉用宰牛刀的感受,畢竟咱們須要處理的狀況只是MQ中的一小部分,並且萬一歷史代碼沒有發送MQ怎麼辦呢?


Spring Event

亮架構:你有了解過Spring Event嗎?

:之前研究過,確實用在這裏還蠻合適的。

PS:Spring Event是Spring體系中的事件通知機制,其原理能夠理解爲Spring實現的觀察者模式。

注:上文中的簡單觀察者模式指的是JDK(1.8)實現的觀察者模式。

// 如下爲Demo代碼
@RestController
public class EventRequest implements ApplicationContextAware {

    private ApplicationContext appContext;

    @RequestMapping("/testEvent")
    public String testEventSave(String name) {
        appContext.publishEvent(new User(this, name));
        return "ok";
    }

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        appContext = applicationContext;
    }
}
複製代碼
// 監聽者
@Component
public class WebEventListener {

    /** * 僅監聽字段值爲 foo 時,類爲 User.class 時 */
    @EventListener(classes = User.class, condition = "#event.name == 'foo'")
    public void listen(User event){
        // TODO
    }

    /** * 監聽 User.class 狀況 */
    @EventListener(classes = User.class)
    public void listen1(User event){
        // TODO
    }
}
複製代碼

亮架構:是的,這個Demo就很能反映它的優點之處了

  1. 咱們能夠在單一方法內Publish多個事件,互不干擾
  2. 監聽者能夠基於表達式進行基本的過濾
  3. 一個事件能夠被重複監聽

:是的,並且它還能夠支持異步事件處理!

亮架構(停頓了一下):你以爲支持異步是它獨特的優點嗎?哈哈哈,即便是同步監聽到事件,你只要用線程池異步處理就行了。可以自然異步化,只是錦上添花的東西,不要弄混了哦。固然了,每種技術和特性都有其獨特的使用場景,在使用的時候須要注意它的特殊狀況,好比:

  1. 業務上是否容許異步處理(即便是延遲了比較久的時間)
  2. 可否徹底相信事件通知裏面的參數,是否須要反查等等。

還有別的方式嗎

我(開心):若是我用Spring Event的話,我只須要稍微改動一下就行了,代碼的拓展性,可維護性一會兒就上來了,不過剛我們聊了那麼多方式方法,怎麼感受全是觀察者模式啊?

亮架構:是的,不管是JDK的仍是Spring,亦或是AOP、MQ,這些通通都是觀察者模式的思想,畢竟觀察者模式的特色就是解耦

:難道不能用別的設計模式思想嗎?

亮架構:固然能夠,就是改動可能略大一點,畢竟這個類都快幾千行了,仍是儘可能少加東西了。

:好比呢,能夠用什麼其餘的方式?

亮架構:額...你既然想聽的話,能夠這樣,回顧一下你最初的代碼:

finally {
    SkuMainBean retVal = skuMainBridgeService.updateSkuBridgeMainBean(skuMainBean);
    if(retVal != null){
        // 商品建立/修改異步處理邏輯
        SimpleThreadPool.executeRunnable(SimpleThreadPool.ThreadPoolName.openSkuHandle, () -> {
            skuOperateBusinessService.checkOpenSkuReview(retVal);
        });
    }
}
複製代碼

在這個業務方法裏處理的確定是skuMainBean對象,由於整個方法都是在操做它,那咱們徹底能夠抽象出一個個策略類,而後利用工廠來處理,好比改爲這樣:

// 修改後代碼
finally {
    skuMainBeanFactory.checkAndHandle(skuMainBean);
}

// 工廠方法
public void checkAndHandle (SkuMainBean skuMainBean) {
    for (策略集合: 策略) {
        if (check(skuMainBean)) {
        	// TODO
        }
	}
}
複製代碼

亮架構:你看這樣是否是也具備很好的拓展性?

我(興奮):是的,我忽然感受這種方式和SpringEvent有殊途同歸之妙!

亮架構(笑了笑)孺子可教也,這種策略+工廠的方式是基於接口編程,經過check方法判斷是否須要處理,而SpringEvent說白了是經過事件的傳播,即方法直接調用來判斷是否須要處理,本質都是同樣的,那你知道將來的新需求你該怎麼寫了嗎?

我(興奮):我知道了,要寫可拓展性的代碼,像我今天改的這種代碼就不行,太垃圾了!

亮架構(搖了搖頭,起身走了)Kerwin,你錯了,你今天改的歷史代碼在當時能夠說是最佳實踐了,只是由於你遇到了以前的設計者未考慮到的問題而已。咱們講設計模式、講七大原則,講不要過分設計,就是爲了你如今出現的狀況,咱們在編碼過程當中可能會遇到千奇百怪的代碼,咱們能夠抱怨,能夠吐槽,但記住,不要爲了某些需求就把原本漂亮的代碼變成屎山。因此你須要去學習編程的思想,學習設計的思想。

我(大聲)那,架構師!若是有一段代碼已經爛到不能再爛了呢!


「那就把它重構了!而後把做者的名字記下來,狠狠的吐槽他!🤪」

最後

回顧全文作一個總結,若是你的需求是容許前置校驗返回的,那麼絕不猶豫的CheckAndReturn便可!可是,若是你的需求和我同樣,那麼推薦如下幾種方案:

  1. 利用MQ解耦
  2. 利用SpringEvent解耦
  3. 自行根據當前需求和將來可能的需求考慮是否須要策略類
  4. 終極方案:真正理解編程的七大原則及經常使用的設計模式、隨機應變便可

那麼請容許我推薦一下以前的文章:設計模式總篇:從爲何須要原則到實際落地

若是你以爲這篇內容對你有幫助的話:

  1. 固然要點贊支持一下啦~
  2. 另外,能夠搜索並關注公衆號「是Kerwin啊」,一塊兒在技術的路上走下去吧~ 😋
相關文章
相關標籤/搜索