Boolan(博覽網)——C++ 設計模式(第十三周大結局!撒花!)

目錄

1. 單件模式(Singleton)

2. 享元模式(Flyweight)

3. 狀態模式(State)

4. 備忘錄(Memento)

5. 組合模式(Composite)

6. 迭代器(Iterator)

7. 職責鏈(Chain of Responsibility)

8. 命令模式(Command)

9. 訪問器(Visitor)

10. 解析器(Interpreter)

11. 設計模式總結


「對象性能」模式

  • 面向對象很好的解決了“抽象”的問題,但是必不可免地要付出一定的代價。對于通常情況來講,面向對象的成本大都可以忽略不計。但是某些情況,面向對象所帶來的成本必須謹慎處理。

  • 典型模式

    • Singleton(單件模式)
    • Flyweight(享元模式)

1. 單件模式(Singleton)

動機

  • 在軟件系統中,經常有這樣一些特殊的類,必須保證它們在系統中只存在一個實例,才能確保它們的邏輯正確性、以及良好的效率。

  • 如何繞過常規的構造器,提供一種機制來保證一個類只有一個實例?

  • 這應該是類設計者的責任,而不是使用者的責任。

保證一個類僅有一個實例,并提供一個該實例的全局訪問點。
——《設計模式》GoF

結構

筆記

  • 所有類都有構造方法,不編碼則系統默認生成空的構造方法,若有顯式定義的構造方法,默認的構造方法就會失效。

  • 通常我們可以讓一個全局變量使得一個對象被訪問,但它不能防止你實例化多個對象。一個最好的辦法就是,讓類自身負責保存它的唯一實例。這個類可以保證沒有其他實例可以被創建,并且它可以提供一個訪問該實例的方法。

  • 不同情況下的代碼及分析:


class Singleton{
private:
    Singleton();
    Singleton(const Singleton& other);
public:
    static Singleton* getInstance();
    static Singleton* m_instance;
};

Singleton* Singleton::m_instance=nullptr;

//線程非安全版本
Singleton* Singleton::getInstance() {
    if (m_instance == nullptr) {
        m_instance = new Singleton();
    }
    return m_instance;
}

/*
在單線程環境下,以上的代碼沒問題,但是在多線程的情況下會出問題。
當線程1執行到 if (m_instance == nullptr) 時,如果這時候正好線程2獲得了CPU的執行權,
那么,此時對于兩個線程來說,都檢測到了這個對象為空,
那么兩者都會創建該對象,也就是會破壞了單例的本質
*/

/*
為了解決以上多線程的問題,就出現了下面的線程安全的版本,通過鎖對象的方案來解決。
也就是說在一個線程執行到getInstance方法時,在鎖對象未被釋放前,不會交出CPU的執行權。
那么此時可以解決好多線程問題,但是另外一個問題同時產生,
那就是這樣的代碼,效率相對比較低,破壞了多線程機制。
如果在代碼部署在服務器端,在對象創建的開始時,如果有兩個客戶端訪問,
那么一個進入了鎖對象,那么他必然會獲得鎖對象,
而另一個只有等待第一個用戶完成后才能進入getIntances方法來獲取對象。
并且對于對象創建完成之后,所有的getInstance方法來說,
都是讀取這個進程,
但每次都會有一個鎖對象。那么資源是浪費的。如果高并發的情況,也會拖累效率。
 */

//線程安全版本,但鎖的代價過高
Singleton* Singleton::getInstance() {
    Lock lock;
    if (m_instance == nullptr) {
        m_instance = new Singleton();
    }
    return m_instance;
}

/*
那么,為了解決以上的問題,如果為空的情況,
也就是創建的時候才去創建鎖對象 
通過這樣的方法可以避免在讀取的時候每次都創建鎖對象。
但是在這個代碼中,必須要對所創建的對象判空兩次。
因為如果只判一次空,還是會出現線程安全的問題。
 */

