《大話設計模式》這本書買了很久,但是一直沒有堅持看完。畢業之后,丟了很多書在學校,但是這本書沒有舍得丟下。現在看來當初是明智的,再次閱讀的時候,才發現這本書淺顯易懂,有趣,通過一個個小故事,緩緩道來設計模式的妙用。在閱讀的時候,有很多的收獲,其實說的這些模式,雖然沒有真正的去了解,但是發現在開發過程中,很多都是常用到的,沒有總結出來而已。在閱讀的過程中,很多時候有感,但是又覺得差一點就能突破。在這里做下閱讀筆記,算是閱讀的輸出。
原書中的代碼都是用C#是實現的,在閱讀的過程中,我根據對Python的掌握進行了轉換。 demo倉庫地址
設計模式的基本原則
開放-封閉原則
對于拓展是開放的,對于更改是封閉的。
無論模塊是多么的封閉,都會存在一些無法對之封閉的變化,既然不可能完全封閉,設計人員必須對于他設計的模塊應該對哪種變化做出選擇。他必須先猜測出最有可能發生的種類,然后構造抽象來隔離那些變化。
面對需求,對程序的改動是通過新代碼進行的,而不是更改現有的代碼。
拒絕不成熟的抽象和抽象本省一樣重要。
單一職責原則
就一個類而言,應該僅有一個引起它變化的原因。
軟件設計真正要做的許多內容,就是發現職責并把這些職責分離。其實要去判斷是否應該分離出類來,也不難,那就是你能夠想到對于一個動機去改變一個類,那么這個類就是具有多于一個的職責。
依賴倒轉原則
A、高層模塊不應該依賴低層模塊,兩個都應該依賴抽象
B、抽象不應該依賴細節。細節應該依賴抽象。
依賴倒轉原則其實可以說是面向對象設計的標志,用哪種語言來寫程序不重要,如果編寫時,考慮都是如何針對抽象編程而不是針對細節編程,即程序中所有的依賴關系都是終止于抽象類,或者接口,那就是面向對象的設計,反之就是過程化的設計了。
里氏替換原則
子類必須能夠替換掉它們的父類。換句話你說,任何基類可以出現的地方,子類一定可以出現。 里氏替換原則是繼承復用的基石,只有當衍生類可以替換基類,軟件單位的功能不受到影響時,基類才能真正被復用,而衍生類也能夠在基類的基礎上增加新的行為。
里氏代換原則是對“開-閉”原則的補充。實現“開閉”原則的關鍵步驟就是抽象化。而基類與子類的繼承關系就是抽象化的具體實現,所以里氏代換原則是對實現抽象化的具體步驟的規范。里氏替換原則中,子類對父類的方法盡量不要重寫和重載。因為父類代表了定義好的結構,通過這個規范的接口與外界交互,子類不應該隨便破壞它。
迪米特法則(最少知道原則)
一個類對自己依賴的類知道的越少越好。
無論被依賴的類多么復雜,都應該將邏輯封裝在方法的內部,通過公用方法提供給外部。這樣當被依賴的類變化時,才能最少的影響該類。
最少知道原則的另外一個表達方式是:只與直接的朋友通信,類之間只要有耦合關系,就叫朋友關系。耦合分為依賴、關聯、聚合、組合等。我們稱出現為成員變量、方法參數、方法返回值中的類為直接朋友。局部變量、臨時變量則不是直接關系。我們要求陌生的類不要作為局部變量出現在類中。
迪米特法則是面向對象的根本思想,是強調了類的松耦合關系。類之間的耦合越弱,越有利于復用,一個處在弱耦合的類被修改,不會對關系的類造成波及。
合成復用原則
盡量首先使用合成\聚合的方式,而不是使用繼承。
設計模式分類:
- 創建型模式,共五種:工廠方法模式、抽象工廠模式、單例模式、建造者模式、原型模式。
- 結構型模式,共七種:適配器模式、裝飾器模式、代理模式、外觀模式、橋接模式、組合模式、享元模式。
- 行為型模式,共十一種:策略模式、模板方法模式、觀察者模式、迭代器模式、職責鏈模式、命令模式、備忘錄模式、狀態模式、訪問者模式、中介者模式、解釋器模式。
參考
設計模式詳述
源碼倉庫https://github.com/suAdminWen/studyForPython/tree/master/design_patterns/dahua
簡單工廠模式
>>> oper = OperationFactory.create_operate('+')
>>> oper.number1 = 12
>>> oper.number2 = 13
>>> oper.get_result()
25
>>> oper = OperationFactory.create_operate('/')
>>> oper.number1 = 12
>>> oper.number2 = 0
>>> oper.get_result()
Traceback (most recent call last):
...
ValueError: 除數不能為0
策略模式
策略模式:它定義了算法家族,分別封裝起來,讓他們之間可以相互替換,此模式讓算法的變化,不會影響到使用算法的客戶。
>>> csuper = CashContext('正常收費')
>>> csuper.get_result(20)
20.0
>>> csuper = CashContext('滿300返100')
>>> csuper.get_result(300)
200.0
>>> csuper.get_result(200)
200.0
>>> csuper = CashContext('打八折')
>>> csuper.get_result(300)
240.0
策略模式就是用來封裝算法的,但是在實踐中,我們可以用它來封裝幾乎任何類型的規則,只要在分析過程中根據需要在不同時間應用不同的業務規則,就可以考慮使用策略模式處理這種變化的可能性。
裝飾模式
裝飾模式:動態地給一個對象添加一些額外的職責,就增加功能來說,裝飾模式比生成子類更靈活。
裝飾模式的優點就是把類中的裝飾功能從類中搬移去除,這樣可以簡化原有的類。有效地把類的核心職責和裝飾功能區分開了,而且可以去除相關類中的重復的裝飾邏輯。
代理模式
為其他對象提供一種代理以控制對這個對象的訪問。
代碼示例:A追求C,委托B送禮物給C。需求的重點是A和C不能直接接觸::
>>> jiaojiao = SchoolGirl()
>>> jiaojiao.name = '李嬌嬌'
工廠方法模式
工廠方法模式,定義一個用于創建對象的接口,讓子類決定實例化哪個類。工廠方法使一個類的實例化延遲到其子類。
代碼示例:學習雷鋒好榜樣,繼承雷鋒精神的大學生和社區的構建::
>>> factory = UndergraduateFactory()
>>> student = factory.create_leifeng()
>>> student.buyrice()
買米
>>> student.sweep()
掃地
>>> student.wash()
洗衣
工廠方法把簡單工廠的內部邏輯判斷移到了客戶端代碼來進行。
原型模式
原型模式:用原型實例指定創建對象的種類,并且通過拷貝這些原型創建新的對象。其實就是從一個對象再創建另外一個可定制的對象,并且不需要知道任何創建的細節::
>>> a = Resume('大鳥')
>>> a.set_personal_info('男', '29')
>>> a.set_work_experience('1998-2000', 'xx公司')
>>> b = a.clone()
>>> b.set_work_experience('2000-2006', 'xx企業')
>>> c = a.clone()
>>> c.set_personal_info('男', '24')
>>> a.display()
大鳥 男 29
工作經歷: 1998-2000 xx公司
>>> b.display()
大鳥 男 29
工作經歷: 2000-2006 xx企業
>>> c.display()
大鳥 男 24
工作經歷: 1998-2000 xx公司
一般在初始化的信息不發生變化的情況下,克隆是最好的辦法,它既隱藏了對象的創建細節,又對性能是大大的提升。
模板方法模式
模板方法模式:定義一個操作中的算法的骨架,而將一些步驟延遲到子類中,模板方法是的子類可以不改變一個算法的結構即可重新定義該算法的特定步驟::
>>> a = ConcreteClassA()
>>> a.template_method()
具體類A方法1實現
具體類A方法2實現
>>> b = ConcreteClassB()
>>> b.template_method()
具體類B方法1實現
具體類B方法2實現
外觀模式
外觀模式:為子系統中的一組接口提供一個一致的界面,此模式定義了一個高層接口,這個接口使得這一子系統更加容易使用::
>>> facade = Facade()
>>> facade.method_a()
--- 方法組A() ---
子系統方法一
子系統方法二
子系統方法四
>>> facade.method_b()
--- 方法組B() ---
子系統方法二
子系統方法三
外觀模式完美的體現了依賴倒轉原則和迪米特法則的思想,是常用的模式之一。經典的三層架構,就需要考慮在層與層之間建立外觀Facade。
建造者模式
建造者模式:將一個復雜對象的構建與它的表示分離,是的同樣的構建過程,可以創建不同的表示::
>>> director = Director()
>>> b1 = ConcreateBuilder1()
>>> b2 = ConcreateBuilder2()
>>> director.construct(b1)
>>> p1 = b1.get_result()
>>> p1.show()
產品 創建 ----
部件A
部件B
>>> director.construct(b2)
>>> p2 = b2.get_result()
>>> p2.show()
產品 創建 ----
部件X
部件Y
建造者模式是當常見復雜對象的算法應該獨立于該對象的組成部分以及它們的裝配方式時使用的模式。
如果我們用了建造者模式,那么用戶就只需指定需要建造的類型就可以得到他們,而具體建造的過程和細節就不需要知道了。
觀察者模式
觀察者模式:定義了一種一對多的依賴關系,讓多個觀察者對象同時監聽某一個主體對象。這個主題對象在狀態發生變化時,會通知所有的觀察者對象,讓它們能夠自動更新自己::
>>> s = ConcreteSubject()
>>> s.attach(ConcreteObserver(s, 'X'))
>>> s.attach(ConcreteObserver(s, 'Y'))
>>> s.attach(ConcreteObserver(s, 'Z'))
>>> s.subject_state = 'ABC'
>>> s.notify()
觀察者X的新狀態是ABC
觀察者Y的新狀態是ABC
觀察者Z的新狀態是ABC
當一個對象的改變需要同時改變其他對象的時候,且它不知道具體有多少對象有待改變時,應該考慮使用觀察者模式。
觀察者模式所做的工作其實就是解除耦合,讓耦合的雙方都依賴抽象,而不是依賴于具體。從而使得各自的變化都不會影響另一邊的變化。是依賴倒轉原則的最佳體現。
但是抽象通知者還是依賴抽象觀察者。在代碼中抽象觀察者中有一個update()方法,具體觀察者中都要實現該方法,但是在實際中,可能有一些不一樣的操作,根本就不是同名的方法。這是不足的地方。
抽象工廠模式
抽象工廠方法:提供一個創建一系列相關或相互依賴對象的接口,而無需指定它們具體的類::
>>> user = User()
>>> dept = Department()
>>> factory = SqlserverFactory()
>>> iu = factory.create_user()
>>> iu.insert(user)
在sql server 中給User表增加一條記錄
>>> iu.get_user(1)
在sql server 中根據ID得到User表一條記錄
>>> factory = AccessFactory()
>>> id = factory.create_department()
>>> id.insert(dept)
在Access中給Department表增加一條記錄
>>> id.get_dept(1)
在Access中根據ID得到Department表一條記錄
>>> iu = factory.create_user()
>>> iu.insert(user)
在Access中給User表增加一條記錄
>>> iu.get_user(1)
在Access中根據Id得到User表一條記錄
抽象工廠模式的優點:
- 1、易于交換產品系列,由于具體工廠類,在一個應用中只需要在初始化的時候出現一次,這就使得改變一個應用的具體工廠變得非常容易,它只需要改變具體工廠即可使用不 同
的產品配置。 - 2、讓具體的創建實例過程和客戶端分離,客戶端是通過它們的抽象接口操作實例,產品的具體類名也被具體工廠的實現分離,不會出現在客戶代碼中。
缺點:
新增或修改時,可能需要大量的改動。
狀態模式
狀態模式:當一個對象的內在狀態改變時,允許改變其行為,這個對象看起來像是一個改變了其類::
>>> c = Context(ConcreteStateA())
>>> c.request()
當前狀態:stateB
>>> c.request()
當前狀態:stateA
>>> c.request()
當前狀態:stateB
>>> c.request()
當前狀態:stateA
>>> c.request()
當前狀態:stateB
狀態模式主要解決的是當控制一個對象狀態轉換的條件表達式過于復雜時的情況,把狀態的判斷轉移到表示不同狀態的一系列類中可以把復雜的判斷邏輯簡化。
狀態模式的好處是將與特定狀態相關的行為局部化,并且將不同狀態的行為分割開來。
適配器模式
適配器模式:將一個類的接口轉換成客戶希望的另外一個接口。使得原本由于接口不兼容而不能一起工作的那些類可以一起工作::
>>> target = Adapter()
>>> target.request()
特殊請求
備忘錄模式
備忘錄模式:在不破壞封裝性的前提下,捕獲一個對象的內部狀態,并在該對象之外保存這個狀態。這樣就可以將該對象恢復到原先保存的狀態::
>>> o = Originator()
>>> o.state = 'ON' # 初始狀態
>>> o.show()
state= ON
>>> caretaker = Caretaker()
>>> caretaker.memento = o.create_memento()
>>> o.state = 'OFF' # 改變狀態
>>> o.show()
state= OFF
>>> o.set_memento(caretaker.memento) # 恢復
>>> o.show()
state= ON
缺點:當需要備忘的對象狀態數據很大很多時,那么在資源消耗上,備忘錄對象會非常消耗資源。
組合模式
組合模式:將對象組合成樹形結構以表示部分整體的層次結構,組合模式使得用戶對單個對象和組合對象的使用具有一致性::
>>> root = Composite('root')
>>> root.add(Leaf('Leaf A'))
>>> root.add(Leaf('Leaf B'))
>>> comp = Composite('Composite X')
>>> comp.add(Leaf('Leaf A'))
>>> comp.add(Leaf('Leaf B'))
>>> root.add(comp)
>>> comp2 = Composite('Composite XY')
>>> comp2.add(Leaf('Leaf A'))
>>> comp2.add(Leaf('Leaf B'))
>>> comp.add(comp2)
>>> root.display(1)
- root
-- Leaf A
-- Leaf B
-- Composite X
--- Leaf A
--- Leaf B
--- Composite XY
---- Leaf A
---- Leaf B
組合模式定義了包含基本對象和組合對象的類層次結構,基本對象可以被組合成更復雜的組合對象,而這個組合對象又可以被組合這樣不斷地遞歸下去。
迭代器模式
迭代器模式:提供一種方法順序訪問一個聚合對象中各個元素,而不是暴露該對象的內部表示。代碼事例為乘務員迭代檢票的模擬演示::
>>> a = ConcreteAggregate()
>>> a[0] = '大鳥'
>>> a[1] = '小菜'
>>> a[2] = '行李'
>>> a[3] = '老外'
>>> a[4] = '員工'
>>> a[5] = '小偷'
>>> i = ConcreteIterator(a)
>>> item = i.first()
>>> while not i.is_done():
... print('{} 請買票!'.format(i.current_item()))
... i.next()
大鳥 請買票!
小菜 請買票!
行李 請買票!
老外 請買票!
員工 請買票!
小偷 請買票!
為遍歷不同的聚合結構提供如開始、下一個、是否結束、當前哪一項等統一的接口。現在高級編程語言如C#、JAVA、Python等本身已經把這個模式做在語言中。
單例模式
單例模式:保證一個類僅有一個實例,并提供一個訪問它的全局訪問點::
>>> singleton1 = Singleton('wen') # doctest: +ELLIPSIS
單例:...
>>> singleton2 = Singleton('wen1') # doctest: +ELLIPSIS
單例:...
>>> print(singleton1 is singleton2)
True
通常我們可以讓一個全局變量使得一個對象被訪問,但是它不能防止你實例化多個對象,一個最好的辦法就是:讓類自身負責它的唯一實例。這個類可以保證沒有其他的實例可以被創建,并且它可以提供一個訪問該實例的方法。
橋接模式
橋接模式:將抽象部分與它的實現部分分離,使他們都可以獨立的變化::
>>> ab = RefinedAbstraction()
>>> ab.implementor = ConcreteImplementorA()
>>> ab.operation()
具體實現A的方法執行。
>>> ab.implementor = ConcreteImplementorB()
>>> ab.operation()
具體實現B的方法執行。
命令模式
命令模式:將一個請求封裝為一個對象,從而使你可用不同的請求對客戶進行參數化;對請求排隊或記錄請求日志,以及支持可撤銷的操作。
>>> r = Receiver()
>>> c = ConcreteCommand(r)
>>> i = Invoker()
>>> i.command = c
>>> i.execute_command()
執行請求!
命令模式的優點:
- 1 能較容易的設計一個命令隊列
- 2 在需要的情況下,可以較容易的將命令記入日志
- 3 允許接收請求的一方決定是否要否決請求
- 4 可以容易的實現對請求的撤銷和重做
- 5 由于加新的具體命令類不影響其他的類,因此增加新的具體命令類很容易
- 6 命令模式把請求一個操作的對象與知道怎么執行一個操作的對象分割開。
職責鏈模式
職責鏈模式:使多個對象都有機會處理請求,從而避免請求的發送者和接收者之間的耦合關系。將這個對象連成一條鏈,并沿著這條鏈傳遞該請求,直到有一個對象處理它為止。
>>> h1 = ConcreteHandler1()
>>> h2 = ConcreteHandler2()
>>> h3 = ConcreteHandler3()
>>> h1.successor = h2 # 設置職責鏈上家和下家
>>> h2.successor = h3
>>> requests = [2, 5, 14, 22, 18, 3, 27, 20]
>>> for request in requests:
... h1.handle_request(request)
ConcreteHandler1 處理請求 2
ConcreteHandler1 處理請求 5
ConcreteHandler2 處理請求 14
ConcreteHandler3 處理請求 22
ConcreteHandler2 處理請求 18
ConcreteHandler1 處理請求 3
ConcreteHandler3 處理請求 27
ConcreteHandler3 處理請求 20
需要注意的地方,一個請求極有可能到了鏈的末端都得不到處理,或者因為沒有正確配置而得不到處理。
中介者模式
中介者模式:用一個中介對象來封裝一系列的對象交互。中介者使各對象不需要顯式的相互引用,從而使其耦合松散,而且可以獨立地改變它們之間的交互::
>>> m = ConcreteMediator()
>>> c1 = ConcreteHandler1(m)
>>> c2 = ConcreteHandler2(m)
>>> m.colleague1 = c1
>>> m.colleague2 = c2
>>> c1.send('吃飯了嗎?')
同事2得到消息: 吃飯了嗎?
>>> c2.send('沒有呢,你打算請客?')
同事1得到消息: 沒有呢,你打算請客?
享元模式
享元模式:運用共享技術有效地支持大量細粒度的對象::
>>> extrinsicstate = 22
>>> f = FlyweightFactory()
>>> fx = f.get_flyweight('X')
>>> fx.operation(extrinsicstate - 1)
具體Flyweight: 21
>>> fy = f.get_flyweight('Y')
>>> fy.operation(extrinsicstate - 2)
具體Flyweight: 20
>>> uf = UnsharedConcreteFlyweight()
>>> uf.operation(extrinsicstate - 3)
不共享的具體Flyweight: 19
享元模式可以避免大量非常相似類的開銷。在程序設計中,有時需要生成大量的細粒度的類示例來表示數據,如果能夠發現這些實例除了幾個參數外基本上都相同的,有時就能夠受大幅度的減少需要實例化的類數量。如果把這些參數移到類實例的外面,在方法調用的時候將它們傳遞進來,就可以通過共享大幅度的減少單個實例的數目。
應用場合:如果一個應用程序使用了大量的對象,而大量的這些對象造成了很大的存儲開銷時,應該考慮使用,還有就是對象的大多數狀態可以外部狀態,如果刪除對象的外部狀態,那么可以用相對較少的共享對象取代很多的組對象,此時可以考慮使用享元模式。
解釋器模式
解釋器模式:給定一個語言,定義它的文法的一種表示,并定義一個解釋器,這個解釋器使用該表示來解釋語言中的句子::
>>> context = Context()
>>> ll = []
>>> ll.append(TerminalExpression())
>>> ll.append(TerminalExpression())
>>> ll.append(NonterminalExpression())
>>> ll.append(TerminalExpression())
>>> for exp in ll:
... exp.interpret(context)
終端解釋器
終端解釋器
非終端解釋器
終端解釋器
比如,在字符串搜索匹配的字符或判斷一個字符串是否符合我們規定的格式,我們會用到的正則表達式技術,就是該模式的很好應用。
優點:容易的改變和擴展文法,因為該模式使用類來表示文法規則,你可使用繼承來改變或擴展該方法。也比較容易實現文法,因為定義抽象語法樹中各個節點的類的實現大體類似,這些類都易于直接編寫。
缺點:該模式為文法中的每一條規則至少定義了一個類,因此包含許多規則的文法就很難管理和維護。建議當文法非常復雜時,使用其他的技術如語法分析程序或編譯器生成器來處理。
訪問者模式
訪問者模式:表示一個作用于某對象結構中各元素的操作。它使你可以在不改變各元素的類的前提下定義于這些元素的新操作::
>>> o = ObjectStructure()
>>> o.attach(ConcreteElementA())
>>> o.attach(ConcreteElementB())
>>> v1 = ConcreteVisitor1()
>>> v2 = ConcreteVisitor2()
>>> o.accept(v1)
ConcreteElementA被ConcreteVisitor1訪問
ConcreteElementB被ConcreteVisitor1訪問
>>> o.accept(v2)
ConcreteElementA被ConcreteVisitor2訪問
ConcreteElementB被ConcreteVisitor2訪問
訪問者模式適用于數據結構相對穩定的系統,它把數據結構作用于結構上的操作之間的耦合解脫開,使得操作集合可以相對自由地演化。
如果系統有比較穩定的數據結構又有易于變化的算法的話,使用訪問者模式是比較合適的因為該模式使得算法操作的增加變得容易。
缺點:就是使增加新的數據結構變得困難了。該模式較為復雜。當你真正需要它的時候,才考慮使用它。