面試的時候別再說你不會設計模式了

crossoverJie發表於2022-12-27

前言

最近在設計一個對某個中介軟體的測試方案,這個測試方案需要包含不同的測試邏輯,但相同的是需要對各個環節進行記錄;比如統計耗時、呼叫通知 API 等相同的邏輯。

如果每個測試都單獨寫這些邏輯那無疑是做了許多重複工作了。

基於以上的特徵很容易能想到模板方法這個設計模式。

這是一種有上層定義框架,下層提供不同實現的設計模式。

比如裝修房子的時候業主可以按照自己的喜好對不同的房間進行裝修,但是整體的戶型圖不能做修改,比如承重牆是肯定不能打的。

而這些固定好的條條框框就是上層框架給的約束,下層不同的實現就有業主自己決定;所以對於整棟樓來說框架都是固定好的,讓業主在有限的範圍內自由發揮也方便物業的管理。

具體實現

以我這個案例的背景為例,首先需要定義出上層框架:

Java

Event 介面:

public interface Event {

    /**
     * 新增一個任務
     */
    void addJob();

    /**
     * 單個任務執行完畢
     *
     * @param jobName    任務名稱
     * @param finishCost 任務完成耗時
     */
    void finishOne(String jobName, String finishCost);

    /**單個任務執行異常
     * @param jobDefine 任務
     * @param e 異常
     */
    void oneException(AbstractJobDefine jobDefine, Exception e);

    /**
     * 所有任務執行完畢
     */
    void finishAll();
}
    public void start() {
        event.addJob();
        try {
            CompletableFuture.runAsync(() -> {
                StopWatch watch = new StopWatch();
                try {
                    watch.start(jobName);
                    // 不同的子業務實現
                    run(client);
                } catch (Exception e) {
                    event.oneException(this, e);
                } finally {
                    watch.stop();
                    event.finishOne(jobName, StrUtil.format("cost: {}s", watch.getTotalTimeSeconds()));
                }
            }, TestCase.EXECUTOR).get(timeout, TimeUnit.SECONDS);
        } catch (Exception e) {
            event.oneException(this, e);
        }
    }

    /** Run busy code
     * @param client
     * @throws Exception e
     */
    public abstract void run(Client client) throws Exception;    

其中最核心的就是 run 函式,它是一個抽象函式,具體實現交由子類完成;這樣不同的測試用例之間也互不干擾,同時整體的流程完全相同:

  • 記錄任務數量
  • 統計耗時
  • 異常記錄

等流程。


接下來看看如何使用:

        AbstractJobDefine job1 = new Test1(event, "測試1", client, 10);
        CompletableFuture<Void> c1 = CompletableFuture.runAsync(job1::start, EXECUTOR);

        AbstractJobDefine job2 = new Test2(event, "測試2", client, 10);
        CompletableFuture<Void> c2 = CompletableFuture.runAsync(job2::start, EXECUTOR);

        AbstractJobDefine job3 = new Test3(event, "測試3", client, 20);
        CompletableFuture<Void> c3 = CompletableFuture.runAsync(job3::start, EXECUTOR);

        CompletableFuture<Void> all = CompletableFuture.allOf(c1, c2, c3);
        all.whenComplete((___, __) -> {
            event.finishAll();
            client.close();
        }).get();

顯而易見 Test1~3 都繼承了 AbstractJobDefine 同時實現了其中的 run 函式,使用的時候只需要建立不同的例項等待他們都執行完成即可。

以前在 Java 中也有不同的應用:

https://crossoverjie.top/2019/03/01/algorithm/consistent-hash/?highlight=%E6%A8%A1%E6%9D%BF%E6%96%B9%E6%B3%95#%E6%A8%A1%E6%9D%BF%E6%96%B9%E6%B3%95

Go

同樣的示例用 Go 自然也可以實現:

func TestJobDefine_start(t *testing.T) {
    event := NewEvent()
    j1 := &JobDefine{
        Event:   event,
        Run:     &run1{},
        JobName: "job1",
        Param1:  "p1",
        Param2:  "p2",
    }
    j2 := &JobDefine{
        Event:   event,
        Run:     &run2{},
        JobName: "job2",
        Param1:  "p11",
        Param2:  "p22",
    }
    j1.Start()
    j2.Start()
    for _, ch := range event.GetChan() {
        <-ch
    }
    log.Println("finish all")

}

func (r *run2) Run(param1, param2 string) error {
    log.Printf("run3 param1:%s, param2:%s", param1, param2)
    time.Sleep(time.Second * 3)
    return errors.New("test err")
}

func (r *run1) Run(param1, param2 string) error {
    log.Printf("run1 param1:%s, param2:%s", param1, param2)
    return nil
}

使用起來也與 Java 類似,建立不同的例項;最後等待所有的任務執行完畢。

總結

設計模式往往是對某些共效能力的抽象,但也沒有一個設計模式可以適用於所有的場景;需要對不同的需求選擇不同的設計模式。

至於在工作中如何進行正確的選擇,那就需要自己日常的積累了;比如多去了解不同的設計模式對於的場景,或者多去閱讀優秀的程式碼,Java 中的 InputStream/Reader/Writer 這類 IO 相關的類都有具體的應用。

相關文章