/*
對于雙檢查看起來已經很好的完成了Singleton的要求和線程安全的問題。但實際上很容易出問題。

但是以上的代碼實際存在漏洞,雙檢查在內存讀寫時會出現reorder不安全的情況。

reorder:我們看代碼有一個指令序列,但代碼在匯編之后,可能在執行的時候,搶CPU的指向權的時候,可能和我們預想的不一樣。

一般m_instance = new Singleton();只想的時候我們認為是先分配內存,再調用構造函數創建對象,再把對象的地址賦值給變量。
但在CPU實際執行的時候,以上的三個步驟可能會被重新打亂順序執行。
可能會是先分配內存,然后就把內存地址直接賦值給變量,最后在調用構造函數來創建對象。
那么如果出現以上的reorder的情況,變量已經被賦值了對象的指針,但實際卻指向了沒被初始化的內存。
那么此時,線程安全問題就再次出現了。
 */

//雙檢查鎖,但由于內存讀寫reorder不安全(已經被棄用)
Singleton* Singleton::getInstance() {

    if(m_instance==nullptr){
        Lock lock;
        if (m_instance == nullptr) {
            m_instance = new Singleton();//我們以為會先分配內存,再調用構造器,最后把地址賦值給m_instance
                                         //但因為 reorder,很可能真實順序是先分配內存,地址賦值,再調用構造器
        }
    }
    return m_instance; //另一個進程可能看到 m_instance 非空,就返回了,但可能構造器還未調用,就返回了一個原生地址
}

/*
 在java和C#這類語言來說,增加了一個volatile關鍵字,通過他來修飾單例的對象,此時編譯器不會在進行reorder的優化編譯,以此保證代理的正確性。

2005年VC的編譯器自己添加了volatile關鍵字,但跨平臺的問題沒辦法解決。直到C++11后才真正的解決了這個問題,實現了跨平臺。
具體代碼如下:
 */

//C++ 11版本之后的跨平臺實現 (volatile)
std::atomic<Singleton*> Singleton::m_instance;
std::mutex Singleton::m_mutex;

Singleton* Singleton::getInstance() {
    Singleton* tmp = m_instance.load(std::memory_order_relaxed);
    std::atomic_thread_fence(std::memory_order_acquire);//獲取內存fence
    if (tmp == nullptr) {
        std::lock_guard<std::mutex> lock(m_mutex);
        tmp = m_instance.load(std::memory_order_relaxed);
        if (tmp == nullptr) {
            tmp = new Singleton;
            std::atomic_thread_fence(std::memory_order_release);//釋放內存fence
            m_instance.store(tmp, std::memory_order_relaxed);
        }
    }
    return tmp;
}

lock 是確保當一個線程位于代碼的臨界區時,另一個線程不進入臨界區。如果其他線程試圖進入鎖定的代碼,則它將一直等待(即被阻止),直到該對象被釋放。

餓漢式單件類:靜態初始化,在自己被加載時就將自己實例化。
懶漢式單件類:第一次被引用時,才會將自己實例化。

要點總結

  • Singleton 模式中的實例構造器可以設置為 protected 以允許子類派生。

  • Singleton 模式一般不要支持拷貝構造函數和 Clone 接口,因為這有可能會導致多個對象實例,與 Singleton 模式的初衷違背。

  • 如何實現多線程環境下安全的 Singleton ?注意對雙檢查鎖的正確實現。

2. 享元模式(Flyweight)

動機

  • 在軟件系統中采用純粹對象方案的問題在于大量細粒度的對象會很快充斥在系統中,從而帶來很高的運行時代價——主要指內存需求方面的代價。

  • 如何在避免大量細粒度對象問題的同時,讓外部客戶程序仍然能夠透明地使用面向對象的方式來進行操作?

運用共享技術有效地支持大量細粒度的對象。
——《設計模式》GoF

結構

筆記

  • 享元模式可以避免大量非常相似類的開銷。在程序設計中,有時需要生成大量細粒度的類實例來表示數據。如果能發現這些實例除了幾個參數外基本上都是相同的,有時就能夠大幅度地減少需要實例化的類的數量。如果能把那些參數移到類實例的外面,在方法調用時將它們傳遞進來,就可以通過共享大幅度地減少單個實例的數目。

  • 如果一個應用程序使用了大量的對象,而這些對象造成了很大的存儲開銷時就應該考慮使用該模式;還有就是對象的大多數狀態可以外部狀態,如果刪除對象的外部狀態,那么可以用相對較少的共享對象取代很多組對象,此時可以考慮使用享元模式。

