重學 Java 設計模式:實戰備忘錄模式「模擬網際網路系統上線過程中,配置檔案回滾場景」

小傅哥發表於2020-06-29


作者:小傅哥
部落格:https://bugstack.cn - 原創系列專題文章

沉澱、分享、成長,讓自己和他人都能有所收穫!?

一、前言

實現不了是研發的藉口?

實現不了,有時候是功能複雜度較高難以實現,有時候是工期較短實現不完。而編碼的行為又是一個不太好量化的過程,同樣一個功能每個人的實現方式不一樣,遇到開發問題解決問題的速度也不一樣。除此之外還很不好給產品解釋具體為什麼要這個工期時間,這就像蓋樓的圖紙最終要多少水泥砂漿一樣。那麼這時研發會盡可能的去通過一些經驗,制定流程規範、設計、開發、評審等,確定一個可以完成的時間範圍,又避免風險的時間點後。再被壓縮,往往會出一些矛盾點,能壓縮要解釋為什麼之前要那麼多時間,不能壓縮又有各方不斷施加的壓力。因此有時候不一定是藉口,是要考慮如何讓整個團隊健康的發展。

鼓勵有時比壓力要重要!

在學習的過程中,很多時候我們聽到的都是,你要怎樣,怎樣,你瞧瞧誰誰誰,哪怕今天聽不到這樣的聲音了,但因為曾經反覆聽到過而導致內心抗拒。雖然也知道自己要去學,但是很難堅持,學著學著就沒有了方向,看到還有那麼多不會的就更慌了,以至於最後心態崩了,更不願意學。其實程式設計師的壓力並不小,想成長几乎是需要一直的學習,就像似乎再也不敢說精通java了一樣,知識量實在是隨著學習的深入,越來越深,越來越廣。所以需要,開心學習,快樂成長!

臨陣的你好像一直很著急!

經常的聽到;老師明天就要了你幫我弄弄吧你給我寫一下完事我就學這次著急現在這不是沒時間學嗎快給我看看。其實看到的類似的還有很多,很納悶你的著急怎麼來的,不太可能,人在家中坐,禍從天上落。老師怎麼就那個時間找你了,老闆怎麼就今天管你要了,還不是日積月累你沒有學習,臨時抱佛腳亂著急!即使後來真的有人幫你了,但最好不要放鬆,要儘快學會,躲得過初一還有初二呢!

二、開發環境

  1. JDK 1.8
  2. Idea + Maven
  3. 涉及工程一個,可以通過關注公眾號bugstack蟲洞棧,回覆原始碼下載獲取(開啟獲取的連結,找到序號18)
工程 描述
itstack-demo-design-17-00 開發配置檔案備忘錄

三、備忘錄模式介紹

備忘錄模式,圖片來自 refactoringguru.cn

備忘錄模式是以可以恢復或者說回滾,配置、版本、悔棋為核心功能的設計模式,而這種設計模式屬於行為模式。在功能實現上是以不破壞原物件為基礎增加備忘錄操作類,記錄原物件的行為從而實現備忘錄模式。

這個設計在我們平常的生活或者開發中也是比較常見的,比如:後悔藥、孟婆湯(一下回滾到0),IDEA編輯和撤銷、小霸王遊戲機存檔。當然還有我們非常常見的Photoshop,如下;

Photoshop 歷史記錄

四、案例場景模擬

場景模擬;系統釋出上線配置回滾

在本案例中我們模擬系統在釋出上線的過程中記錄線上配置檔案用於緊急回滾

在大型網際網路公司系統的釋出上線一定是易用、安全、可處理緊急狀況的,同時為了可以隔離線上和本地環境,一般會把配置檔案抽取出來放到線上,避免有人誤操作導致本地的配置內容釋出出去。同時線上的配置檔案也會在每次變更的時候進行記錄,包括;版本號、時間、MD5、內容資訊和操作人。

在後續上線時如果發現緊急問題,系統就會需要回滾操作,如果執行回滾那麼也可以設定配置檔案是否回滾。因為每一個版本的系統可能會隨著帶著一些配置檔案的資訊,這個時候就可以很方便的讓系統與配置檔案一起回滾操作。

我們接下來就使用備忘錄模式,模擬如何記錄配置檔案資訊。實際的使用過程中還會將資訊存放到庫中進行儲存,這裡暫時只是使用記憶體記錄。

五、備忘錄模式記錄配置檔案版本資訊

