1、 設計原則javascript
使用設計模式的根本緣由是適應變化,提升代碼複用率,使軟件更具備可維護性和可擴展性。而且,在進行設計的時候,也須要遵循如下幾個原則:單一職責原則、開放封閉原則、里氏代替原則、依賴倒置原則、接口隔離原則、合成複用原則和迪米特法則。下面就分別介紹了每種設計原則。css
1.1 單一職責原則html
就一個類而言,應該只有一個引發它變化的緣由。若是一個類承擔的職責過多,就等於把這些職責耦合在一塊兒,一個職責的變化可能會影響到其餘的職責。另外,把多個職責耦合在一塊兒,也會影響複用性。前端
1.2 開閉原則(Open-Closed Principle)java
開閉原則即OCP(Open-Closed Principle縮寫)原則,該原則強調的是:一個軟件實體(指的類、函數、模塊等)應該對擴展開放,對修改關閉。即每次發生變化時,要經過添加新的代碼來加強現有類型的行爲,而不是修改原有的代碼。jquery
符合開閉原則的最好方式是提供一個固有的接口,而後讓全部可能發生變化的類實現該接口,讓固定的接口與相關對象進行交互。git
1.3 里氏代替原則(Liskov Substitution Principle)github
Liskov Substitution Principle,LSP(里氏代替原則)指的是子類必須替換掉它們的父類型。也就是說,在軟件開發過程當中,子類替換父類後,程序的行爲是同樣的。只有當子類替換掉父類後,此時軟件的功能不受影響時,父類才能真正地被複用,而子類也能夠在父類的基礎上添加新的行爲。面試
1.4 依賴倒置原則ajax
依賴倒置(Dependence Inversion Principle, DIP)原則指的是抽象不該該依賴於細節,細節應該依賴於抽象,也就是提出的 「面向接口編程,而不是面向實現編程」。這樣能夠下降客戶與具體實現的耦合。
1.5 接口隔離原則
接口隔離原則(Interface Segregation Principle, ISP)指的是使用多個專門的接口比使用單一的總接口要好。也就是說不要讓一個單一的接口承擔過多的職責,而應把每一個職責分離到多個專門的接口中,進行接口分離。過於臃腫的接口是對接口的一種污染。
1.6 合成複用原則
合成複用原則(Composite Reuse Principle, CRP)就是在一個新的對象裏面使用一些已有的對象,使之成爲新對象的一部分。新對象經過向這些對象的委派達到複用已用功能的目的。簡單地說,就是要儘可能使用合成/聚合,儘可能不要使用繼承。
要使用好合成複用原則,首先須要區分"Has—A"和「Is—A」的關係:
1)「Is—A」是指一個類是另外一個類的「一種」,是屬於的關係;
2)而「Has—A」則不一樣,它表示某一個角色具備某一項責任。
致使錯誤的使用繼承而不是聚合的常見的緣由是錯誤地把「Has—A」當成「Is—A」。例如:
實際上,僱員、經理、學生描述的是一種角色。好比,一我的是「經理」必然是「僱員」。在上面的設計中,一我的沒法同時擁有多個角色,是「僱員」就不能再是「學生」了,這顯然不合理,由於如今不少在職研究生,即便僱員也是學生。
上面的設計的錯誤源於把「角色」的等級結構與「人」的等級結構混淆起來了,誤把「Has—A」看成"Is—A"。具體的解決方法就是抽象出一個角色類:
1.7 迪米特法則
迪米特法則(Law of Demeter,LoD)又叫最少知識原則(Least Knowledge Principle,LKP),指的是一個對象應當對其餘對象有儘量少的瞭解。也就是說,一個模塊或對象應儘可能少的與其餘實體之間發生相互做用,使得系統功能模塊相對獨立,這樣當一個模塊修改時,影響的模塊就會越少,擴展起來更加容易。
關於迪米特法則其餘的一些表述有:只與你直接的朋友們通訊,不要跟「陌生人」說話。
外觀模式(Facade Pattern)和中介者模式(Mediator Pattern)就使用了迪米特法則。
2、建立型模式
建立型模式就是用來建立對象的模式,抽象了實例化的過程。全部的建立型模式都有兩個共同點。第一,它們都將系統使用哪些具體類的信息封裝起來;第二,它們隱藏了這些類的實例是如何被建立和組織的。建立型模式包括單例模式、工廠方法模式、抽象工廠模式、建造者模式和原型模式。
單例模式:解決的是實例化對象的個數的問題,好比抽象工廠中的工廠、對象池等。除了Singleton以外,其餘建立型模式解決的都是 new 所帶來的耦合關係。
抽象工廠:建立一系列相互依賴對象,並能在運行時改變系列。
工廠方法:建立單個對象,在Abstract Factory有使用到。
原型模式:經過拷貝原型來建立新的對象。
工廠方法,抽象工廠, 建造者都須要一個額外的工廠類來負責實例化「一個對象」,而Prototype則是經過原型(一個特殊的工廠類)來克隆「易變對象」。
下面詳細介紹下它們。
2.1 單例模式
單例模式指的是確保某一個類只有一個實例,並提供一個全局訪問點。解決的是實體對象個數的問題,而其餘的建造者模式都是解決new所帶來的耦合關係問題。其實現要點有:
類只有一個實例。問:如何保證呢?答:經過私有構造函數來保證類外部不能對類進行實例化。
提供一個全局的訪問點。問:如何實現呢?答:建立一個返回該類對象的靜態方法。
單例模式的結構圖以下所示:
2.2 工廠方法模式
工廠方法模式指的是定義一個建立對象的工廠接口,由其子類決定要實例化的類,將實際建立工做推遲到子類中。它強調的是」單個對象「的變化。其實現要點有:
定義一個工廠接口。問:如何實現呢?答:聲明一個工廠抽象類。
由其具體子類建立對象。問:如何去實現呢?答:建立派生於工廠抽象類,即由具體工廠去建立具體產品,既然要建立產品,天然須要產品抽象類和具體產品類了。
其具體的UML結構圖以下所示:
在工廠方法模式中,工廠類與具體產品類具備平行的等級結構,它們之間是一一對應關係。
2.3 抽象工廠模式
抽象工廠模式指的是提供一個建立一系列相關或相互依賴對象的接口,使得客戶端能夠在沒必要指定產品的具體類型的狀況下,建立多個產品族中的產品對象,強調的是」系列對象「的變化。其實現要點有:
提供一系列對象的接口。問:如何去實現呢?答:提供多個產品的抽象接口。
建立多個產品族中的多個產品對象。問:如何作到呢?答:每一個具體工廠建立一個產品族中的多個產品對象,多個具體工廠就能夠建立多個產品族中的多個對象了。
具體的UML結構圖以下所示:
2.4 建造者模式
建造者模式指的是將一個產品的內部表示與產品的構造過程分割開來,從而可使一個建造過程生成具體不一樣的內部表示的產品對象。強調的是產品的構造過程。其實現要點有:
將產品的內部表示與產品的構造過程分割開來。問:如何把它們分割開呢?答:不要把產品的構造過程放在產品類中,而是由建造者類來負責構造過程,產品的內部表示放在產品類中,這樣不就分割開了嘛。
具體的UML結構圖以下所示:
2.5 原型工廠模式
原型模式指的是經過給出一個原型對象來指明所要建立的對象類型,而後用複製的方法來建立出更多的同類型對象。其實現要點有:
給出一個原型對象。問:如何辦到呢?答:很簡單嘛,直接給出一個原型類就行了。
經過複製的方法來建立同類型對象。問:又是如何實現呢?答:.NET能夠直接調用MemberwiseClone方法來實現淺拷貝。
具體的UML結構圖以下所示:
3、結構型模式
結構型模式,顧名思義討論的是類和對象的結構 ,主要用來處理類或對象的組合。它包括兩種類型,一是類結構型模式,指的是採用繼承機制來組合接口或實現;二是對象結構型模式,指的是經過組合對象的方式來實現新的功能。它包括適配器模式、橋接模式、裝飾者模式、組合模式、外觀模式、享元模式和代理模式。
適配器模式:注重轉換接口,將不吻合的接口適配對接。
橋接模式:注重分離接口與其實現,支持多維度變化。
組合模式:注重統一接口,將「一對多」的關係轉化爲「一對一」的關係。
裝飾者模式:注重穩定接口,在此前提下爲對象擴展功能。
外觀模式:注重簡化接口,簡化組件系統與外部客戶程序的依賴關係。
享元模式:注重保留接口,在內部使用共享技術對對象存儲進行優化。
代理模式:注重假借接口,增長間接層來實現靈活控制。
3.1 適配器模式
適配器模式意在轉換接口,它可以使本來不能再一塊兒工做的兩個類一塊兒工做。因此常常用來在類庫的複用、代碼遷移等方面。例如DataAdapter類就應用了適配器模式。適配器模式包括類適配器模式和對象適配器模式,具體結構以下圖所示,左邊是類適配器模式,右邊是對象適配器模式。
3.2 橋接模式
橋接模式旨在將抽象化與實現化解耦,使得二者能夠獨立地變化。意思就是說,橋接模式把原來基類的實現化細節再進一步進行抽象,構造到一個實現化的結構中,而後再把原來的基類改形成一個抽象化的等級結構,這樣就能夠實現系統在多個維度的獨立變化,橋接模式的結構圖以下所示。
3.3 裝飾者模式
裝飾者模式又稱包裝(Wrapper)模式,它能夠動態地給一個對象添加一些額外的功能。裝飾者模式較繼承生成子類的方式更加靈活。雖然裝飾者模式可以動態地將職責附加到對象上,但它也會形成產生一些細小的對象,增長了系統的複雜度。具體的結構圖以下所示。
3.4 組合模式
組合模式又稱爲部分—總體模式。組合模式將對象組合成樹形結構,用來表示總體與部分的關係。組合模式使得客戶端將單個對象和組合對象同等對待。如在.NET中WinForm中的控件,TextBox、Label等簡單控件繼承與Control類,同時GroupBox這樣的組合控件也是繼承於Control類。組合模式的具體結構圖以下所示。
3.5 外觀模式
在系統中,客戶端常常須要與多個子系統進行交互,這樣致使客戶端會隨着子系統的變化而變化。此時,可使用外觀模式把客戶端與各個子系統解耦。外觀模式指的是爲子系統中的一組接口提供一個一致的門面,它提供了一個高層接口,這個接口使子系統更加容易使用。如電信的客戶專員,你可讓客戶專員來完成衝話費,修改套餐等業務,而不須要本身去與各個子系統進行交互。具體類結構圖以下所示:
3.6 享元模式
在系統中,若是咱們須要重複使用某個對象時。此時,若是重複地使用new操做符來建立這個對象的話,這對系統資源是一個極大的浪費,既然每次使用的都是同一個對象,爲何不能對其共享呢?這也是享元模式出現的緣由。
享元模式運用共享的技術有效地支持細粒度的對象,使其進行共享。在.NET類庫中,String類的實現就使用了享元模式,String類採用字符串駐留池的來使字符串進行共享。更多內容參考博文:http://www.cnblogs.com/artech/archive/2010/11/25/internedstring.html。享元模式的具體結構圖以下所示:
3.7 代理模式
在系統開發中,有些對象因爲網絡或其餘的障礙,以致於不能直接對其訪問。此時能夠經過一個代理對象來實現對目標對象的訪問。如.NET中的調用Web服務等操做。
代理模式指的是給某一個對象提供一個代理,並由代理對象控制對原對象的訪問。具體的結構圖以下所示:
注:外觀模式、適配器模式和代理模式區別?
解答:這三個模式的相同之處是,它們都是做爲客戶端與真實被使用的類或系統之間的一箇中間層,起到讓客戶端間接調用真實類的做用。不一樣之處在於,所應用的場合和意圖不一樣。
代理模式與外觀模式主要區別在於:
1)客戶對象沒法直接訪問對象,只能由代理對象提供訪問。
2)而外觀對象提供對各個子系統簡化訪問調用接口。
而適配器模式則不須要虛構一個代理者,目的是複用原有的接口。
外觀模式是定義新的接口,而適配器則是複用一個原有的接口。
另外,它們應用設計的不一樣階段。外觀模式用於設計的前期,由於系統須要前期就須要依賴於外觀。而適配器應用於設計完成以後,當發現設計完成的類沒法協同工做時,能夠採用適配器模式。然而,不少狀況下在設計初期就要考慮適配器模式的使用,如涉及到大量第三方應用接口的狀況;代理模式是設計完成後,想以服務的方式提供給其餘客戶端進行調用,此時其餘客戶端可使用代理模式來對模塊進行訪問。
總之,代理模式提供與真實類一致的接口,旨在用代理類來訪問真實的類。外觀模式旨在簡化接口。適配器模式旨在轉換接口。
4、行爲型模式
行爲型模式是對在不一樣對象之間劃分責任和算法的抽象化。行爲模式不只僅關於類和對象,還關於它們之間的相互做用。行爲型模式又分爲類的行爲模式和對象的行爲模式兩種。
類的行爲模式——使用繼承關係在幾個類之間分配行爲。
對象的行爲模式——使用對象聚合的方式來分配行爲。
行爲型模式包括11種模式:模板方法模式、命令模式、迭代器模式、觀察者模式、中介者模式、狀態模式、策略模式、責任鏈模式、訪問者模式、解釋器模式和備忘錄模式。
模板方法模式:封裝算法結構,定義算法骨架,支持算法子步驟變化。
命令模式:注重將請求封裝爲對象,支持請求的變化,經過將一組行爲抽象爲對象,實現行爲請求者和行爲實現者之間的解耦。
迭代器模式:注重封裝特定領域變化,支持集合的變化,屏蔽集合對象內部複雜結構,提供客戶程序對它的透明遍歷。
觀察者模式:注重封裝對象通知,支持通訊對象的變化,實現對象狀態改變,通知依賴它的對象並更新。
中介者模式:注重封裝對象間的交互,經過封裝一系列對象之間的複雜交互,使他們不須要顯式相互引用,實現解耦。
狀態模式:注重封裝與狀態相關的行爲,支持狀態的變化,經過封裝對象狀態,從而在其內部狀態改變時改變它的行爲。
策略模式:注重封裝算法,支持算法的變化,經過封裝一系列算法,從而能夠隨時獨立於客戶替換算法。
責任鏈模式:注重封裝對象責任,支持責任的變化,經過動態構建職責鏈,實現事務處理。
訪問者模式:注重封裝對象操做變化,支持在運行時爲類結構添加新的操做,在類層次結構中,在不改變各種的前提下定義做用於這些類實例的新的操做。
備忘錄模式:注重封裝對象狀態變化,支持狀態保存、恢復。
解釋器模式:注重封裝特定領域變化,支持領域問題的頻繁變化,將特定領域的問題表達爲某種語法規則下的句子,而後構建一個解釋器來解釋這樣的句子,從而達到解決問題的目的。
4.1 模板方法模式
在現實生活中,有論文模板,簡歷模板等。在現實生活中,模板的概念是給定必定的格式,而後其餘全部使用模板的人能夠根據本身的需求去實現它。一樣,模板方法也是這樣的。
模板方法模式是在一個抽象類中定義一個操做中的算法骨架,而將一些具體步驟實現延遲到子類中去實現。模板方法使得子類能夠不改變算法結構的前提下,從新定義算法的特定步驟,從而達到複用代碼的效果。具體的結構圖以下所示:
4.2 命令模式
命令模式屬於對象的行爲模式,命令模式把一個請求或操做封裝到一個對象中,經過對命令的抽象化來使得發出命令的責任和執行命令的責任分隔開。命令模式的實現能夠提供命令的撤銷和恢復功能。具體的結構圖以下所示:
4.3 迭代器模式
迭代器模式是針對集合對象而生的,對於集合對象而言,必然涉及到集合元素的添加刪除操做,也確定支持遍歷集合元素的操做。此時,若是把遍歷操做也放在集合對象的話,集合對象就承擔太多的責任了。因此進行責任分離,把集合的遍歷放在另外一個對象中,這個對象就是迭代器對象。
迭代器模式提供了一種方法來順序訪問一個集合對象中各個元素,而又無需暴露該對象的內部表示,這樣既能夠作到不暴露集合的內部結構,又可讓外部代碼透明地訪問集合內部元素。具體的結構圖以下所示:
4.4 觀察者模式
在現實生活中,到處可見觀察者模式,例如,微信中的訂閱號,訂閱博客和QQ微博中關注好友,這些都屬於觀察者模式的應用。
觀察者模式定義了一種一對多的依賴關係,讓多個觀察者對象同時監聽某一個主題對象,這個主題對象在狀態發生變化時,會通知全部觀察者對象,使它們可以自動更新本身的行爲。具體結構圖以下所示:
4.5 中介者模式
在現實生活中,有不少中介者模式的身影,例如QQ遊戲平臺,聊天室、QQ羣和短信平臺,這些都是中介者模式在現實生活中的應用。
中介者模式,定義了一箇中介對象來封裝一系列對象之間的交互關係。中介者使各個對象之間不須要顯式地相互引用,從而使耦合性下降,並且能夠獨立地改變它們之間的交互行爲。具體的結構圖以下所示:
4.6 狀態模式
每一個對象都有其對應的狀態,而每一個狀態又對應一些相應的行爲,若是某個對象有多個狀態時,那麼就會對應不少的行爲。那麼對這些狀態的判斷和根據狀態完成的行爲,就會致使多重條件語句,而且若是添加一種新的狀態時,須要更改以前現有的代碼。這樣的設計顯然違背了開閉原則,狀態模式正是用來解決這樣的問題的。
狀態模式——容許一個對象在其內部狀態改變時自動改變其行爲,對象看起來就像是改變了它的類。具體的結構圖以下所示:
4.7 策略模式
在現實生活中,中國的所得稅,分爲企業所得稅、外商投資企業或外商企業所得稅和我的所得稅。針對於這3種所得稅,每種所計算的方式不一樣。我的所得稅有我的所得稅的計算方式,而企業所得稅有其對應計算方式。若是不採用策略模式來實現這樣一個需求的話,咱們會定義一個所得稅類,該類有一個屬性來標識所得稅的類型,而且有一個計算稅收的CalculateTax()方法。在該方法體內須要對稅收類型進行判斷,經過if-else語句來針對不一樣的稅收類型來計算其所得稅。這樣的實現確實能夠解決這個場景,可是這樣的設計不利於擴展。若是系統後期須要增長一種所得稅時,此時不得不回去修改CalculateTax方法來多添加一個判斷語句,這樣明白違背了「開放——封閉」原則。此時,咱們能夠考慮使用策略模式來解決這個問題,既然稅收方法是這個場景中的變化部分,此時天然能夠想到對稅收方法進行抽象,這也是策略模式實現的精髓所在。
策略模式是對算法的包裝,是把使用算法的責任和算法自己分割開,委派給不一樣的對象負責。策略模式一般把一系列的算法包裝到一系列的策略類裏面。用一句話慨括策略模式就是——「將每一個算法封裝到不一樣的策略類中,使得它們能夠互換」。下面是策略模式的結構圖:
4.8 責任鏈模式
在現實生活中,有不少請求並非一我的說了就算的。例如面試時的工資,低於1萬的薪水可能技術經理就能夠決定了,可是1萬~1萬5的薪水可能技術經理就沒這個權利批准,可能須要請求技術總監的批准。
責任鏈模式——某個請求須要多個對象進行處理,從而避免請求的發送者和接收之間的耦合關係。將這些對象連成一條鏈子,並沿着這條鏈子傳遞該請求,直到有對象處理它爲止。具體結構圖以下所示:
4.9 訪問者模式
訪問者模式是封裝一些施加於某種數據結構之上的操做。一旦這些操做須要修改的話,接受這個操做的數據結構則能夠保存不變。訪問者模式適用於數據結構相對穩定的系統, 它把數據結構和做用於數據結構之上的操做之間的耦合度下降,使得操做集合能夠相對自由地改變。具體結構圖以下所示:
4.10 備忘錄模式
生活中的手機通信錄備忘錄,操做系統備份點,數據庫備份等都是備忘錄模式的應用。備忘錄模式是在不破壞封裝的前提下,捕獲一個對象的內部狀態,並在該對象以外保存這個狀態,這樣之後就能夠把該對象恢復到原先的狀態。具體的結構圖以下所示:
4.11 解釋器模式
解釋器模式是一個比較少用的模式,因此我本身也沒有對該模式進行深刻研究。在生活中,英漢詞典的做用就是實現英文和中文互譯,這就是解釋器模式的應用。
解釋器模式是給定一種語言,定義它文法的一種表示,並定義一種解釋器。這個解釋器使用該表示來解釋語言中的句子。具體的結構圖以下所示:
5、總結
23種設計模式,是前輩們總結出來解決問題的方式。它們追求的宗旨仍是保證系統的低耦合高內聚,指導它們的原則無非就是封裝變化,責任單一,面向接口編程等設計原則。
參考連接:http://www.cnblogs.com/zhili/p/DesignPatternSummery.html
C#設計模式(22)——訪問者模式(Vistor Pattern)
1、引言
在上一篇博文中分享了責任鏈模式,責任鏈模式主要應用在系統中的某些功能須要多個對象參與才能完成的場景。在這篇博文中,我將爲你們分享我對訪問者模式的理解。
2、訪問者模式介紹
2.1 訪問者模式的定義
訪問者模式是封裝一些施加於某種數據結構之上的操做。一旦這些操做須要修改的話,接受這個操做的數據結構則能夠保存不變。訪問者模式適用於數據結構相對穩定的系統, 它把數據結構和做用於數據結構之上的操做之間的耦合度下降,使得操做集合能夠相對自由地改變。
數據結構的每個節點均可以接受一個訪問者的調用,此節點向訪問者對象傳入節點對象,而訪問者對象則反過來執行節點對象的操做。這樣的過程叫作「雙重分派」。節點調用訪問者,將它本身傳入,訪問者則將某算法針對此節點執行。
2.2 訪問者模式的結構圖
從上面描述可知,訪問者模式是用來封裝某種數據結構中的方法。具體封裝過程是:每一個元素接受一個訪問者的調用,每一個元素的Accept方法接受訪問者對象做爲參數傳入,訪問者對象則反過來調用元素對象的操做。具體的訪問者模式結構圖以下所示。
這裏須要明確一點:訪問者模式中具體訪問者的數目和具體節點的數目沒有任何關係。從訪問者的結構圖能夠看出,訪問者模式涉及如下幾類角色。
- 抽象訪問者角色(Vistor):聲明一個活多個訪問操做,使得全部具體訪問者必須實現的接口。
- 具體訪問者角色(ConcreteVistor):實現抽象訪問者角色中全部聲明的接口。
- 抽象節點角色(Element):聲明一個接受操做,接受一個訪問者對象做爲參數。
- 具體節點角色(ConcreteElement):實現抽象元素所規定的接受操做。
- 結構對象角色(ObjectStructure):節點的容器,能夠包含多個不一樣類或接口的容器。
2.3 訪問者模式的實現
在講訴訪問者模式的實現時,我想先不用訪問者模式的方式來實現某個場景。具體場景是——如今我想遍歷每一個元素對象,而後調用每一個元素對象的Print方法來打印該元素對象的信息。若是此時不採用訪問者模式的話,實現這個場景再簡單不過了,具體實現代碼以下所示:
1 namespace DonotUsevistorPattern 2 { 3 // 抽象元素角色 4 public abstract class Element 5 { 6 public abstract void Print(); 7 } 8 9 // 具體元素A 10 public class ElementA : Element 11 { 12 public override void Print() 13 { 14 Console.WriteLine("我是元素A"); 15 } 16 } 17 18 // 具體元素B 19 public class ElementB : Element 20 { 21 public override void Print() 22 { 23 Console.WriteLine("我是元素B"); 24 } 25 } 26 27 // 對象結構 28 public class ObjectStructure 29 { 30 private ArrayList elements = new ArrayList(); 31 32 public ArrayList Elements 33 { 34 get { return elements; } 35 } 36 37 public ObjectStructure() 38 { 39 Random ran = new Random(); 40 for (int i = 0; i < 6; i++) 41 { 42 int ranNum = ran.Next(10); 43 if (ranNum > 5) 44 { 45 elements.Add(new ElementA()); 46 } 47 else 48 { 49 elements.Add(new ElementB()); 50 } 51 } 52 } 53 } 54 55 class Program 56 { 57 static void Main(string[] args) 58 { 59 ObjectStructure objectStructure = new ObjectStructure(); 60 // 遍歷對象結構中的對象集合,訪問每一個元素的Print方法打印元素信息 61 foreach (Element e in objectStructure.Elements) 62 { 63 e.Print(); 64 } 65 66 Console.Read(); 67 } 68 } 69 }
上面代碼很準確瞭解決了咱們剛纔提出的場景,可是需求在時刻變化的,若是此時,我除了想打印元素的信息外,還想打印出元素被訪問的時間,此時咱們就不得不去修改每一個元素的Print方法,再加入相對應的輸入訪問時間的輸出信息。這樣的設計顯然不符合「開-閉」原則,即某個方法操做的改變,會使得必須去更改每一個元素類。既然,這裏變化的點是操做的改變,而每一個元素的數據結構是不變的。因此此時就思考——能不能把操做於元素的操做和元素自己的數據結構分開呢?解開這二者的耦合度,這樣若是是操做發現變化時,就不須要去更改元素自己了,可是若是是元素數據結構發現變化,例如,添加了某個字段,這樣就不得不去修改元素類了。此時,咱們可使用訪問者模式來解決這個問題,即把做用於具體元素的操做由訪問者對象來調用。具體的實現代碼以下所示:
1 namespace VistorPattern 2 { 3 // 抽象元素角色 4 public abstract class Element 5 { 6 public abstract void Accept(IVistor vistor); 7 public abstract void Print(); 8 } 9 10 // 具體元素A 11 public class ElementA :Element 12 { 13 public override void Accept(IVistor vistor) 14 { 15 // 調用訪問者visit方法 16 vistor.Visit(this); 17 } 18 public override void Print() 19 { 20 Console.WriteLine("我是元素A"); 21 } 22 } 23 24 // 具體元素B 25 public class ElementB :Element 26 { 27 public override void Accept(IVistor vistor) 28 { 29 vistor.Visit(this); 30 } 31 public override void Print() 32 { 33 Console.WriteLine("我是元素B"); 34 } 35 } 36 37 // 抽象訪問者 38 public interface IVistor 39 { 40 void Visit(ElementA a); 41 void Visit(ElementB b); 42 } 43 44 // 具體訪問者 45 public class ConcreteVistor :IVistor 46 { 47 // visit方法而是再去調用元素的Accept方法 48 public void Visit(ElementA a) 49 { 50 a.Print(); 51 } 52 public void Visit(ElementB b) 53 { 54 b.Print(); 55 } 56 } 57 58 // 對象結構 59 public class ObjectStructure 60 { 61 private ArrayList elements = new ArrayList(); 62 63 public ArrayList Elements 64 { 65 get { return elements; } 66 } 67 68 public ObjectStructure() 69 { 70 Random ran = new Random(); 71 for (int i = 0; i < 6; i++) 72 { 73 int ranNum = ran.Next(10); 74 if (ranNum > 5) 75 { 76 elements.Add(new ElementA()); 77 } 78 else 79 { 80 elements.Add(new ElementB()); 81 } 82 } 83 } 84 } 85 86 class Program 87 { 88 static void Main(string[] args) 89 { 90 ObjectStructure objectStructure = new ObjectStructure(); 91 foreach (Element e in objectStructure.Elements) 92 { 93 // 每一個元素接受訪問者訪問 94 e.Accept(new ConcreteVistor()); 95 } 96 97 Console.Read(); 98 } 99 } 100 }
從上面代碼可知,使用訪問者模式實現上面場景後,元素Print方法的訪問封裝到了訪問者對象中了(我以爲能夠把Print方法封裝到具體訪問者對象中。),此時客戶端與元素的Print方法就隔離開了。此時,若是須要添加打印訪問時間的需求時,此時只須要再添加一個具體的訪問者類便可。此時就不須要去修改元素中的Print()方法了。
3、訪問者模式的應用場景
每一個設計模式都有其應當使用的狀況,那讓咱們看看訪問者模式具體應用場景。若是遇到如下場景,此時咱們能夠考慮使用訪問者模式。
- 若是系統有比較穩定的數據結構,而又有易於變化的算法時,此時能夠考慮使用訪問者模式。由於訪問者模式使得算法操做的添加比較容易。
- 若是一組類中,存在着類似的操做,爲了不出現大量重複的代碼,能夠考慮把重複的操做封裝到訪問者中。(固然也能夠考慮使用抽象類了)
- 若是一個對象存在着一些與自己對象不相干,或關係比較弱的操做時,爲了不操做污染這個對象,則能夠考慮把這些操做封裝到訪問者對象中。
4、訪問者模式的優缺點
訪問者模式具備如下優勢:
- 訪問者模式使得添加新的操做變得容易。若是一些操做依賴於一個複雜的結構對象的話,那麼通常而言,添加新的操做會變得很複雜。而使用訪問者模式,增長新的操做就意味着添加一個新的訪問者類。所以,使得添加新的操做變得容易。
- 訪問者模式使得有關的行爲操做集中到一個訪問者對象中,而不是分散到一個個的元素類中。這點相似與"中介者模式"。
- 訪問者模式能夠訪問屬於不一樣的等級結構的成員對象,而迭代只能訪問屬於同一個等級結構的成員對象。
訪問者模式也有以下的缺點:
- 增長新的元素類變得困難。每增長一個新的元素意味着要在抽象訪問者角色中增長一個新的抽象操做,並在每個具體訪問者類中添加相應的具體操做。
5、總結
訪問者模式是用來封裝一些施加於某種數據結構之上的操做。它使得能夠在不改變元素自己的前提下增長做用於這些元素的新操做,訪問者模式的目的是把操做從數據結構中分離出來。
C#設計模式總結
1、引言
通過這段時間對設計模式的學習,本身的感觸仍是不少的,由於我如今在寫代碼的時候,常常會想一想這裏能不能用什麼設計模式來進行重構。因此,學完設計模式以後,感受它會慢慢地影響到你寫代碼的思惟方式。這裏對設計模式作一個總結,一來能夠對全部設計模式進行一個梳理,二來能夠作一個索引來幫助你們收藏。
PS: 其實,很早以前我就看過全部的設計模式了,可是並無寫博客,可是不久就很快忘記了,也沒有起到什麼做用,此次以博客的形式總結出來,發現效果仍是很明顯的,由於經過這種總結的方式,我對它理解更深入了,也記住的更牢靠了,也影響了本身平時實現功能的思惟。因此,我鼓勵你們能夠經過作筆記的方式來把本身學到的東西進行梳理,這樣相信能夠理解更深,更好,我也會一直寫下來,以後打算寫WCF一系列文章。
其實WCF內容很早也看過了,而且博客園也有不少前輩寫的很好,可是,我以爲我仍是須要本身總結,由於只有這樣,知識才是本身的,別人寫的多好,你看了以後,其實仍是別人了,因此鼓勵你們幾點(對於這幾點,也是對本身的一個提醒):
- 要動手實戰別人博客中的例子;
- 實現以後進行總結,能夠寫博客也能夠本身記錄雲筆記等;
- 想一想能不能進行擴展,進行觸類旁通。
系列導航:
C#設計模式(5)——建造者模式(Builder Pattern)
C#設計模式(6)——原型模式(Prototype Pattern)
C#設計模式(7)——適配器模式(Adapter Pattern)
C#設計模式(8)——橋接模式(Bridge Pattern)
C#設計模式(9)——裝飾者模式(Decorator Pattern)
C#設計模式(10)——組合模式(Composite Pattern)
C#設計模式(11)——外觀模式(Facade Pattern)
C#設計模式(12)——享元模式(Flyweight Pattern)
C#設計模式(13)——代理模式(Proxy Pattern)
C#設計模式(14)——模板方法模式(Template Method)
C#設計模式(15)——命令模式(Command Pattern)
C#設計模式(16)——迭代器模式(Iterator Pattern)
C#設計模式(17)——觀察者模式(Observer Pattern)
C#設計模式(18)——中介者模式(Mediator Pattern)
C#設計模式(19)——狀態者模式(State Pattern)
C#設計模式(20)——策略者模式(Stragety Pattern)
C#設計模式(22)——訪問者模式(Vistor Pattern)
C#設計模式(23)——備忘錄模式(Memento Pattern)
2、 設計原則
使用設計模式的根本緣由是適應變化,提升代碼複用率,使軟件更具備可維護性和可擴展性。而且,在進行設計的時候,也須要遵循如下幾個原則:單一職責原則、開放封閉原則、里氏代替原則、依賴倒置原則、接口隔離原則、合成複用原則和迪米特法則。下面就分別介紹了每種設計原則。
2.1 單一職責原則
就一個類而言,應該只有一個引發它變化的緣由。若是一個類承擔的職責過多,就等於把這些職責耦合在一塊兒,一個職責的變化可能會影響到其餘的職責,另外,把多個職責耦合在一塊兒,也會影響複用性。
2.2 開閉原則(Open-Closed Principle)
開閉原則即OCP(Open-Closed Principle縮寫)原則,該原則強調的是:一個軟件實體(指的類、函數、模塊等)應該對擴展開放,對修改關閉。即每次發生變化時,要經過添加新的代碼來加強現有類型的行爲,而不是修改原有的代碼。
符合開閉原則的最好方式是提供一個固有的接口,而後讓全部可能發生變化的類實現該接口,讓固定的接口與相關對象進行交互。
2.3 里氏代替原則(Liskov Substitution Principle)
Liskov Substitution Principle,LSP(里氏代替原則)指的是子類必須替換掉它們的父類型。也就是說,在軟件開發過程當中,子類替換父類後,程序的行爲是同樣的。只有當子類替換掉父類後,此時軟件的功能不受影響時,父類才能真正地被複用,而子類也能夠在父類的基礎上添加新的行爲。爲了就來看看違反了LSP原則的例子,具體代碼以下所示:
public class Rectangle { public virtual long Width { get; set; } public virtual long Height { get; set; } } // 正方形 public class Square : Rectangle { public override long Height { get { return base.Height; } set { base.Height = value; base.Width = value; } } public override long Width { get { return base.Width; } set { base.Width = value; base.Height = value; } } } class Test { public void Resize(Rectangle r) { while (r.Height >= r.Width) { r.Width += 1; } } var r = new Square() { Width = 10, Height = 10 }; new Test().Resize(r); }
上面的設計,正如上面註釋的同樣,在執行SmartTest的resize方法時,若是傳入的是長方形對象,當高度大於寬度時,會自動增長寬度直到超出高度。可是若是傳入的是正方形對象,則會陷入死循環。此時根本緣由是,矩形不能做爲正方形的父類,既然出現了問題,能夠進行重構,使它們倆都繼承於四邊形類。重構後的代碼以下所示:
// 四邊形 public abstract class Quadrangle { public virtual long Width { get; set; } public virtual long Height { get; set; } } // 矩形 public class Rectangle : Quadrangle { public override long Height { get; set; } public override long Width { get; set; } } // 正方形 public class Square : Quadrangle { public long _side; public Square(long side) { _side = side; } } class Test { public void Resize(Quadrangle r) { while (r.Height >= r.Width) { r.Width += 1; } } static void Main(string[] args) { var s = new Square(10); new Test().Resize(s); } }
2.4 依賴倒置原則
依賴倒置(Dependence Inversion Principle, DIP)原則指的是抽象不該該依賴於細節,細節應該依賴於抽象,也就是提出的 「面向接口編程,而不是面向實現編程」。這樣能夠下降客戶與具體實現的耦合。
2.5 接口隔離原則
接口隔離原則(Interface Segregation Principle, ISP)指的是使用多個專門的接口比使用單一的總接口要好。也就是說不要讓一個單一的接口承擔過多的職責,而應把每一個職責分離到多個專門的接口中,進行接口分離。過於臃腫的接口是對接口的一種污染。
2.6 合成複用原則
合成複用原則(Composite Reuse Principle, CRP)就是在一個新的對象裏面使用一些已有的對象,使之成爲新對象的一部分。新對象經過向這些對象的委派達到複用已用功能的目的。簡單地說,就是要儘可能使用合成/聚合,儘可能不要使用繼承。
要使用好合成複用原則,首先須要區分"Has—A"和「Is—A」的關係。
「Is—A」是指一個類是另外一個類的「一種」,是屬於的關係,而「Has—A」則不一樣,它表示某一個角色具備某一項責任。致使錯誤的使用繼承而不是聚合的常見的緣由是錯誤地把「Has—A」當成「Is—A」.例如:
實際上,僱員、經歷、學生描述的是一種角色,好比一我的是「經理」必然是「僱員」。在上面的設計中,一我的沒法同時擁有多個角色,是「僱員」就不能再是「學生」了,這顯然不合理,由於如今不少在職研究生,即便僱員也是學生。
上面的設計的錯誤源於把「角色」的等級結構與「人」的等級結構混淆起來了,誤把「Has—A」看成"Is—A"。具體的解決方法就是抽象出一個角色類:
2.7 迪米特法則
迪米特法則(Law of Demeter,LoD)又叫最少知識原則(Least Knowledge Principle,LKP),指的是一個對象應當對其餘對象有儘量少的瞭解。也就是說,一個模塊或對象應儘可能少的與其餘實體之間發生相互做用,使得系統功能模塊相對獨立,這樣當一個模塊修改時,影響的模塊就會越少,擴展起來更加容易。
關於迪米特法則其餘的一些表述有:只與你直接的朋友們通訊;不要跟「陌生人」說話。
外觀模式(Facade Pattern)和中介者模式(Mediator Pattern)就使用了迪米特法則。
3、建立型模式
建立型模式就是用來建立對象的模式,抽象了實例化的過程。全部的建立型模式都有兩個共同點。第一,它們都將系統使用哪些具體類的信息封裝起來;第二,它們隱藏了這些類的實例是如何被建立和組織的。建立型模式包括單例模式、工廠方法模式、抽象工廠模式、建造者模式和原型模式。
- 單例模式:解決的是實例化對象的個數的問題,好比抽象工廠中的工廠、對象池等,除了Singleton以外,其餘建立型模式解決的都是 new 所帶來的耦合關係。
- 抽象工廠:建立一系列相互依賴對象,並能在運行時改變系列。
- 工廠方法:建立單個對象,在Abstract Factory有使用到。
- 原型模式:經過拷貝原型來建立新的對象。
工廠方法,抽象工廠, 建造者都須要一個額外的工廠類來負責實例化「一個對象」,而Prototype則是經過原型(一個特殊的工廠類)來克隆「易變對象」。
下面詳細介紹下它們。
3.1 單例模式
單例模式指的是確保某一個類只有一個實例,並提供一個全局訪問點。解決的是實體對象個數的問題,而其餘的建造者模式都是解決new所帶來的耦合關係問題。其實現要點有:
- 類只有一個實例。問:如何保證呢?答:經過私有構造函數來保證類外部不能對類進行實例化
- 提供一個全局的訪問點。問:如何實現呢?答:建立一個返回該類對象的靜態方法
單例模式的結構圖以下所示:
3.2 工廠方法模式
工廠方法模式指的是定義一個建立對象的工廠接口,由其子類決定要實例化的類,將實際建立工做推遲到子類中。它強調的是」單個對象「的變化。其實現要點有:
- 定義一個工廠接口。問:如何實現呢?答:聲明一個工廠抽象類
- 由其具體子類建立對象。問:如何去實現呢?答:建立派生於工廠抽象類,即由具體工廠去建立具體產品,既然要建立產品,天然須要產品抽象類和具體產品類了。
其具體的UML結構圖以下所示:
在工廠方法模式中,工廠類與具體產品類具備平行的等級結構,它們之間是一一對應關係。
3.3 抽象工廠模式
抽象工廠模式指的是提供一個建立一系列相關或相互依賴對象的接口,使得客戶端能夠在沒必要指定產品的具體類型的狀況下,建立多個產品族中的產品對象,強調的是」系列對象「的變化。其實現要點有:
- 提供一系列對象的接口。問:如何去實現呢?答:提供多個產品的抽象接口
- 建立多個產品族中的多個產品對象。問:如何作到呢?答:每一個具體工廠建立一個產品族中的多個產品對象,多個具體工廠就能夠建立多個產品族中的多個對象了。
具體的UML結構圖以下所示:
3.4 建造者模式
建造者模式指的是將一個產品的內部表示與產品的構造過程分割開來,從而可使一個建造過程生成具體不一樣的內部表示的產品對象。強調的是產品的構造過程。其實現要點有:
- 將產品的內部表示與產品的構造過程分割開來。問:如何把它們分割開呢?答:不要把產品的構造過程放在產品類中,而是由建造者類來負責構造過程,產品的內部表示放在產品類中,這樣不就分割開了嘛。
具體的UML結構圖以下所示:
3.5 原型工廠模式
原型模式指的是經過給出一個原型對象來指明所要建立的對象類型,而後用複製的方法來建立出更多的同類型對象。其實現要點有:
- 給出一個原型對象。問:如何辦到呢?答:很簡單嘛,直接給出一個原型類就行了。
- 經過複製的方法來建立同類型對象。問:又是如何實現呢?答:.NET能夠直接調用MemberwiseClone方法來實現淺拷貝
具體的UML結構圖以下所示:
4、結構型模式
結構型模式,顧名思義討論的是類和對象的結構 ,主要用來處理類或對象的組合。它包括兩種類型,一是類結構型模式,指的是採用繼承機制來組合接口或實現;二是對象結構型模式,指的是經過組合對象的方式來實現新的功能。它包括適配器模式、橋接模式、裝飾者模式、組合模式、外觀模式、享元模式和代理模式。
- 適配器模式注重轉換接口,將不吻合的接口適配對接
- 橋接模式注重分離接口與其實現,支持多維度變化
- 組合模式注重統一接口,將「一對多」的關係轉化爲「一對一」的關係
- 裝飾者模式注重穩定接口,在此前提下爲對象擴展功能
- 外觀模式注重簡化接口,簡化組件系統與外部客戶程序的依賴關係
- 享元模式注重保留接口,在內部使用共享技術對對象存儲進行優化
- 代理模式注重假借接口,增長間接層來實現靈活控制
4.1 適配器模式
適配器模式意在轉換接口,它可以使本來不能再一塊兒工做的兩個類一塊兒工做,因此常常用來在類庫的複用、代碼遷移等方面。例如DataAdapter類就應用了適配器模式。適配器模式包括類適配器模式和對象適配器模式,具體結構以下圖所示,左邊是類適配器模式,右邊是對象適配器模式。
4.2 橋接模式
橋接模式旨在將抽象化與實現化解耦,使得二者能夠獨立地變化。意思就是說,橋接模式把原來基類的實現化細節再進一步進行抽象,構造到一個實現化的結構中,而後再把原來的基類改形成一個抽象化的等級結構,這樣就能夠實現系統在多個維度的獨立變化,橋接模式的結構圖以下所示。
4.3 裝飾者模式
裝飾者模式又稱包裝(Wrapper)模式,它能夠動態地給一個對象添加一些額外的功能,裝飾者模式較繼承生成子類的方式更加靈活。雖然裝飾者模式可以動態地將職責附加到對象上,但它也會形成產生一些細小的對象,增長了系統的複雜度。具體的結構圖以下所示。
4.4 組合模式
組合模式又稱爲部分—總體模式。組合模式將對象組合成樹形結構,用來表示總體與部分的關係。組合模式使得客戶端將單個對象和組合對象同等對待。如在.NET中WinForm中的控件,TextBox、Label等簡單控件繼承與Control類,同時GroupBox這樣的組合控件也是繼承於Control類。組合模式的具體結構圖以下所示。
4.5 外觀模式
在系統中,客戶端常常須要與多個子系統進行交互,這樣致使客戶端會隨着子系統的變化而變化,此時可使用外觀模式把客戶端與各個子系統解耦。外觀模式指的是爲子系統中的一組接口提供一個一致的門面,它提供了一個高層接口,這個接口使子系統更加容易使用。如電信的客戶專員,你可讓客戶專員來完成衝話費,修改套餐等業務,而不須要本身去與各個子系統進行交互。具體類結構圖以下所示:
4.6 享元模式
在系統中,如何咱們須要重複使用某個對象時,此時若是重複地使用new操做符來建立這個對象的話,這對系統資源是一個極大的浪費,既然每次使用的都是同一個對象,爲何不能對其共享呢?這也是享元模式出現的緣由。
享元模式運用共享的技術有效地支持細粒度的對象,使其進行共享。在.NET類庫中,String類的實現就使用了享元模式,String類採用字符串駐留池的來使字符串進行共享。更多內容參考博文:http://www.cnblogs.com/artech/archive/2010/11/25/internedstring.html。享元模式的具體結構圖以下所示。
4.7 代理模式
在系統開發中,有些對象因爲網絡或其餘的障礙,以致於不能直接對其訪問,此時能夠經過一個代理對象來實現對目標對象的訪問。如.NET中的調用Web服務等操做。
代理模式指的是給某一個對象提供一個代理,並由代理對象控制對原對象的訪問。具體的結構圖以下所示。
注:外觀模式、適配器模式和代理模式區別?
解答:這三個模式的相同之處是,它們都是做爲客戶端與真實被使用的類或系統之間的一箇中間層,起到讓客戶端間接調用真實類的做用,不一樣之處在於,所應用的場合和意圖不一樣。
代理模式與外觀模式主要區別在於,代理對象沒法直接訪問對象,只能由代理對象提供訪問,而外觀對象提供對各個子系統簡化訪問調用接口,而適配器模式則不須要虛構一個代理者,目的是複用原有的接口。外觀模式是定義新的接口,而適配器則是複用一個原有的接口。
另外,它們應用設計的不一樣階段,外觀模式用於設計的前期,由於系統須要前期就須要依賴於外觀,而適配器應用於設計完成以後,當發現設計完成的類沒法協同工做時,能夠採用適配器模式。然而不少狀況下在設計初期就要考慮適配器模式的使用,如涉及到大量第三方應用接口的狀況;代理模式是模式完成後,想以服務的方式提供給其餘客戶端進行調用,此時其餘客戶端可使用代理模式來對模塊進行訪問。
總之,代理模式提供與真實類一致的接口,旨在用來代理類來訪問真實的類,外觀模式旨在簡化接口,適配器模式旨在轉換接口。
5、行爲型模式
行爲型模式是對在不一樣對象之間劃分責任和算法的抽象化。行爲模式不只僅關於類和對象,還關於它們之間的相互做用。行爲型模式又分爲類的行爲模式和對象的行爲模式兩種。
- 類的行爲模式——使用繼承關係在幾個類之間分配行爲。
- 對象的行爲模式——使用對象聚合的方式來分配行爲。
行爲型模式包括11種模式:模板方法模式、命令模式、迭代器模式、觀察者模式、中介者模式、狀態模式、策略模式、責任鏈模式、訪問者模式、解釋器模式和備忘錄模式。
- 模板方法模式:封裝算法結構,定義算法骨架,支持算法子步驟變化。
- 命令模式:注重將請求封裝爲對象,支持請求的變化,經過將一組行爲抽象爲對象,實現行爲請求者和行爲實現者之間的解耦。
- 迭代器模式:注重封裝特定領域變化,支持集合的變化,屏蔽集合對象內部複雜結構,提供客戶程序對它的透明遍歷。
- 觀察者模式:注重封裝對象通知,支持通訊對象的變化,實現對象狀態改變,通知依賴它的對象並更新。
- 中介者模式:注重封裝對象間的交互,經過封裝一系列對象之間的複雜交互,使他們不須要顯式相互引用,實現解耦。
- 狀態模式:注重封裝與狀態相關的行爲,支持狀態的變化,經過封裝對象狀態,從而在其內部狀態改變時改變它的行爲。
- 策略模式:注重封裝算法,支持算法的變化,經過封裝一系列算法,從而能夠隨時獨立於客戶替換算法。
- 責任鏈模式:注重封裝對象責任,支持責任的變化,經過動態構建職責鏈,實現事務處理。
- 訪問者模式:注重封裝對象操做變化,支持在運行時爲類結構添加新的操做,在類層次結構中,在不改變各種的前提下定義做用於這些類實例的新的操做。
- 備忘錄模式:注重封裝對象狀態變化,支持狀態保存、恢復。
- 解釋器模式:注重封裝特定領域變化,支持領域問題的頻繁變化,將特定領域的問題表達爲某種語法規則下的句子,而後構建一個解釋器來解釋這樣的句子,從而達到解決問題的目的。
5.1 模板方法模式
在現實生活中,有論文模板,簡歷模板等。在現實生活中,模板的概念是給定必定的格式,而後其餘全部使用模板的人能夠根據本身的需求去實現它。一樣,模板方法也是這樣的。
模板方法模式是在一個抽象類中定義一個操做中的算法骨架,而將一些具體步驟實現延遲到子類中去實現。模板方法使得子類能夠不改變算法結構的前提下,從新定義算法的特定步驟,從而達到複用代碼的效果。具體的結構圖以下所示。
以生活中作菜爲例子實現的模板方法結構圖
5.2 命令模式
命令模式屬於對象的行爲模式,命令模式把一個請求或操做封裝到一個對象中,經過對命令的抽象化來使得發出命令的責任和執行命令的責任分隔開。命令模式的實現能夠提供命令的撤銷和恢復功能。具體的結構圖以下所示。
5.3 迭代器模式
迭代器模式是針對集合對象而生的,對於集合對象而言,必然涉及到集合元素的添加刪除操做,也確定支持遍歷集合元素的操做,此時若是把遍歷操做也放在集合對象的話,集合對象就承擔太多的責任了,此時能夠進行責任分離,把集合的遍歷放在另外一個對象中,這個對象就是迭代器對象。
迭代器模式提供了一種方法來順序訪問一個集合對象中各個元素,而又無需暴露該對象的內部表示,這樣既能夠作到不暴露集合的內部結構,又可讓外部代碼透明地訪問集合內部元素。具體的結構圖以下所示。
5.4 觀察者模式
在現實生活中,到處可見觀察者模式,例如,微信中的訂閱號,訂閱博客和QQ微博中關注好友,這些都屬於觀察者模式的應用。
觀察者模式定義了一種一對多的依賴關係,讓多個觀察者對象同時監聽某一個主題對象,這個主題對象在狀態發生變化時,會通知全部觀察者對象,使它們可以自動更新本身的行爲。具體結構圖以下所示:
5.5 中介者模式
在現實生活中,有不少中介者模式的身影,例如QQ遊戲平臺,聊天室、QQ羣和短信平臺,這些都是中介者模式在現實生活中的應用。
中介者模式,定義了一箇中介對象來封裝一系列對象之間的交互關係。中介者使各個對象之間不須要顯式地相互引用,從而使耦合性下降,並且能夠獨立地改變它們之間的交互行爲。具體的結構圖以下所示:
5.6 狀態模式
每一個對象都有其對應的狀態,而每一個狀態又對應一些相應的行爲,若是某個對象有多個狀態時,那麼就會對應不少的行爲。那麼對這些狀態的判斷和根據狀態完成的行爲,就會致使多重條件語句,而且若是添加一種新的狀態時,須要更改以前現有的代碼。這樣的設計顯然違背了開閉原則,狀態模式正是用來解決這樣的問題的。
狀態模式——容許一個對象在其內部狀態改變時自動改變其行爲,對象看起來就像是改變了它的類。具體的結構圖以下所示:
5.7 策略模式
在現實生活中,中國的所得稅,分爲企業所得稅、外商投資企業或外商企業所得稅和我的所得稅,針對於這3種所得稅,每種所計算的方式不一樣,我的所得稅有我的所得稅的計算方式,而企業所得稅有其對應計算方式。若是不採用策略模式來實現這樣一個需求的話,咱們會定義一個所得稅類,該類有一個屬性來標識所得稅的類型,而且有一個計算稅收的CalculateTax()方法,在該方法體內須要對稅收類型進行判斷,經過if-else語句來針對不一樣的稅收類型來計算其所得稅。這樣的實現確實能夠解決這個場景,可是這樣的設計不利於擴展,若是系統後期須要增長一種所得稅時,此時不得不回去修改CalculateTax方法來多添加一個判斷語句,這樣明白違背了「開放——封閉」原則。此時,咱們能夠考慮使用策略模式來解決這個問題,既然稅收方法是這個場景中的變化部分,此時天然能夠想到對稅收方法進行抽象,這也是策略模式實現的精髓所在。
策略模式是對算法的包裝,是把使用算法的責任和算法自己分割開,委派給不一樣的對象負責。策略模式一般把一系列的算法包裝到一系列的策略類裏面。用一句話慨括策略模式就是——「將每一個算法封裝到不一樣的策略類中,使得它們能夠互換」。下面是策略模式的結構圖:
5.8 責任鏈模式
在現實生活中,有不少請求並非一我的說了就算的,例如面試時的工資,低於1萬的薪水可能技術經理就能夠決定了,可是1萬~1萬5的薪水可能技術經理就沒這個權利批准,可能須要請求技術總監的批准。
責任鏈模式——某個請求須要多個對象進行處理,從而避免請求的發送者和接收之間的耦合關係。將這些對象連成一條鏈子,並沿着這條鏈子傳遞該請求,直到有對象處理它爲止。具體結構圖以下所示:
5.9 訪問者模式
訪問者模式是封裝一些施加於某種數據結構之上的操做。一旦這些操做須要修改的話,接受這個操做的數據結構則能夠保存不變。訪問者模式適用於數據結構相對穩定的系統, 它把數據結構和做用於數據結構之上的操做之間的耦合度下降,使得操做集合能夠相對自由地改變。具體結構圖以下所示:
5.10 備忘錄模式
生活中的手機通信錄備忘錄,操做系統備份點,數據庫備份等都是備忘錄模式的應用。備忘錄模式是在不破壞封裝的前提下,捕獲一個對象的內部狀態,並在該對象以外保存這個狀態,這樣之後就能夠把該對象恢復到原先的狀態。具體的結構圖以下所示:
5.11 解釋器模式
解釋器模式是一個比較少用的模式,因此我本身也沒有對該模式進行深刻研究,在生活中,英漢詞典的做用就是實現英文和中文互譯,這就是解釋器模式的應用。
解釋器模式是給定一種語言,定義它文法的一種表示,並定義一種解釋器,這個解釋器使用該表示來解釋器語言中的句子。具體的結構圖以下所示:
6、總結
23種設計模式,其實前輩們總結出來解決問題的方式,它們追求的宗旨仍是保證系統的低耦合高內聚,指導它們的原則無非就是封裝變化,責任單一,面向接口編程等設計原則。以後,我會繼續分享本身WCF的學習過程,儘管博客園中有不少WCF系列,以前以爲不必寫,以爲會用就好了,可是不寫,總感受知識不是本身的,感受沒有深刻,因此仍是想寫這樣一個系列,但願各位博友後面多多支持。
PS: 不少論壇都看到初學者問,WCF如今還有沒有必要深刻學之類的問題,由於他們以爲這些技術可能會過期,說不定到時候微軟又推出了一個新的SOA的實現方案了,那豈不是白花時間深刻學了,因此就以爲不必深刻去學,知道用就能夠了。對於這個問題,我以前也有這樣一樣的感受,可是如今我以爲,儘管WCF技術可能會被替換,但深刻了解一門技術,重點不是知道一些更高深API的調用啊,而是瞭解它的實現機制和思惟方式,即便後面這個技術被替代了,其背後機制也確定是類似的。因此深刻了解了一個技術,你就會感受新的技術熟悉,對其感受放鬆。而且,你深刻了解完一門技術以後,你面試時也敢說你很好掌握了這門技術,而不至於說平時使用的不少,一旦深刻問時殊不知道背後實現原理。這也是我要寫WCF系列的緣由。但願這點意見對一些初學者有幫助。
.NET Core launch.json 簡介
1.環境 Windows,.NET Core 2.0,VS Code
dotnet> dotnet new console -o myApp
2.launch.json配置文件
{ // Use IntelliSense to find out which attributes exist for C# debugging // Use hover for the description of the existing attributes // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md "version": "0.2.0", "configurations": [ { "name": ".NET Core Launch (console)", "type": "coreclr", "request": "launch", "preLaunchTask": "build", // If you have changed target frameworks, make sure to update the program path. "program": "${workspaceFolder}/myApp/bin/Debug/netcoreapp2.0/myApp.dll", "args": [], "cwd": "${workspaceFolder}/myApp", // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window "console": "internalConsole", "stopAtEntry": false, "internalConsoleOptions": "openOnSessionStart" }, { "name": ".NET Core Attach", "type": "coreclr", "request": "attach", "processId": "${command:pickProcess}" } ,] }
在launch.json中
利用Bootstrap Paginator插件和knockout.js完成分頁功能
在最近一個項目中,須要結合一堆條件查詢並對查詢的結果數據完成一個簡單分頁功能,但是作着作着,本身的思路愈來愈模糊,作到心態崩潰!!! 哈哈
特此花點時間從新總結,並從最簡單的分頁,而後向多條件查詢分頁慢慢過渡,或許有人以爲這個很簡單(能夠繞道啦,哈哈),倒是對基礎知識的一次學習過程。
Demo地址:https://gitee.com/530521314/Pagination.git
本文地址:http://www.javashuo.com/article/p-ogyapptc-ca.html
1、環境介紹
分頁功能不少已有的很完美的插件或是第三方應用包都可以完美實現,我在此利用了一些前端插件來完成分頁功能。
前端的Bootstrap Paginator插件完成前端分頁數字之類的切換展現;
利用knockout.js插件完成分頁數據的綁定;
在後端,利用asp.net core mvc 完成分頁信息的接收和處理工做。
2、簡單分頁
完成對已有數據的分頁功能,不帶條件查詢。
首先,一上來即是完成數據綁定工做:
1 bookList: ko.mapping.fromJS(@Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(Model))),//展現數據 2 pageEntity: ko.mapping.fromJS(@Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(new PageRequestViewModel()))),//分頁信息
頁面剛展現的時候得有分頁數據吧,能夠在當頁面展現的時候,數據也帶過來了,也能夠頁面展現後,再經過ajax去後臺調用,我選擇後者。
在頁面啓動時,調用該函數完成初始化頁面數據。
1 getBookData: function () { 2 var pageEntity = ko.mapping.toJS(viewModel.pageEntity); 3 $.ajax({ 4 url: '@Url.Action("SampleGetData")', 5 type: 'POST', 6 dataType: 'json', 7 data: pageEntity, 8 success: function (result) { 9 ko.mapping.fromJS(result.data, viewModel.bookList); 10 options.totalPages = result.totalCount > 0 ? result.totalCount % options.numberOfPages == 0 ? 11 result.totalCount / options.numberOfPages : (result.totalCount / options.numberOfPages) + 1 : 1; 12 $('#pagination').bootstrapPaginator(options); 13 } 14 }); 15 }
分頁信息(當前頁面,頁面展現數據條數)帶過去,後臺根據分頁信息完成數據查詢,並得到總的記錄條數用於前端頁面計算總頁數。
1 [HttpPost] 2 public IActionResult SampleGetData(PageRequestViewModel pageEntity) 3 { 4 var bookList = PageDataSeed.GetPageDataList() 5 .Skip((pageEntity.PageIndex - 1) * pageEntity.PageSize) 6 .Take(pageEntity.PageSize); 7 8 var pageResultViewModel = new PageResultViewModel<Book>() 9 { 10 PageIndex = pageEntity.PageIndex, 11 PageSize = pageEntity.PageSize, 12 TotalCount = PageDataSeed.GetPageDataList().Count(), 13 Data = bookList 14 }; 15 16 return Json(pageResultViewModel); 17 }
根據分頁信息查詢也就搞定了,當點擊底部的頁面碼的時候得改變當前分頁信息,而後要切換當前分頁信息所對應的數據出來,在Bootstrap paginator插件中,點擊頁面碼有一個函數onPageClicked,點擊具體的某一頁後,根據參數page得到頁面,改變當前展現的頁面碼數字,並對分頁信息修改,而後再次調用ajax得到新數據。
1 onPageClicked: function (event, originalEvent, type, page) { 2 options.currentPage = page; 3 viewModel.pageEntity.PageIndex = page; 4 viewModel.getBookData(); 5 },
至此,簡單分頁功能便搞定了,在此實現中,偏重於前端實現分頁邏輯,後臺只是取得相應的數據。
3、單條件查詢分頁
複製一份簡單分頁後,改造下加入一個根據書名條件項,查詢後完成分頁功能。
首先加入書名綁定,在此用了兩個書名,第二個是有目的性的留着,也只是個人理解,條件查詢後,若是底部展現有多頁,那麼我在點擊每一頁的時候,個人查詢條件不能動吧,基於此考慮的。
1 bookName: ko.observable(), 2 bookNameBackup: ko.observable(),
數據查詢部分改動不大,主要是把查詢條件加入進來,所以只改動data參數便可。
1 getBookData: function () { 2 var pageEntity = ko.mapping.toJS(viewModel.pageEntity); 3 $.ajax({ 4 url: '@Url.Action("SingleQueryGetData")', 5 type: 'POST', 6 dataType: 'json', 7 data: {"pageEntity":pageEntity, "bookName":viewModel.bookName()}, 8 success: function (result) { 9 ko.mapping.fromJS(result.data, viewModel.bookList); 10 options.totalPages = result.totalCount > 0 ? result.totalCount % options.numberOfPages == 0 ? 11 result.totalCount / options.numberOfPages : (result.totalCount / options.numberOfPages) + 1 : 1; 12 $('#pagination').bootstrapPaginator(options); 13 } 14 }); 15 },
前端Html部分加入單條件項,以書名爲例,查詢按鈕綁定點擊後的觸發函數
1 <div class="form-group"> 2 <label for="bookName" class="col-sm-2 control-label">書名</label> 3 <div class="col-sm-2"> 4 <input type="text" class="form-control" id="bookName" data-bind="value:bookName"> 5 </div> 6 <button class="btn btn-primary col-sm-1" data-bind="click:queryBookList">查詢</button> 7 </div>
點擊查詢後執行函數,將當前頁面重置爲1,查詢數據,並記錄該次查詢的查詢條件。
1 //查詢 2 queryBookList: function () { 3 options.currentPage = 1; 4 viewModel.pageEntity.PageIndex = options.currentPage; 5 viewModel.getBookData(); 6 viewModel.bookNameBackup(viewModel.bookName());//記錄查詢條件,分頁點擊時須要用到 7 }
查詢完畢,而後假設底部還存在不少頁面碼能夠選擇,當咱們點擊頁面碼的時候,由於查詢條件仍然存在,咱們點擊後仍然會完成分頁功能,可是!!!
當把查詢條件清空,好比在此demo中,把書名置空,此時不點查詢按鈕,而是直接點擊頁面碼,那狀況會如何呢,查詢條件已經沒了;
或是說查詢條件爲空時,輸入查詢條件,不點查詢按鈕,直接點擊頁面碼;
這都是不合理的情形,點擊頁面碼的時候確定得保證原查詢條件的存在,至少我是這麼想的,或許您有更好的建議,請告訴我,十分感謝。這也就是我在以前設置了一個監控對象bookNameBackup的緣由,備份查詢條件,防止點擊頁碼切換時查詢條件變動的情形。
前端已經改好了,而後進入後端來改一下,先對條件判空處理,而後執行相應的邏輯。
1 [HttpPost] 2 public IActionResult SingleQueryGetData(PageRequestViewModel pageEntity, string bookName) 3 { 4 IEnumerable<Book> bookList = PageDataSeed.GetPageDataList(); 5 PageResultViewModel<Book> pageResultViewModel = null; 6 7 if (!string.IsNullOrEmpty(bookName)) 8 bookList = bookList.Where(b => b.BookName.Contains(bookName)); 9 10 var books = bookList.Skip((pageEntity.PageIndex - 1) * pageEntity.PageSize) 11 .Take(pageEntity.PageSize); 12 13 pageResultViewModel = new PageResultViewModel<Book>() 14 { 15 PageIndex = pageEntity.PageIndex, 16 PageSize = pageEntity.PageSize, 17 TotalCount = bookList.Count(), 18 Data = books 19 }; 20 21 return Json(pageResultViewModel); 22 }
單條件查詢分頁功能也就搞定了,查詢和點擊頁面碼所執行的邏輯是不同的,雖然都調用到了最終的數據查詢方法,可是對於條件的處理是不同的。
4、多條件查詢分頁
對單條件查詢分頁下多加入幾個條件,進入到多條件查詢,變化其實不大,只是爲了方便管理如此多的查詢條件,改爲了以對象形式管理
首先綁定查詢對象信息,該對象中包括了三個查詢條件,書名,做者,出版社,仍然設置一個備份對象,緣由和單條件查詢下是同樣的。
1 queryItemEntity: ko.mapping.fromJS(@Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(new QueryItemViewModel()))),//查詢條件信息 2 queryItemEntityBackup: ko.mapping.fromJS(@Html.Raw(Newtonsoft.Json.JsonConvert.SerializeObject(new QueryItemViewModel()))),//查詢條件信息備份
頁面的查詢條件多了,經過queryItemEntity爲前綴展現,也方便維護
1 <div id="queryItem" class="form-horizontal"> 2 <div class="form-group"> 3 <label for="bookName" class="col-sm-2 control-label">書名</label> 4 <div class="col-sm-2"> 5 <input type="text" class="form-control" id="bookName" data-bind="value:queryItemEntity.BookName"> 6 </div> 7 <label for="author" class="col-sm-2 control-label">做者</label> 8 <div class="col-sm-2"> 9 <input type="text" class="form-control" id="author" data-bind="value:queryItemEntity.Author"> 10 </div> 11 <label for="press" class="col-sm-2 control-label">出版社</label> 12 <div class="col-sm-2"> 13 <input type="text" class="form-control" id="press" data-bind="value:queryItemEntity.Press"> 14 </div> 15 </div> 16 <button class="btn btn-primary" data-bind="click:queryBookList">查詢</button> 17 </div>
查詢條件改動,也使得數據查詢部分的條件改變了,將查詢條件總體封裝,而不是零散的傳遞,改動之處加入了第三行將ko上的查詢條件信息轉換爲JS對象和改變了參數data的值。
1 getBookData: function () { 2 var pageEntity = ko.mapping.toJS(viewModel.pageEntity); 3 var queryItemEntity = ko.mapping.toJS(viewModel.queryItemEntity); 4 5 $.ajax({ 6 url: '@Url.Action("MultipleQueryGetData")', 7 type: 'POST', 8 dataType: 'json', 9 data: { "pageEntity": pageEntity, "queryItemEntity": queryItemEntity}, 10 success: function (result) { 11 ko.mapping.fromJS(result.data, viewModel.bookList); 12 options.totalPages = result.totalCount > 0 ? result.totalCount % options.numberOfPages == 0 ? 13 result.totalCount / options.numberOfPages : (result.totalCount / options.numberOfPages) + 1 : 1; 14 $('#pagination').bootstrapPaginator(options); 15 } 16 }); 17 },
點擊按鈕查詢處的邏輯仍是沒有改變,仍然是查詢記錄並將查詢條件備份,用於分頁控件中頁面碼的點擊函數。
1 //查詢 2 queryBookList: function () { 3 options.currentPage = 1; 4 viewModel.pageEntity.PageIndex = options.currentPage; 5 viewModel.getBookData(); 6 ko.mapping.fromJS(viewModel.queryItemEntity, viewModel.queryItemEntityBackup);//記錄查詢條件,分頁點擊時須要用到 7 }
對於前端來說改動並非很大,無非是將查詢條件封裝一下,一樣後端的改動只是多個查詢條件的過濾,依次比對三個查詢條件是否爲空,並挨個去完成查詢的過濾。
1 [HttpPost] 2 public IActionResult MultipleQueryGetData(PageRequestViewModel pageEntity, QueryItemViewModel queryItemEntity) 3 { 4 var bookList = PageDataSeed.GetPageDataList(); 5 6 #region 條件過濾 7 if (!string.IsNullOrEmpty(queryItemEntity.BookName)) 8 bookList = bookList.Where(b => b.BookName.Contains(queryItemEntity.BookName)).ToList(); 9 if (!string.IsNullOrEmpty(queryItemEntity.Author)) 10 bookList = bookList.Where(b => b.Author.Contains(queryItemEntity.Author)).ToList(); 11 if (!string.IsNullOrEmpty(queryItemEntity.Press)) 12 bookList = bookList.Where(b => b.Press.Contains(queryItemEntity.Press)).ToList(); 13 #endregion 14 15 var books = bookList.Skip((pageEntity.PageIndex - 1) * pageEntity.PageSize).Take(pageEntity.PageSize); 16 17 var pageResultViewModel = new PageResultViewModel<Book>() 18 { 19 PageIndex = pageEntity.PageIndex, 20 PageSize = pageEntity.PageSize, 21 TotalCount = bookList.Count(), 22 Data = books 23 }; 24 25 return Json(pageResultViewModel); 26 }
多條件查詢分頁的效果展現
至此,查詢功能算是簡單完成了,或許還有漏洞地方,沒有發現,特別是邏輯漏洞,防不勝防,望多指教,感謝。
設計一個小Demo一方面是對分頁功能進行一下總結,以防本身若干天或是若干年後還須要,能夠回來看看,一方面也是若是有須要的朋友能夠加快編碼和設計的過程。
碼雲上存放Demo的地址:https://gitee.com/530521314/Pagination.git
2018-6-24,望技術有成後能回來看見本身的腳步
圖片在線裁剪和圖片上傳總結
上週須要作一個圖片上傳而且將上傳的圖片在線能夠裁剪展現,以爲這個功能頗有用,可是找參考資料的時候卻並非不少,所以來將我用到的總結總結,也讓有須要的博友們直接借鑑。
首先環境介紹:
一、asp.net mvc網站,用到的前端插件是JCrop和Bootstrap-fileinput,在後端用框架自帶的一些類庫進行處理便可。
JCrop插件是用來裁剪圖片的,頁面上裁剪就是保存裁剪的位置信息,而後將位置信息轉給後臺在後臺進行實際圖片裁剪功能。
插件地址:http://code.ciaoca.com/jquery/jcrop/demo/
Bootstrap-fileinput插件是Bootstrap下的文件上傳的插件,功能強大,我將依靠這個完成文件的上傳,固然也可以使用其餘的文件上傳工具。
插件地址:http://plugins.krajee.com/file-input
文件上傳後頁面上展現的圖片是以Data URI Scheme方式進行展現的。
Data URI Scheme知識點:https://blog.csdn.net/aoshilang2249/article/details/51009947
二、asp.net core mvc網站,前端插件不變,可是在後端不可以使用自帶類庫了,core下面的圖片處理相關的類庫尚未徹底移植過來,只可以借用第三方類庫SixLabors.ImageSharp。
快速瀏覽
@{ ViewBag.Title = "文件上傳"; } <link href="@Url.Content("~/Content/Jcrop/jquery.Jcrop.css")" rel="stylesheet" /> <link href="@Url.Content("~/Content/bootstrap-fileinput/fileinput.css")" rel="stylesheet" /> <br /> <button class="btn btn-primary" data-toggle="modal" data-target="#myModal">頭像</button> <div class="modal fade" id="myModal" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true"> <div class="modal-dialog"> <div class="modal-content"> <div class="modal-header"> <button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button> <h4 class="modal-title" id="myModalLabel">圖片上傳</h4> </div> <div class="modal-body"> <div class="row"> <div class="col-md-6" style="width: 300px;"> <img id="cut-img" class="thumbnail" style="width: 300px;height:300px;" src="~/Content/defaultAvatar.jpg"><br /> </div> <div class="col-md-5"> <input type="file" name="txt_file" id="txt_file" multiple class="file-loading" /><br /> <h4>圖片說明:</h4> <p>一、圖片格式須要jpg、gif、png爲後綴名.</p> <p>二、圖片能夠在線裁剪大小,以裁剪後爲最終結果.</p> <p>三、圖片上傳完畢便可關閉窗口.</p> </div> </div> </div> </div> </div> </div> @section scripts{ <script src="@Url.Content("~/Scripts/Jcrop/jquery.Jcrop.js")"></script> <script src="@Url.Content("~/Scripts/bootstrap-fileinput/fileinput.js")"></script> <script src="@Url.Content("~/Scripts/bootstrap-fileinput/zh.js")"></script> <script type="text/javascript"> //http://code.ciaoca.com/jquery/jcrop/ //http://code.ciaoca.com/jquery/jcrop/demo/animation //http://plugins.krajee.com/file-input //http://plugins.krajee.com/file-advanced-usage-demo#advanced-example-5 var tailorInfo = ""; //初始化fileinput function FileInput() { var oFile = new Object(); oFile.Init = function (ctrlName, uploadUrl) { var control = $('#' + ctrlName); //初始化上傳控件的樣式 control.fileinput({ language: 'zh', //設置語言 browseLabel: '選擇', browseIcon: "<i class=\"glyphicon glyphicon-picture\"></i> ", browseClass: "btn btn-primary", //按鈕樣式 uploadUrl: uploadUrl, //上傳的地址 allowedFileExtensions: ['jpg', 'gif', 'png'],//接收的文件後綴 showUpload: true, //是否顯示上傳按鈕 showCaption: false,//是否顯示標題 showPreview: false,//隱藏預覽 dropZoneEnabled: false,//是否顯示拖拽區域 uploadAsync: true,//採用異步 autoReplace: true, //minImageWidth: 50, //minImageHeight: 50, //maxImageWidth: 1000, //maxImageHeight: 1000, //maxFileSize: 0,//單位爲kb,若是爲0表示不限制文件大小 //minFileCount: 0, maxFileCount: 1, //表示容許同時上傳的最大文件個數 enctype: 'multipart/form-data', validateInitialCount: true, previewFileIcon: "<i class='glyphicon glyphicon-king'></i>", msgFilesTooMany: "選擇上傳的文件數量({n}) 超過容許的最大數值{m}!", uploadExtraData: function () { return { "tailorInfo": tailorInfo } } }); } return oFile; }; function PageInit() { var jcorp = null; var _this = this; var fileInput = new FileInput(); fileInput.Init("txt_file", "@Url.Action("UpLoadFile")"); var input = $('#txt_file'); //圖片上傳完成後 input.on("fileuploaded", function (event, data, previewId, index) { if (data.response.success) { jcorp.destroy(); $('#cut-img').attr('src', data.response.newImage);//Data URI Scheme形式 //$('#cut-img').attr('src', data.response.newImage + "?t=" + Math.random());//加尾巴解決緩存問題 } alert(data.response.message); }); //選擇圖片後觸發 input.on('change', function (event, data, previewId, index) { var img = $('#cut-img'); if (input[0].files && input[0].files[0]) { var reader = new FileReader(); reader.readAsDataURL(input[0].files[0]); reader.onload = function (e) { img.removeAttr('src'); img.attr('src', e.target.result); img.Jcrop({ setSelect: [0, 0, 260, 290], handleSize: 10, aspectRatio: 1,//選框寬高比 bgFade: false, bgColor: 'black', bgOpacity: 0.3, onSelect: updateCords }, function () { jcorp = this; }); }; if (jcorp != undefined) { jcorp.destroy(); } } function updateCords(obj) { tailorInfo = JSON.stringify({ "PictureWidth": $('.jcrop-holder').css('width'), "PictureHeight": $('.jcrop-holder').css('height'), "CoordinateX": obj.x, "CoordinateY": obj.y, "CoordinateWidth": obj.w, "CoordinateHeight": obj.h }); console.log(tailorInfo); } }); //上傳出現錯誤 input.on('fileuploaderror', function (event, data, msg) { alert(msg); //jcorp.destroy(); //$('#cut-img').attr('src', '/Content/defaultAvatar.jpg'); return false; }); //移除圖片 input.on('fileclear', function (event) { console.log("fileclear"); jcorp.destroy(); $('#cut-img').attr('src', '/Content/defaultAvatar.jpg'); }); }; $(function () { PageInit(); }); </script> }
public class FileInputController : Controller { [HttpGet] public ActionResult UpLoadFile() { return View(); } [HttpPost] public ActionResult UpLoadFile(string tailorInfo) { var success = false; var message = string.Empty; var newImage = string.Empty; try { var tailorInfoEntity = JsonConvert.DeserializeObject<TailorInfo>(tailorInfo); tailorInfoEntity.PictureWidth = tailorInfoEntity.PictureWidth.Replace("px", ""); tailorInfoEntity.PictureHeight = tailorInfoEntity.PictureHeight.Replace("px", ""); var file = HttpContext.Request.Files[0]; if (file != null && file.ContentLength != 0) { newImage = ImageHelper.TailorImage(file.InputStream, tailorInfoEntity); success = true; message = "保存成功"; } } catch (Exception ex) { message = "保存失敗" + ex.Message; } return Json(new { success = success, message = message, newImage = newImage }); } }
/// <summary> /// 前端裁剪信息及前端圖片展現規格 /// </summary> public class TailorInfo { public string PictureWidth { get; set; } public string PictureHeight { get; set; } public int CoordinateX { get; set; } public int CoordinateY { get; set; } public int CoordinateWidth { get; set; } public int CoordinateHeight { get; set; } }
/// <summary> /// 圖片處理 /// </summary> public static class ImageHelper { /// <summary> /// 圖片按照實際比例放大 /// </summary> /// <param name="content"></param> /// <param name="tailorInfo"></param> /// <returns></returns> public static string TailorImage(Stream content, TailorInfo tailorInfo) { var scaleWidth = Convert.ToInt16(tailorInfo.PictureWidth); var scaleHeight = Convert.ToInt16(tailorInfo.PictureHeight); Bitmap sourceBitmap = new Bitmap(content); double scaleWidthPercent = Convert.ToDouble(sourceBitmap.Width) / scaleWidth; double scaleHeightPercent = Convert.ToDouble(sourceBitmap.Height) / scaleHeight; double realX = scaleWidthPercent * tailorInfo.CoordinateX; double realY = scaleHeightPercent * tailorInfo.CoordinateY; double realWidth = scaleWidthPercent * tailorInfo.CoordinateWidth; double realHeight = scaleHeightPercent * tailorInfo.CoordinateHeight; return CropImage(content, (int)realX, (int)realY, (int)realWidth, (int)realHeight); } /// <summary> /// 生成新圖片 /// </summary> /// <param name="content"></param> /// <param name="x"></param> /// <param name="y"></param> /// <param name="cropWidth"></param> /// <param name="cropHeight"></param> /// <returns></returns> public static string CropImage(byte[] content, int x, int y, int cropWidth, int cropHeight) { using (MemoryStream stream = new MemoryStream(content)) { return CropImage(stream, x, y, cropWidth, cropHeight); } } /// <summary> /// 生成新圖片 /// </summary> /// <param name="content"></param> /// <param name="x"></param> /// <param name="y"></param> /// <param name="cropWidth"></param> /// <param name="cropHeight"></param> /// <returns></returns> public static string CropImage(Stream content, int x, int y, int cropWidth, int cropHeight) { using (Bitmap sourceBitmap = new Bitmap(content)) { Bitmap bitSource = new Bitmap(sourceBitmap, sourceBitmap.Width, sourceBitmap.Height); Rectangle cropRect = new Rectangle(x, y, cropWidth, cropHeight); using (Bitmap newBitMap = new Bitmap(cropWidth, cropHeight)) { newBitMap.SetResolution(sourceBitmap.HorizontalResolution, sourceBitmap.VerticalResolution); using (Graphics g = Graphics.FromImage(newBitMap)) { g.InterpolationMode = InterpolationMode.HighQualityBicubic; g.SmoothingMode = SmoothingMode.HighQuality; g.PixelOffsetMode = PixelOffsetMode.HighQuality; g.CompositingQuality = CompositingQuality.HighQuality; g.DrawImage(bitSource, new Rectangle(0, 0, newBitMap.Width, newBitMap.Height), cropRect, GraphicsUnit.Pixel); return BitmapToBytes(newBitMap); } } } } /// <summary> /// 圖片轉到byte數組 /// </summary> /// <param name="source"></param> /// <returns></returns> public static string BitmapToBytes(Bitmap source) { ImageCodecInfo codec = ImageCodecInfo.GetImageEncoders()[4]; EncoderParameters parameters = new EncoderParameters(1); parameters.Param[0] = new EncoderParameter(Encoder.Quality, 100L); using (MemoryStream tmpStream = new MemoryStream()) { source.Save(tmpStream, codec, parameters); byte[] data = new byte[tmpStream.Length]; tmpStream.Seek(0, SeekOrigin.Begin); tmpStream.Read(data, 0, (int)tmpStream.Length); return SaveImageToLocal(data); } } /// <summary> /// 存儲圖片到本地文件系統 /// </summary> /// <param name="binary"></param> public static string SaveImageToLocal(byte[] binary) { //根據用戶信息生成圖片名 todo var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"Content\avatar.png"); File.WriteAllBytes(path, binary); return "data:images/png;base64," + Convert.ToBase64String(binary);//Data URI Scheme形式 //return @"/Content/avatar.png";//Url形式 } }
git地址:https://gitee.com/530521314/TailorImage.git
步驟:
1、引用前端插件
加入JCrop插件,在官網下載或是git上下載都可,引用這兩個文件便可,注意引用地址正確哈。
<link href="@Url.Content("~/Content/Jcrop/jquery.Jcrop.css")" rel="stylesheet" /> <script src="@Url.Content("~/Scripts/Jcrop/jquery.Jcrop.js")"></script>
加入Bootstrap插件,一樣下載或是git上下載,不建議經過nuget或是bower下載,會下載一大堆的東西,不少用不到的。引用以下文件。
<link href="@Url.Content("~/Content/bootstrap-fileinput/fileinput.css")" rel="stylesheet" /> <script src="@Url.Content("~/Scripts/bootstrap-fileinput/fileinput.js")"></script> <script src="@Url.Content("~/Scripts/bootstrap-fileinput/zh.js")"></script>
我是直接在頁面上引用的並無將其移動到模板上出於一些因素,暫時能夠不討論這裏,頁面引用結構:
2、前端文件上傳頁面設計
加入文件上傳元素,一行代碼便可,沒錯就一行:
<input type="file" name="txt_file" id="txt_file" multiple class="file-loading" /><br />
加入文件上傳相關的js代碼:
//初始化fileinput function FileInput() { var oFile = new Object(); oFile.Init = function (ctrlName, uploadUrl) { var control = $('#' + ctrlName); //初始化上傳控件的樣式 control.fileinput({ language: 'zh', //設置語言 browseLabel: '選擇', browseIcon: "<i class=\"glyphicon glyphicon-picture\"></i> ", browseClass: "btn btn-primary", //按鈕樣式 uploadUrl: uploadUrl, //上傳的地址 allowedFileExtensions: ['jpg', 'gif', 'png'],//接收的文件後綴 showUpload: true, //是否顯示上傳按鈕 showCaption: false,//是否顯示標題 showPreview: false,//隱藏預覽 dropZoneEnabled: false,//是否顯示拖拽區域 uploadAsync: true,//採用異步 autoReplace: true, //minImageWidth: 50, //minImageHeight: 50, //maxImageWidth: 1000, //maxImageHeight: 1000, //maxFileSize: 0,//單位爲kb,若是爲0表示不限制文件大小 //minFileCount: 0, maxFileCount: 1, //表示容許同時上傳的最大文件個數 enctype: 'multipart/form-data', validateInitialCount: true, previewFileIcon: "<i class='glyphicon glyphicon-king'></i>", msgFilesTooMany: "選擇上傳的文件數量({n}) 超過容許的最大數值{m}!", uploadExtraData: function () { return { "tailorInfo": tailorInfo } } }); } return oFile; };
而後實例化一個fileinput
var fileInput = new FileInput(); fileInput.Init("txt_file", "@Url.Action("UpLoadFile")");
至此文件上傳前端頁面已經OK了,接下來加入圖片裁剪功能。
3、前端圖片裁剪頁面設計
裁剪的話不可以在文件上傳的框中進行直接裁剪,而是經過一個圖片標籤進行裁剪,所以加入一個圖片標籤
<img id="cut-img" class="thumbnail" style="width: 300px;height:300px;" src="~/Content/defaultAvatar.jpg"><br />
裁剪的js代碼
var input = $('#txt_file'); //選擇圖片後觸發 input.on('change', function (event, data, previewId, index) { var img = $('#cut-img'); if (input[0].files && input[0].files[0]) { var reader = new FileReader(); reader.readAsDataURL(input[0].files[0]); reader.onload = function (e) { img.removeAttr('src'); img.attr('src', e.target.result);
//關鍵在這裏 img.Jcrop({ setSelect: [0, 0, 260, 290], handleSize: 10, aspectRatio: 1,//選框寬高比 bgFade: false, bgColor: 'black', bgOpacity: 0.3, onSelect: updateCords }, function () { jcorp = this; }); }; if (jcorp != undefined) { jcorp.destroy(); } } function updateCords(obj) { tailorInfo = JSON.stringify({ "PictureWidth": $('.jcrop-holder').css('width'), "PictureHeight": $('.jcrop-holder').css('height'), "CoordinateX": obj.x, "CoordinateY": obj.y, "CoordinateWidth": obj.w, "CoordinateHeight": obj.h }); console.log(tailorInfo); } });
至此前端頁面文件上傳和裁剪能夠直接使用了。
4、後端文件上傳和裁剪設計
將裁剪信息傳遞到後端,前端可使用到Bootstrap-fileinput的一個屬性uploadExtraData,能夠傳遞除文件外的一些信息過來。固然若是看了Bootstrap-fileinput,實際上是有兩種模式,模式一是直接表單提交,額外屬性直接用相關字段便可保存提交,我用的是模式二,異步上傳,額外的信息只能經過屬性uploadExtraData來保存提交。
得到上傳過來的圖片信息,經過圖片輔助類的處理獲得圖片的的地址,這個是用的DataURI Scheme協議,能夠直接在圖片上展現圖片,而不須要用相對/絕對路徑的形式,某寶好像就是這樣。這樣一來,小圖片能夠直接轉換成Base64的形式保存在數據庫,而不須要依賴本地的文件系統了,不錯額。
[HttpPost] public ActionResult UpLoadFile(string tailorInfo) { var success = false; var message = string.Empty; var newImage = string.Empty; try { var tailorInfoEntity = JsonConvert.DeserializeObject<TailorInfo>(tailorInfo); tailorInfoEntity.PictureWidth = tailorInfoEntity.PictureWidth.Replace("px", ""); tailorInfoEntity.PictureHeight = tailorInfoEntity.PictureHeight.Replace("px", ""); var file = HttpContext.Request.Files[0]; if (file != null && file.ContentLength != 0) { newImage = ImageHelper.TailorImage(file.InputStream, tailorInfoEntity); success = true; message = "保存成功"; } } catch (Exception ex) { message = "保存失敗" + ex.Message; } return Json(new { success = success, message = message, newImage = newImage }); }
圖片輔助類
/// <summary> /// 圖片處理 /// </summary> public static class ImageHelper { /// <summary> /// 圖片按照實際比例放大 /// </summary> /// <param name="content"></param> /// <param name="tailorInfo"></param> /// <returns></returns> public static string TailorImage(Stream content, TailorInfo tailorInfo) { var scaleWidth = Convert.ToInt16(tailorInfo.PictureWidth); var scaleHeight = Convert.ToInt16(tailorInfo.PictureHeight); Bitmap sourceBitmap = new Bitmap(content); double scaleWidthPercent = Convert.ToDouble(sourceBitmap.Width) / scaleWidth; double scaleHeightPercent = Convert.ToDouble(sourceBitmap.Height) / scaleHeight; double realX = scaleWidthPercent * tailorInfo.CoordinateX; double realY = scaleHeightPercent * tailorInfo.CoordinateY; double realWidth = scaleWidthPercent * tailorInfo.CoordinateWidth; double realHeight = scaleHeightPercent * tailorInfo.CoordinateHeight; return CropImage(content, (int)realX, (int)realY, (int)realWidth, (int)realHeight); } /// <summary> /// 生成新圖片 /// </summary> /// <param name="content"></param> /// <param name="x"></param> /// <param name="y"></param> /// <param name="cropWidth"></param> /// <param name="cropHeight"></param> /// <returns></returns> public static string CropImage(byte[] content, int x, int y, int cropWidth, int cropHeight) { using (MemoryStream stream = new MemoryStream(content)) { return CropImage(stream, x, y, cropWidth, cropHeight); } } /// <summary> /// 生成新圖片 /// </summary> /// <param name="content"></param> /// <param name="x"></param> /// <param name="y"></param> /// <param name="cropWidth"></param> /// <param name="cropHeight"></param> /// <returns></returns> public static string CropImage(Stream content, int x, int y, int cropWidth, int cropHeight) { using (Bitmap sourceBitmap = new Bitmap(content)) { Bitmap bitSource = new Bitmap(sourceBitmap, sourceBitmap.Width, sourceBitmap.Height); Rectangle cropRect = new Rectangle(x, y, cropWidth, cropHeight); using (Bitmap newBitMap = new Bitmap(cropWidth, cropHeight)) { newBitMap.SetResolution(sourceBitmap.HorizontalResolution, sourceBitmap.VerticalResolution); using (Graphics g = Graphics.FromImage(newBitMap)) { g.InterpolationMode = InterpolationMode.HighQualityBicubic; g.SmoothingMode = SmoothingMode.HighQuality; g.PixelOffsetMode = PixelOffsetMode.HighQuality; g.CompositingQuality = CompositingQuality.HighQuality; g.DrawImage(bitSource, new Rectangle(0, 0, newBitMap.Width, newBitMap.Height), cropRect, GraphicsUnit.Pixel); return BitmapToBytes(newBitMap); } } } } /// <summary> /// 圖片轉到byte數組 /// </summary> /// <param name="source"></param> /// <returns></returns> public static string BitmapToBytes(Bitmap source) { ImageCodecInfo codec = ImageCodecInfo.GetImageEncoders()[4]; EncoderParameters parameters = new EncoderParameters(1); parameters.Param[0] = new EncoderParameter(Encoder.Quality, 100L); using (MemoryStream tmpStream = new MemoryStream()) { source.Save(tmpStream, codec, parameters); byte[] data = new byte[tmpStream.Length]; tmpStream.Seek(0, SeekOrigin.Begin); tmpStream.Read(data, 0, (int)tmpStream.Length); return SaveImageToLocal(data); } } /// <summary> /// 存儲圖片到本地文件系統 /// </summary> /// <param name="binary"></param> public static string SaveImageToLocal(byte[] binary) { //根據用戶信息生成圖片名 todo var path = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, @"Content\avatar.png"); File.WriteAllBytes(path, binary); return "data:images/png;base64," + Convert.ToBase64String(binary);//Data URI Scheme形式 //return @"/Content/avatar.png";//Url形式 } }
最後這一步能夠控制你須要的形式,是要使用路徑形式仍是Data URI Scheme形式。
第一個方法將圖片根據得到的位置信息,進行比例縮放,而後一系列處理獲得新圖片。再將圖片返回給前端展現。
裁剪信息的類我是這樣設計的,可是裏面的內容能夠所有自定義,是由前端傳遞過來的信息決定的,應該屬於ViewModel這一類的。
/// <summary> /// 前端裁剪信息及前端圖片展現規格 /// </summary> public class TailorInfo { public string PictureWidth { get; set; } public string PictureHeight { get; set; } public int CoordinateX { get; set; } public int CoordinateY { get; set; } public int CoordinateWidth { get; set; } public int CoordinateHeight { get; set; } }
5、成果展現
至此圖片的在線裁剪搞定了。
6、 asp.net core下的後端設計
因爲core並尚未將圖片相關的類庫移植到core下,可是社區上有大神們已經完成了core下的圖片處理的類庫,我使用的是SixLabors.ImageSharp
地址:https://www.nuget.org/packages/SixLabors.ImageSharp其中介紹了怎麼樣安裝的方法,再也不陳述。
在asp.net core下,我只實現了一個圖片縮放的代碼,若有更多須要,能夠聯繫我,我將嘗試嘗試。
前端頁面無需作改動,後端代碼咱們須要改變了,須要用到ImageSharp中的一些方法和屬性。
asp.net core取文件的方式變了。
var file = HttpContext.Request.Form.Files["txt_file"];
pictureUrl = "data:images/png;base64," + ImageHelper.ImageCompress(file.OpenReadStream());//圖片縮放到必定規格
在圖片輔助類中,經過ImageSharp進行處理便可將圖片縮放,固然也能夠裁剪,可是我沒有去設計了:
public static string ImageCompress(Stream content) { var imageString = string.Empty; using (Image<Rgba32> image = Image.Load(content)) { image.Mutate(x => x .Resize(image.Width / 5, image.Height / 5) .Grayscale()); imageString = image.ToBase64String(ImageFormats.Bmp); } return imageString; }
在此,感謝幾位博友的文章:
@夢遊的龍貓 https://www.cnblogs.com/wuxinzhe/p/6198506.html
https://yq.aliyun.com/ziliao/1140
2018-3-31,望技術有成後能回來看見本身的腳步
按部就班學.Net Core Web Api開發系列【2】:利用Swagger調試WebApi
系列目錄
本系列涉及到的源碼下載地址:https://github.com/seabluescn/Blog_WebApi
1、概述
既然先後端開發徹底分離,那麼接口的測試和文檔就顯得很是重要,文檔維護是一件比較麻煩的事情,特別是變動的文檔,這時採用Swagger就會很是方便,同時解決了測試和接口文檔兩個問題。
2、使用NuGet獲取包
使用NuGet搜索包:Swashbuckle.aspnetcore並安裝。
3、添加代碼
在Startup類的ConfigureServices方法內添加下面代碼(加粗部分)
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddSwaggerGen(option => { option.SwaggerDoc("v1", new Info { Version = "v1", Title = "SaleService接口文檔", Description = "RESTful API for SaleService.", TermsOfService = "None", Contact = new Contact { Name = "seabluescn", Email = "seabluescn@163.com", Url = "" } }); //Set the comments path for the swagger json and ui. var basePath = PlatformServices.Default.Application.ApplicationBasePath; var xmlPath = Path.Combine(basePath, "SaleService.xml"); option.IncludeXmlComments(xmlPath); }); }
在Startup類的Configure方法內添加下面代碼(加粗部分)
public void Configure(IApplicationBuilder app, IHostingEnvironment env) { app.UseMvcWithDefaultRoute(); app.UseSwagger(); app.UseSwaggerUI(option => { option.ShowExtensions(); option.SwaggerEndpoint("/swagger/v1/swagger.json", "SaleService V1"); }); }
4、配置
須要在生成配置選項內勾選XML文檔文件,項目編譯時會生成該文件。Debug和Release模式都設置一下。不然會報一個System.IO.FileNotFoundException的錯誤。
5、啓動
啓動項目,瀏覽器輸入:http://localhost:50793/swagger/ 便可。
也能夠修改launchsettings.json文件,默認項目啓動時運行swagger
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
{
"iisSettings"
: {
"windowsAuthentication"
:
false
,
"anonymousAuthentication"
:
true
,
"iisExpress"
: {
"applicationUrl"
:
"http://localhost:50792/"
,
"sslPort"
: 0
}
},
"profiles"
: {
"IIS Express"
: {
"commandName"
:
"IISExpress"
,
"launchBrowser"
:
true
,
"launchUrl"
:
"api/values"
,
"environmentVariables"
: {
"ASPNETCORE_ENVIRONMENT"
:
"Development"
}
},
"SaleService"
: {
"commandName"
:
"Project"
,
"launchBrowser"
:
true
,
"launchUrl"
:
"swagger"
,
"environmentVariables"
: {
"ASPNETCORE_ENVIRONMENT"
:
"Development"
},
"applicationUrl"
:
"http://localhost:50793/"
}
}}
|
若是你對你的控制器方法進行了註釋,swagger接口頁面就會體現出來。
/// <summary> /// 根據產品編號查詢產品信息 /// </summary> /// <param name="code">產品編碼</param> /// <returns>產品信息</returns> [HttpGet("{code}")] public Product GetProduct(String code) { var product = new Product { ProductCode = code, ProductName = "啫喱水" }; return product; }
下面爲Swagger的首頁:能夠用它進行API的調試了。