要點總結

  • 面向對象很好地解決了抽象性的問題,但是作為一個運行在機器中的程序實體,我們需要考慮對象的代價問題。Flyweight 主要解決面向對象的代價問題,一般不觸及面向對象的抽象性問題。

  • Flyweight 采用對象共享的做法來降低系統中對象的個數,從而降低細粒度對象給系統帶來的內存壓力。在具體實現方面,要注意對象狀態的處理。

  • 對象的數量太大從而導致對象內存開銷加大——什么樣的數量才算大?這需要我們仔細根據具體應用情況進行評估,而不能憑空臆斷。


「狀態變化」模式

  • 在組件構建過程中,某些對象的狀態經常面臨變化,如何對這些變化進行有效的管理?同時又維持高層模塊的穩定?“狀態變化”模式為這一個問題提供了一種解決方案。

  • 典型模式

    • State
    • Memento

3. 狀態模式(State)

動機

  • 在軟件構建過程中,某些對象的狀態如果改變,其行為也會隨之而發生變化,比如文檔處于只讀狀態,其支持的行為和讀寫狀態支持的行為就可能會完全不同。

  • 如何在運行時根據對象的狀態來透明地更改對象的行為?而不會為對象操作和狀態轉化之間引入緊耦合?

允許一個對象在其內部狀態改變時改變它的行為。從而使對象看起來似乎修改了其行為。
——《設計模式》GoF

結構

筆記

  • 狀態模式主要解決的是當控制一個對象狀態轉換的條件表達式過于復雜時的情況。把狀態的判斷邏輯轉移到表示不同狀態的一系列類當中,可以把復雜的判斷邏輯簡化。

  • 狀態模式的好處是將與特定狀態相關的行為局部化,并且將不同狀態的行為分隔開來。

  • 將特定的狀態相關的行為都放入一個對象中,由于所有與狀態相關的代碼都存在于某個 ConcreteState 中,所以通過定義新的子類可以很容易地增加新的狀態和轉換。

  • 當一個對象的行為取決于它的狀態,并且它必須在運行時刻根據狀態改變它的行為時,就可以考慮使用狀態模式了。

要點總結

  • State 模式將所有與一個特定狀態相關的行為都放入一個 State 的子類對象中,在對象狀態切換時, 切換相應的對象;但同時維持 State 的接口,這樣實現了具體操作與狀態轉換之間的解耦。

  • 為不同的狀態引入不同的對象使得狀態轉換變得更加明確,而且可以保證不會出現狀態不一致的情況,因為轉換是原子性的——即要么徹底轉換過來,要么不轉換。

  • 如果 State 對象沒有實例變量,那么各個上下文可以共享同一個 State 對象,從而節省對象開銷。

4. 備忘錄(Memento)

動機

  • 在軟件構建過程中,某些對象的狀態在轉換過程中,可能由于某種需要,要求程序能夠回溯到對象之前處于某個點時的狀態。如果使用一些公有接口來讓其它對象得到對象的狀態,便會暴露對象的細節實現。

  • 如何實現對象狀態的良好保存與恢復?但同時又不會因此而破壞對象本身的封裝性。

在不破壞封裝性的前提下,捕獲一個對象的內部狀態,并在該對象之外保存這個狀態。這樣以后就可以將該對象恢復到原先保存的狀態。
——《設計模式》GoF

結構

筆記

  • Memento 模式比較適用于功能比較復雜的,但需要維護或記錄屬性歷史的類,或者需要保存的屬性只是眾多屬性中的一小部分時,Originator 可以根據保存的 Memento 信息還原到前一狀態。