備忘錄的設計模式實現方式,重點在於不更改原有類的基礎上,增加備忘錄類存放記錄。可能平時雖然不一定非得按照這個設計模式的程式碼結構來實現自己的需求,但是對於功能上可能也完成過類似的功能,記錄系統的資訊。

除了現在的這個案例外,還可以是運營人員在後臺erp建立活動對資訊的記錄,方便運營人員可以上下修改自己的版本,而不至於因為誤操作而丟失資訊。

1. 工程結構

itstack-demo-design-17-00
└── src
    ├── main
    │   └── java
    │       └── org.itstack.demo.design
    │           ├── Admin.java
    │           ├── ConfigFile.java
    │           ├── ConfigMemento.java
    │           └── ConfigOriginator.java
    └── test
        └── java
            └── org.itstack.demo.design.test
                └── ApiTest.java

備忘錄模式模型結構

備忘錄模式模型結構

  • 以上是工程結構的一個類圖,其實相對來說並不複雜,除了原有的配置類(ConfigFile)以外,只新增加了三個類。
  • ConfigMemento:備忘錄類,相當於是對原有配置類的擴充套件
  • ConfigOriginator:記錄者類,獲取和返回備忘錄類物件資訊
  • Admin:管理員類,用於操作記錄備忘資訊,比如你一些列的順序執行了什麼或者某個版本下的內容資訊

2. 程式碼實現

2.1 配置資訊類

public class ConfigFile {

    private String versionNo; // 版本號
    private String content;   // 內容
    private Date dateTime;    // 時間
    private String operator;  // 操作人
    
    // ...get/set
}
  • 配置類可以是任何形式的,這裡只是簡單的描述了一個基本的配置內容資訊。

2.2 備忘錄類

public class ConfigMemento {

    private ConfigFile configFile;

    public ConfigMemento(ConfigFile configFile) {
        this.configFile = configFile;
    }

    public ConfigFile getConfigFile() {
        return configFile;
    }

    public void setConfigFile(ConfigFile configFile) {
        this.configFile = configFile;
    }
    
}
  • 備忘錄是對原有配置類的擴充套件,可以設定和獲取配置資訊。

2.3 記錄者類

public class ConfigOriginator {

    private ConfigFile configFile;

    public ConfigFile getConfigFile() {
        return configFile;
    }

    public void setConfigFile(ConfigFile configFile) {
        this.configFile = configFile;
    }

    public ConfigMemento saveMemento(){
        return new ConfigMemento(configFile);
    }

    public void getMemento(ConfigMemento memento){
        this.configFile = memento.getConfigFile();
    }

}
  • 記錄者類除了對ConfigFile配置類增加了獲取和設定方法外,還增加了儲存saveMemento()、獲取getMemento(ConfigMemento memento)
  • saveMemento:儲存備忘錄的時候會建立一個備忘錄資訊,並返回回去,交給管理者處理。
  • getMemento:獲取的之後並不是直接返回,而是把備忘錄的資訊交給現在的配置檔案this.configFile,這部分需要注意。

2.4 管理員類

public class Admin {

    private int cursorIdx = 0;
    private List<ConfigMemento> mementoList = new ArrayList<ConfigMemento>();
    private Map<String, ConfigMemento> mementoMap = new ConcurrentHashMap<String, ConfigMemento>();

    public void append(ConfigMemento memento) {
        mementoList.add(memento);
        mementoMap.put(memento.getConfigFile().getVersionNo(), memento);
        cursorIdx++;
    }

    public ConfigMemento undo() {
        if (--cursorIdx <= 0) return mementoList.get(0);
        return mementoList.get(cursorIdx);
    }

    public ConfigMemento redo() {
        if (++cursorIdx > mementoList.size()) return mementoList.get(mementoList.size() - 1);
        return mementoList.get(cursorIdx);
    }

    public ConfigMemento get(String versionNo){
        return mementoMap.get(versionNo);
    }

}
  • 在這個類中主要實現的核心功能就是記錄配置檔案資訊,也就是備忘錄的效果,之後提供可以回滾和獲取的方法,拿到備忘錄的具體內容。
  • 同時這裡設定了兩個資料結構來存放備忘錄,實際使用中可以按需設定。List<ConfigMemento>Map<String, ConfigMemento>
  • 最後是提供的備忘錄操作方法;存放(append)、回滾(undo)、返回(redo)、定向獲取(get),這樣四個操作方法。

