Chapter 5 單件模式
單例模式比全局變量好的是不用程序一開始就初始化全局變量(有的東西的初始化很費(fèi)事兒最好不要都一開始就初始化),可以用到單例的時候再初始化赶盔,主要是將構(gòu)造方法private并且只提供一個對外的getInstance實(shí)現(xiàn)只初始化一次:
public class Singleton
{
private static Singleton uniqueInstance = null;
//其他有用的實(shí)例變量
//構(gòu)造方法是私有的,所以在類外不能new出多個實(shí)例
private Singleton()
{
//初始化其他實(shí)例變量
}
public static Singleton getInstance()
{
if (uniqueInstance == null)
{
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
}
但注意哦榆浓,上面的方式會有線程問題于未,可能還是會產(chǎn)生兩個實(shí)例。
單件模式(又稱單例模式)陡鹃,確保一個類只有一個實(shí)例烘浦,并提供一個全局訪問點(diǎn)。
Chapter 6 命令模式
設(shè)計一個家電自動化遙控器的API萍鲸。這個遙控器具有七個可編程的插槽闷叉,每個插槽都有對應(yīng)的開關(guān)按鈕,這個遙控器還具備一個整體的撤銷按鈕脊阴。希望你能夠創(chuàng)建一組控制遙控器的API握侧,讓每個插槽都能夠控制一個或一組裝置,能夠控制目前的裝置和任何未來可能出現(xiàn)的裝置嘿期,這一點(diǎn)很重要品擎。(這里有一組Java類,這些類時由多個廠商開發(fā)出來的备徐,用來控制家電自動化裝置萄传,例如點(diǎn)燈,風(fēng)扇蜜猾,熱水器盲再,音響設(shè)備和其他類似的可控制裝置西设。)
因?yàn)槊總€家電的接口都不一樣,我們總不好先判斷是什么家電答朋,然后調(diào)用不同的接口,但是我們可以使用命令模式棠笑,把請求例如按下打開電燈按鈕封裝成一個命令對象梦碗,這樣遙控器就不需要知道電燈是怎么做的了。
這里用一個餐廳下單的例子改造舉例:
然后改成命令模式:
就是將菜品的制作改成實(shí)現(xiàn)了command接口蓖救,這樣廚師拿到的時候洪规,可以直接調(diào)用command.execute(),它不會管具體里面是做什么的循捺,菜品自己會執(zhí)行制作斩例。
--
遙控器的改造也是醬紫,首先定義command接口从橘,然后實(shí)現(xiàn)不同電器的command:
interface Command{
public void execute();
}
class LightOnCommand implements Command{
Light light;
public LightOnCommand(Light light){
this.light=light;
}
@Override
public void execute() {
light.on();
}
}
假設(shè)有一個只有一個按鈕的遙控器念赶,就是醬紫的:
class SimpleRemoteControl{
Command slot;
public SimpleRemoteControl(){}
public void setCommand(Command command){
slot=command;
}
public void buttonWasPressed(){
slot.execute();
}
}
// test code
class RemoteControlTest{
public static void main(String[] args){
SimpleRemoteControl remote=new SimpleRemoteControl();
Light light=new Light();
LightOnCommand lightOn=new LightOnCommand(light);
remote.setCommand(lightOn);
remote.buttonWasPressed();
}
}
命令模式:將“請求”封裝成對象,一邊使用不同的請求恰力、隊列或者日志來來參數(shù)化其他對象叉谜。命令模式也支持可撤銷的操作。
空對象
為了不用每次調(diào)用插槽command的時候都先檢查command是不是空踩萎,可以創(chuàng)建一個空command什么都不做來占位停局,這樣就可以避免了判斷null了~
public class NoCommand implements Command {
public void execute() { }
public void undo() { }
}
這里插一句,命令模式的優(yōu)點(diǎn)香府,如果通過讓家具們都實(shí)現(xiàn)某個接口董栽,然后直接把自己給遙控器,遙控器調(diào)用接口也可以企孩,缺點(diǎn)是:
- 如果有一個按鈕控制兩個家具呢锭碳?(如果通過命令模式可以新建一個初始化傳入一組command,然后execute的時候調(diào)用這組command的每一個命令)
- 如果需要增加減少功能柠硕,接口就要變工禾,每個家具類都要變
- 如果又有個什么遙控,家具類就要再增加一套接口蝗柔,但是家具類沒有職責(zé)應(yīng)該這么做吖
Chapter 7 適配器模式與外觀模式
適配器模式:將一個類的接口轉(zhuǎn)換成客戶期望的另一個接口闻葵。適配器讓原本接口不相融的類可以相互合作
例如,當(dāng)我們換了一個新廠商癣丧,不能修改廠商的代碼槽畔,自己的舊代碼由于已經(jīng)提供了接口給別人也不能修改,就可以加一層適配性胁编,我們可以自己提供厢钧,也可以讓廠商提供哈
- 設(shè)計:
實(shí)現(xiàn)想要轉(zhuǎn)換的接口
取得要適配的對象的引用鳞尔,作為局部變量
用要適配的對象的方法實(shí)現(xiàn)接口中的方法
如果有兩個不同的interface分別是火雞和鴨子,但我們只有火雞卻要提供鴨子給外部:
- 客戶使用適配器的過程:
通過目標(biāo)接口調(diào)用適配器方法
適配器使用被適配者的接口轉(zhuǎn)換成對被適配者的調(diào)用
客戶端接受調(diào)用結(jié)果早直,但并未察覺適配器在中轉(zhuǎn)的作用
適配器模式一般只適配(持有)一個類寥假,如果適配多個類也可以啦,但你可以用多個適配器適配一個接口哈霞扬;如果舊接口和新接口共存糕韧,最好讓適配器同時滿足這兩種接口不要用兩套。
之前我們講的是對象適配器喻圃,而類適配器是通過多重繼承代替組合的方式調(diào)用的被適配者萤彩。
類適配器的好處就是他不用新增一個持有的被適配對象,以及不會實(shí)現(xiàn)所有被適配者的接口斧拍,因?yàn)樗抢^承的是有默認(rèn)行為的雀扶。
這里裝飾者是不改變接口加入更多責(zé)任,而適配器是改變接口的哈
外觀模式
外觀模式提供了一個統(tǒng)一的接口肆汹,用來訪問子系統(tǒng)中的一群接口愚墓。外觀定義了一個高層接口,讓子系統(tǒng)更容易使用县踢。
外觀模式不只是簡化了接口转绷,也將客戶從組件的子系統(tǒng)中解耦。
外觀和適配器可以包裝許多類硼啤,但是外觀強(qiáng)調(diào)的是簡化接口议经,而適配器是為了將接口轉(zhuǎn)換成不同的接口。
假設(shè)我們設(shè)計了一個家庭影院谴返,類圖是下面醬紫的煞肾,然后如果我們想要看一場電影,需要打開爆米花嗓袱、打開音效籍救、打開……最后打開dvd,要做一系列的事情非常麻煩渠抹,關(guān)的時候也是要反著一系列操作蝙昙。
外觀只是提供了一些簡化的操作,沒有把子系統(tǒng)的高級操作隔離起來梧却,依然將子系統(tǒng)完整的暴露出來奇颠,因此如果你需要更高級的操作也可以訪問子系統(tǒng)的接口
外觀并沒有實(shí)現(xiàn)新的行為,只是將子系統(tǒng)的操作合理的組合放航。一個子系統(tǒng)可以有多個外觀烈拒,并可以創(chuàng)造分層次的外觀,外觀不只簡化了接口,也將用戶從復(fù)雜的子系統(tǒng)中解耦出來荆几。
外觀vs適配器:
外觀的目的是簡化接口吓妆,適配器的目的是轉(zhuǎn)換接口滿足客戶預(yù)期,和包裝幾個類沒有關(guān)系
OO原則:最少知識
最少知識原則:只和你的密友談話吨铸。
減少對象之間的交互行拢,只留下幾個密友。不要讓太多的類耦合在一起以至于修改系統(tǒng)中的一部分會影響到其他部分诞吱。
反例:
public float getTemp() {
return station.getThermometer().getTemperature();
}
正例:
public float getTemp() {
return station. getTemperature();
}
- 在對象方法內(nèi)剂陡,我們允許調(diào)用哪些對象的方法,簡單理解為以下4個小原則:
該對象本身
方法參數(shù)傳入對象
方法內(nèi)實(shí)例的對象
對象組件
外觀模式其實(shí)幫助了我們實(shí)現(xiàn)最少知識原則狐胎。
Chapter 8 封裝算法
煮茶和咖啡的步驟幾乎是一致的,所以可以通過提取基類來避免重復(fù)歌馍。
public abstract class CaffeineBeverage {
final void prepareRecipe(){
boilWater();
brew();
addCondimennts();
pourInCup();
}
abstract void brew();
abstract void addCondimennts();
public void boilWater(){
System.out.println("把水煮沸");
}
public void pourInCup(){
System.out.println("倒進(jìn)杯子");
}
}
public class Coffee extends CaffeineBeverage{
public void brew(){
System.out.println("用沸水沖泡咖啡");
}
public void addCondimennts(){
System.out.println("添加糖和牛奶");
}
}
public class Tea extends CaffeineBeverage{
public void brew(){
System.out.println("用沸水沖泡茶");
}
public void addCondimennts(){
System.out.println("添加檸檬");
}
}
模板方法定義了一個算法的步驟握巢,并容許子類為一個或多個步驟提供突現(xiàn)。
當(dāng)我們將共同的地方抽出作為父類的時候松却,將來如果有類似的飲料就更容易拓展暴浦,減少了重復(fù)代碼。同時修改的話也只要改父類就好晓锻。
在一個方法中定義一個算法的骨架歌焦,而將一些步驟延遲到子類中。模版方法使得子類可以在不改變算法結(jié)構(gòu)的情況下砚哆,重新定義算法中的某些步驟独撇。
鉤子是一種被聲明在抽象類中的方法,但只有空的或者默認(rèn)的實(shí)現(xiàn)。鉤子的存在, 可以讓子類有能力對算法的不同點(diǎn)進(jìn)行掛鉤躁锁。要不要掛鉤,由子類自行決定纷铣。
public abstract class CaffeineBeverage {
final void prepareRecipe(){
boilWater();
brew();
pourInCup();
/**
* 殘們加上了一個小的條件語句,而該條件是否成立,是由一個
* 具體方法customerWantsCondiments()決定的。
* 如果顧客“想要”調(diào)料,有這時我們才調(diào)用addCondimennts()
*/
if (customerWantsCondiments()){
addCondimennts();
}
}
/**
* 殘們在這里定義了-個方法, (通常)是空的缺省實(shí)現(xiàn)战转。
* 這個方法會返回true,不做別的事搜立。
* 這就是一個鉤子,子類可以覆蓋這個方法,但不見得一定要這么做。
* @return
*/
boolean customerWantsCondiments(){
return true;
}
abstract void brew();
abstract void addCondimennts();
public void boilWater(){
System.out.println("把水煮沸");
}
public void pourInCup(){
System.out.println("倒進(jìn)杯子");
}
}
子類中
public class Coffee extends CaffeineBeverage{
// 用戶輸入的值
private String answer;
public void brew(){
System.out.println("用沸水沖泡咖啡");
}
public void addCondimennts(){
System.out.println("添加糖和牛奶");
}
//覆蓋鉤子槐秧,提供自己的功能
@Override
boolean customerWantsCondiments() {
// 讓用戶根據(jù)他們的輸入來判斷是否需要添加配料
if (answer.toLowerCase().startsWith("y")){
return true;
}else {
return false;
}
}
}
如果是算法中必須的一步就可以用抽象方法啄踊,如果是可選的就可以用鉤子來實(shí)現(xiàn)。鉤子是可以實(shí)現(xiàn)也可以不實(shí)現(xiàn)刁标,不強(qiáng)求的做法颠通。
可選的步驟作為鉤子的話(空或者默認(rèn)實(shí)現(xiàn))就可以讓子類減少必須實(shí)現(xiàn)的抽象方法的數(shù)量啦。
鉤子就類似聲明周期函數(shù)感覺命雀,你可以選擇覆蓋蒜哀,也可以選擇不覆蓋。
好萊塢原則
好萊塢原則:別調(diào)用(打電話給)我們,我們會調(diào)用(打電話給)你撵儿。
好萊塢原則可以給我們一種防止“依賴腐敗”的方法乘客。
當(dāng)高層組件依賴低層組件,而低層組件又依賴高層組件淀歇,而高層組件又依賴邊側(cè)組件易核,而邊側(cè)組件又依賴低層組件時, 依賴腐敗就發(fā)生了。在這種情況下,沒有人可以輕易地搞懂系統(tǒng)是如何設(shè)計的浪默。
在好萊塢原則之下牡直,我們允許低層組件將自己掛鉤到系統(tǒng)上,但是高層組件會決定什么時候和怎樣使用這些低層組件纳决。換句話說,高層組件對待低層組件的方式是“別調(diào)用我們,我們會調(diào)用你”
- 好萊塢原則和依賴倒置原則之間的關(guān)系如何?
依賴倒置原則教我們盡量避免使用具體類碰逸,而多使用抽象。而好菜塢原則是用在創(chuàng)建框架或組件上的一種技巧阔加,好讓低層組件能夠被掛鉤進(jìn)計算中饵史,而且又不會讓高層組件依賴低層組件。兩者的目標(biāo)都是在于解耦,但是依賴倒置原則更加注重如何在設(shè)計中避免依賴胜榔。
好菜塢原則教我們一個技巧胳喷,創(chuàng)建個有彈性的設(shè)計,允許低層結(jié)構(gòu)能夠互相操作夭织,而又防止其他類太過依賴它們吭露。
低層組件結(jié)束的時候經(jīng)常會調(diào)用super的方法,這個是可以的尊惰,只是我們要避免環(huán)狀調(diào)用讲竿。
![java排序中的模板方法images.jianshu.io/upload_images/5219632-deebac020dd36180.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
策略模式會實(shí)現(xiàn)整個算法,而模板模式的子類只實(shí)現(xiàn)部分算法择浊;而且策略是不依賴上層類的戴卜,而模板胡依賴頂層父類,模板會共享一部分代碼例如共通的步驟琢岩;策略模式封裝算法的方式是組合投剥,通過持有不同的實(shí)例實(shí)現(xiàn),模板方法通過繼承封裝不同的算法担孔。
策略模式可以不繼承只是實(shí)現(xiàn)接口江锨,因?yàn)樗恍枰^承默認(rèn)的實(shí)現(xiàn),這點(diǎn)和模板模式是不一樣的糕篇。
而工程模式其實(shí)就是模板模式的一種特殊版本啄育,如果模板里面有一個createXX的方法下放到子類實(shí)現(xiàn),然后模板里直接調(diào)用生成一個instance拌消,那么其實(shí)就是工廠方法挑豌。
Chapter 9 迭代器與組合模式
假設(shè)有兩個集合類,一個是ArrayList,一個是[]氓英,那么當(dāng)一個對象需要操作這兩個集合實(shí)例的時候侯勉,需要分別用不同方式遍歷,這樣是很不方便的铝阐。
所以Java才有迭代器iterator接口址貌,無論是數(shù)組還是ArrayList都可以創(chuàng)建iterator來遍歷。
迭代器接口是醬紫的:
public interface Iterator{
boolean hasNext(); //返回一個布爾值徘键,判斷是否還有更多的元素
Object next(); //返回下一個元素
}
我們來改寫一下print菜單這個事兒:
//實(shí)現(xiàn)迭代器接口 DinerMenu
public class DinerMenuIterator implements Iterator{
MenuItem[] items;
int position = 0; //position記錄當(dāng)前數(shù)組遍歷的位置
public DinerMenuIterator(MenuItem[] items){ //構(gòu)造器需要被傳入一個菜單項的數(shù)組當(dāng)作參數(shù)
this.items=items;
}
public Object next(){ //返回數(shù)組內(nèi)的下一項练对,并遞增其位置
MenuItem menuItem= items[position];
position=position+1;
return menuItem;
}
public boolean hasNext(){
/*檢查是否已經(jīng)取得數(shù)組內(nèi)所有的元素,如果還有元素待遍歷則返回true吹害;
由于使用的是固定長度的數(shù)組螟凭,所以不但要檢查是否超出了數(shù)組長度,也必須檢查是否下一項是null它呀,如果是null赂摆,就沒有其他項了
*/
if(position>=items.length||items[position]==null){
return false;
}else{
return true;
}
}
public class Waitress {
Menu dinerMenu;
public Waitress( Menu dinerMenu) {
this.dinerMenu = dinerMenu;
}
public void printMenu() {
Iterator dinerIterator = dinerMenu.createIterator();
System.out.println("/nLUNCH");
printMenu(dinerIterator);
}
private void printMenu(Iterator iterator) {
while (iterator.hasNext()) {
MenuItem menuItem = (MenuItem)iterator.next();
System.out.print(menuItem.getName() + ", ");
System.out.print(menuItem.getPrice() + " -- ");
System.out.println(menuItem.getDescription());
}
}
這樣以后我們的waitress就不需要知道兩種不同的菜單的具體用什么實(shí)現(xiàn)的菜單存儲了,只要調(diào)用iterator接口即可钟些,把菜單內(nèi)部對外封閉了。
迭代器模式提供一種方法順序訪問一個聚合對象中各個元素, 而又無須暴露該對象的內(nèi)部表示绊谭。
單一責(zé)任
單一責(zé)任原則:一個類應(yīng)該只有一個引起變化的原因政恍。
類的每個責(zé)任都有改變的潛在區(qū)域。超過一個責(zé)任則意味著多個改變的區(qū)域达传。該原則說明了應(yīng)該盡量讓每個類保持單一責(zé)任篙耗。
通過iterator我們改變了菜單的遍歷,讓一切看起來比較方便宪赶,但現(xiàn)在其中一家餐廳提出了子菜單(甜品)宗弯,這個時候我們就需要一個類似樹的結(jié)構(gòu)來存儲了。
組合模式:允許將對象組合成樹形結(jié)構(gòu)來表現(xiàn)“整體/部分”層次結(jié)構(gòu)搂妻。組合能夠讓客戶以抑制的方式處理個別對象以及對象組合蒙保。
組合模式包含組件,組件有兩種:一種是單純的葉節(jié)點(diǎn)欲主,一種是持有一群孩子的組合邓厕,這些孩子可以是葉節(jié)點(diǎn)也還可以是組合。
這里meneComponent接口定義了葉節(jié)點(diǎn) & 組合的方法扁瓢,相當(dāng)于有了兩種responsibility详恼,葉節(jié)點(diǎn)只要實(shí)現(xiàn)它需要的方法即可。
類似frame panel界面也有這種關(guān)系引几。
如果你還想方便一點(diǎn)也可以讓子節(jié)點(diǎn)持有parent指針
這里讓一個接口實(shí)現(xiàn)兩種(節(jié)點(diǎn)以及組合)的特性昧互,其實(shí)是用單一原則換取了透明性,也就是對用戶而言,每個節(jié)點(diǎn)都是一致的敞掘,它可以不用關(guān)心這個點(diǎn)是單純的一個葉節(jié)點(diǎn)還是一個組合的使用叽掘。
這里用stack實(shí)現(xiàn)了根葉節(jié)點(diǎn)的遍歷
策略模式:封裝可互換的行為,并使用委托決定使用哪一個渐逃。
適配器模式:改變一個或多個類的接口够掠。
迭代器模式:提供一個方式來遍歷集合,而無需暴露集合的實(shí)現(xiàn)茄菊。
外觀模式:簡化一群類的接口疯潭。
組合模式:客戶可以將對象的集合以及個別的對象一視同仁。
觀察者模式:當(dāng)某個狀態(tài)改變是面殖,允許一群對象能被通知到竖哩。