要點總結

  • 備忘錄(Memento)存儲原發器(Originator)對象的內部狀態,在需要時恢復原發器的狀態。

  • Memento 模式的核心是信息隱藏,即 Originator 需要向外界隱藏信息,保持其封裝性。但同時又需要將其狀態保持到外界(Memento)。

  • 由于現代語言運行時(如 C#、java 等)都具有相當的對象序列化支持,因此往往采用效率較高、又較容易正確實現的序列化方案來實現 Memento 模式。


「數據結構」模式

  • 常常有一些組件在內部具有特定的數據結構,如果讓客戶程序依賴這些特定的數據結構,將極大地破壞組件的復用。這時候,將這些特定數據結構封裝在內部,在外部提供統一的接口,來實現與特定數據結構無關的訪問,是一種行之有效的解決方案。

  • 典型模式

    • Composite
    • Iterator
    • Chain of Responsibility

5. 組合模式(Composite)

動機

  • 軟件在某些情況下,客戶代碼過多地依賴于對象容器復雜的內部實現結構,對象容器內部實現結構(而非抽象接口)的變化將引起客戶代碼的頻繁變化,帶來了代碼的維護性、擴展性等弊端。

  • 如何將“客戶代碼與復雜的對象容器結構”解耦?讓對象容器自己來實現自身的復雜結構,從而使得客戶代碼就像處理簡單對象一樣來處理復雜的對象容器?

將對象組合成樹形結構以表示“部分-整體”的層級結構。Compisite使得用戶對單個對象和組合對象的使用具有一致性(穩定)。
——《設計模式》GoF

結構

筆記

  • 透明方式:在 Component 中聲明所有用來管理子對象的方法,其中包括 Add、Remove 等。這樣實現 Component 接口的所有子類都具備了 Add 和 Remove。這樣做的好處就是葉節點和枝節點對于外界沒有區別,它們具備完全一致的行為接口。但問題也很明顯,因為 Leaf 類本身不具備 Add()、Remove()方法的功能,所有實現它是沒有意義的。

  • 安全方式:在 Component 接口中不去聲明 Add 和 Remove 方法,那么子類的 Leaf 也就不需要去實現它,而是在 Composite 聲明所有用來管理子類對象的方法。不過由于不夠透明,所有樹葉和樹枝類將不具有相同的接口,客戶端的調用需要做相應的判斷,帶來了不便。

  • 當需求中是體現部分與整體層次的結構時,或希望用戶可以忽略組合對象與單個對象的不同,統一地使用組合結構中的所有對象時,就應該考慮使用組合模式。

要點總結

  • Composite 模式采用樹形結構來實現普遍存在的對象容器,從而將“一對多”的關系轉化為“一對一”的關系,使得客戶代碼可以一致地(復用)處理對象和對象容器,無需關心處理的是單個對象還是組合的對象容器。

  • 將“客戶代碼與復雜的對象容器結構”解耦是 Composite 的核心思想,解耦之后,客戶代碼將與純粹的抽象接口——而非對像容器的內部實現結構——發生依賴,從而更能“應對變化”。

  • Composite 模式在具體實現中,可以讓父對象中的子對象反向追溯;如果父對象有頻繁的遍歷需求,可使用緩存技巧來改善效率。

6. 迭代器(Iterator)

動機

  • 在軟件構建過程中,集合對象內部結構常常變化各異。但由于這些集合對象,我們希望在不暴露其內部結構的同時,可以讓外部客戶代碼透明地訪問其中包含的元素;同時這種“透明遍歷”也為“同一種算法在多種集合對象上進行操作”提供了可能。

  • 使用面向對象技術將這種遍歷機制抽象為“迭代器對象”,為“應對變化中的集合對象”提供了一種優雅的方式。

提供一種方法順序訪問一個聚合對象中的各個元素,而又不暴露(隔離變化,穩定)該對象的內部表示。
——《設計模式》GoF

結構

筆記

  • 當需要訪問一個聚集對象,而且不管這些對象是什么都需要遍歷的時候,就應該考慮使用迭代器模式。

  • 為遍歷不同的聚集結構提供如開始、下一個、是否結束、當前哪一項等統一的接口。

  • 迭代器模式就是分離了集合對象的遍歷行為,抽象出一個迭代器來負責,這樣既可以做到不暴露集合的內部結構,又可以讓外部代碼透明地訪問集合內部的數據。

要點總結

  • 迭代抽象:訪問一個聚合對象的內容而無需暴露它的內部表示。

  • 迭代多態:為遍歷不同的集合結構提供一個統一的接口,從而支持同樣的算法在不同的集合結構上進行操作。

  • 迭代器的健壯性考慮:遍歷的同時更改迭代器所在的集合結構,會導致問題。

7. 職責鏈(Chain of Responsibility)

動機

  • 在軟件構建過程中,一個請求可能被多個對象處理,但是每個請求在運行時只能有一個接受者,如果顯式指定,將必不可少地帶來請求發送者與接受者的緊耦合。

  • 如何使請求的發送者不需要指定具體的接受者?讓請求的接受者自己在運行時決定是否處理請求,從而使兩者解耦。

使多個對象都有機會處理請求,從而避免請求的發送者和接收者之間的耦合關系。將這些對象連成一條鏈,并沿著這條鏈傳遞請求,直到有一個對象處理它為止。
——《設計模式》GoF

結構

筆記

  • 職責鏈模式使得接收者和發送者都沒有對方的明確信息,且鏈中的對象自己也并不知道鏈的結構。結果是職責鏈可簡化對象的相互連接,它們僅需保持一個指向其后繼者的引用,而不需保持它所有的候選接受者的引用。

  • 可以隨時地增加或修改處理一個請求的結構。增強了給對象指派職責的靈活性。

  • 一個請求極有可能到了鏈的末端都得不到處理,或者因為沒有正確配置而得不到處理,需要事先考慮全面。

要點總結

  • Chain of Responsibility 模式的應用場合在于“一個請求可能有多個接受者,但是最后真正的接受者只有一個”,這時候請求發送者與接受者的耦合有可能出現“變化脆弱”的癥狀,職責鏈的目的就是將二者解耦,從而更好地應對變化。

  • 應用了職責鏈模式后,對象的職責分派將更具靈活性。我們可以在運行時動態添加/修改請求的處理職責。

  • 如果請求傳遞到職責鏈的末尾仍得不到處理,應該有一個合理的缺省機制。這也是每一個接受對象的責任,而不是發出請求的對象的責任。


“行為變化”模式

  • 在組件的構建過程中,組件行為的變化經常導致組件本身劇烈的變化。“行為變化”模式將組件的行為和組件本身進行解耦,從而支持組件行為的變化,實現兩者之間的松耦合。

  • 典型模式

    • Command
    • Visitor

8. 命令模式(Command)

動機

  • 在軟件構建過程中,“行為請求者”與“行為實現者”通常呈現一種“緊耦合”。但在某些場合——比如需要對行為進行“記錄、撤銷/重(undo/redo)、事務”等處理,這種無法抵御變化的緊耦合是不合適的。

  • 在這種情況下,如何將“行為請求者”與“行為實現者”解耦?將一組行為抽象為對象,可以實現二者之間的松耦合。

將一個請求(行為)封裝為一個對象,從而使你可用不同的請求對客戶進行參數化;對請求排隊或記錄請求日志,以及支持可撤銷的操作。
——《設計模式》GoF

結構

=

筆記

  • 命令模式的優點:
    1. 它能較容易地設計一個命令隊列
    2. 在需要的情況下,可以較容易地將命令記入日志
    3. 允許接收請求的一方決定是否要否決請求。
    4. 可以容易地實現對請求的撤銷和重做
    5. 由于加進新的具體命令類不影響其他的類,因此增加新的具體命令類很容易。
    6. 最關鍵的,把請求一個操作的對象與知道怎么執行一個操作的對象分割開

要點總結

  • Command 模式的根本目的在于將“行為請求者”與“行為實現者”解耦,在面向對象的語言中,常見的實現手段是“將行為抽象為對象”。

  • 實現 Command 接口的具體命令對象 ConcreteCommand 有時候根據需要可能會保存一些額外的狀態信息。通過使用 Composite 模式,可以將多個“命令”封裝為一個“復合命令” MacroCommand。

  • Command 模式與 C++ 中的函數對象有些類似。但兩者定義行為接口的規范有所區別:Command 以面向對象中的“接口-實現”來定義行為接口規范,更嚴格,但有性能損失;C++ 函數對象以函數簽名來定義行為接口規范,更靈活,性能更高。

9. 訪問器(Visitor)

動機

  • 在軟件構建的過程中,由于需求的改變,某些類層次結構中常常需要增加新的行為(方法)。如果直接在類中做這樣的更改,將會給子類帶來很繁重的變更負擔,甚至破壞原有設計。

  • 如何在不更改類層次結構的前提下,在運行時根據需要透明地為類層次結構上的各個類動態添加新的操作,從而避免上述問題?

表示一個作用于某對象結構中的各元素的操作。使得可以在不改變(穩定)各元素的類的前提下定義(擴展)作用于這些元素的新操作(變化)。
——《設計模式》GoF

結構

筆記

  • 訪問者模式適用于數據結構相對穩定的系統。它把數據結構和作用于結構上的操作之間的耦合解脫開,使得操作集合可以相對自由地演化。

  • 訪問者模式的目的是要把處理從數據結構分離出來。

  • 訪問者模式的優點是增加新的操作很容易,因為增加新的操作就意味著增加一個新的訪問者。訪問者模式將有關的行為集中到一個訪問者對象中。

  • 訪問者模式的缺點就是增加新的數據結構變得困難了。

要點總結

  • Vistor 模式通過所謂的雙重分發(double dispatch)來實現在不更改(不添加新的操作-編譯時)Element 類層次結構的前提下,在運行時透明地為類層次結構上的各個類動態添加新的操作(支持變化)。

  • 所謂雙重分發即 Vistor 模式中包括了兩個多態分發(注意其中的多態機制):第一個 accept 方法的多態解析;第二個為 visitElementX 方法的多態解析。

  • Visitor 模式的最大缺點在于擴展類層次結構(增添新的 Element 子類),會導致 Visitor 類的改變。因此 Visitor 模式適用于“Element 類層次結構穩定,而其中的操作卻經常面臨頻繁改動”。


「領域規則」模式

  • 在特定領域中,某些變化雖然頻繁,但可以抽象為某種規則。這時候,結合特定領域,將問題抽象為語法規則,從而給出在該領域下的一般性解決方案。

  • 典型模式

    • Interpreter

10. 解析器(Interpreter)

動機

  • 在軟件構建過程中,如果某一特定領域的問題比較復雜,類似的結構不斷重復出現,如果使用普通的編程方式來實現將面臨非常頻繁的變化。

  • 在這種情況下,將特定領域的問題表達為某種語法規則下的句子,然后構建一個解析器來解釋這樣的句子,從而達到解決問題的目的。

給定一個語言,定義它的文法的一種表示,并定義一種解釋器,這個解釋器使用該表示來解釋語言中的句子。
——《設計模式》GoF

結構

筆記

  • 當有一個語言需要解釋執行,并且你可將該語言中的句子表示為一個抽象語法樹時,可使用解釋器模式。

  • 解釋器模式為文法中的每一條規則至少定義了一個類,因此包含許多規則的文法可能難以管理和維護。建議當文法非常復雜時,使用其他的技術如語法分析程序或編譯器生成器來處理。

要點總結:

  • Interpreter 模式的應用場合是 Interpreter 模式應用中的難點,只有滿足“業務規則頻繁變化,且類似的結構不斷重復出現,并且容易抽象為語法規則的問題”才適合使用 Interpreter 模式。

  • 使用 Interpreter 模式來表示文法規則,從而可以使用面向對象技巧來方便地“擴展”文法。

  • Interpreter 模式比較適合簡單的文法表示,對于復雜的文法表示,Interpreter 模式會產生比較大的類層次結構,需要求助于語法分析生成器這樣的標準工具。


11. 設計模式總結

一個目標

管理變化,提高復用!

兩種手段

分解 VS. 抽象

八大原則(爛熟于心!)

  • 依賴倒置原則(DIP)
  • 開放封閉原則(OCP)
  • 單一職責原則(SRP)
  • Liskov 替換原則(LSP)
  • 接口隔離原則(ISP)
  • 對象組合優于類繼承
  • 封裝變化點
  • 面向接口編程

重構技法

  • 靜態 → 動態
  • 早綁定 → 晚綁定
  • 繼承 → 組合
  • 編譯時依賴 → 運行時依賴
  • 緊耦合 → 松耦合

從封裝變化角度對模式分類

C++ 對象模型

關注變化點和穩定點

什么時候不用模式

  • 代碼可讀性很差時(不要好高騖遠,勿以浮沙筑高臺!
  • 需求理解還很淺時
  • 變化沒有顯現時
  • 不是系統的關鍵依賴點
  • 項目沒有復用價值時(外包神馬的)
  • 項目將要發布時

經驗之談

  • 不要為模式而模式
  • 關注抽象類接口
  • 理清變化點穩定點
  • 審視依賴關系
  • 要有 Framework 和 Application 的區隔思維
  • 良好的設計是演化的結果

設計模式成長之路

  • 「手中無劍,心中無劍」:見模式而不知
  • 「手中有劍,心中無劍」:可以識別模式,作為應用開發人員使用(目前所處階段,要繼續加油呦~!)
  • 「手中有劍,心中有劍」:作為框架開發人員為應用設計某些模式
  • 「手中無劍,心中有劍」:忘掉模式,只有原則(天下大同)

祝工作順利,學習愉快,早日財務自由,踏入幸福之路,謝謝大家!

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容