3. 測試驗證

3.1 編寫測試類

@Test
public void test() {
    Admin admin = new Admin();
    ConfigOriginator configOriginator = new ConfigOriginator();
    configOriginator.setConfigFile(new ConfigFile("1000001", "配置內容A=哈哈", new Date(), "小傅哥"));
    admin.append(configOriginator.saveMemento()); // 儲存配置
    configOriginator.setConfigFile(new ConfigFile("1000002", "配置內容A=嘻嘻", new Date(), "小傅哥"));
    admin.append(configOriginator.saveMemento()); // 儲存配置
    configOriginator.setConfigFile(new ConfigFile("1000003", "配置內容A=麼麼", new Date(), "小傅哥"));
    admin.append(configOriginator.saveMemento()); // 儲存配置
    configOriginator.setConfigFile(new ConfigFile("1000004", "配置內容A=嘿嘿", new Date(), "小傅哥"));
    admin.append(configOriginator.saveMemento()); // 儲存配置  

    // 歷史配置(回滾)
    configOriginator.getMemento(admin.undo());
    logger.info("歷史配置(回滾)undo:{}", JSON.toJSONString(configOriginator.getConfigFile()));  

    // 歷史配置(回滾)
    configOriginator.getMemento(admin.undo());
    logger.info("歷史配置(回滾)undo:{}", JSON.toJSONString(configOriginator.getConfigFile()));  

    // 歷史配置(前進)
    configOriginator.getMemento(admin.redo());
    logger.info("歷史配置(前進)redo:{}", JSON.toJSONString(configOriginator.getConfigFile()));   

    // 歷史配置(獲取)
    configOriginator.getMemento(admin.get("1000002"));
    logger.info("歷史配置(獲取)get:{}", JSON.toJSONString(configOriginator.getConfigFile()));
}
  • 這個設計模式的學習有一部分重點是體現在了單元測試類上,這裡包括了四次的資訊儲存和備忘錄歷史配置操作。
  • 通過上面新增了四次配置後,下面分別進行操作是;回滾1次再回滾1次之後向前進1次最後是獲取指定的版本配置。具體的效果可以參考測試結果。

3.2 測試結果

23:12:09.512 [main] INFO  org.itstack.demo.design.test.ApiTest - 歷史配置(回滾)undo:{"content":"配置內容A=嘿嘿","dateTime":159209829432,"operator":"小傅哥","versionNo":"1000004"}
23:12:09.514 [main] INFO  org.itstack.demo.design.test.ApiTest - 歷史配置(回滾)undo:{"content":"配置內容A=麼麼","dateTime":159209829432,"operator":"小傅哥","versionNo":"1000003"}
23:12:09.514 [main] INFO  org.itstack.demo.design.test.ApiTest - 歷史配置(前進)redo:{"content":"配置內容A=嘿嘿","dateTime":159209829432,"operator":"小傅哥","versionNo":"1000004"}
23:12:09.514 [main] INFO  org.itstack.demo.design.test.ApiTest - 歷史配置(獲取)get:{"content":"配置內容A=嘻嘻","dateTime":159320989432,"operator":"小傅哥","versionNo":"1000002"}

Process finished with exit code 0
  • 從測試效果上可以看到,歷史配置按照我們的指令進行了回滾和前進,以及最終通過指定的版本進行獲取,符合預期結果。

六、總結

  • 此種設計模式的方式可以滿足在不破壞原有屬性類的基礎上,擴充了備忘錄的功能。雖然和我們平時使用的思路是一樣的,但在具體實現上還可以細細品味,這樣的方式在一些原始碼中也有所體現。
  • 在以上的實現中我們是將配置模擬存放到記憶體中,如果關機了會導致配置資訊丟失,因為在一些真實的場景裡還是需要存放到資料庫中。那麼此種存放到記憶體中進行回覆的場景也不是沒有,比如;Photoshop、運營人員操作ERP配置活動,那麼也就是即時性的一般不需要存放到庫中進行恢復。另外如果是使用記憶體方式存放備忘錄,需要考慮儲存問題,避免造成記憶體大量消耗。
  • 設計模式的學習都是為了更好的寫出可擴充套件、可管理、易維護的程式碼,而這個學習的過程需要自己不斷的嘗試實際操作,理論的知識與實際結合還有很長一段距離。切記多多上手!

七、推薦閱讀

相關文章