1. 程式人生 > >與介面相關的設計模式(1):定製服務模式和介面卡模式詳解

與介面相關的設計模式(1):定製服務模式和介面卡模式詳解

在Java中我們通常把介面作為系統與外界互動的視窗,接下來我們來考慮以下問題:

  • 如何設計介面?
  • 當兩個系統之間介面不匹配時,如何處理?
  • 但系統A無法便捷的引用系統B的介面的實現類例項時,如何處理?
  • ……

為了解決以上問題,需要引入與介面相關的設計模式,接下來介紹定製服務模式、介面卡模式、預設介面卡模式、代理模式、標識型別模式和常量介面模式。

- 定製服務模式

在如今的商業領域,很流行定製服務。例如電信公司會制定各種各樣的服務套餐,滿足各種客戶的需求。下表是電信公司為個人使用者定製的兩款寬頻服務套餐:

極速精英套餐 金融專網套餐
寬頻上網服務(限速2Mbps) 電信金融專網服務(限速1Mbps)
線上防毒服務 線上防毒服務
50MB郵箱服務 28MB網路硬碟服務
5MB郵箱服務
價格:140元無限包月 價格:150元無限包月

當一個系統能對外提供多種型別的服務時,一種方式是設計粗粒度的介面,把所有的服務放在一個介面中宣告,這個介面臃腫龐大,所有的使用者都訪問同一個介面;還有一種方式是設計精粒度的介面,對服務精心分類,針對使用者的需求提供特定的介面。

顯然第二種精粒度的介面方式會讓系統更加容易維護,精粒度的介面可以減輕軟體提供商軟體維護成本。假如某個精粒度的介面不得不發生變更,那麼也只會影響到一小部分訪問該介面使用者。此外,精粒度的介面更有利於介面的重用,通過對介面的繼承,可以方便的生成針對特定使用者的複合介面。

在上述例子中,可以抽象出5個精粒度的介面,代表5種服務,這5種服務分別是:

  1. 寬頻上網服務 BroadbandService
  2. 網路硬碟服務 NetworkDiskService
  3. 線上防毒服務 VirusKillingService
  4. 郵箱服務 MailboxService
  5. 金融專網服務 FinancialNetworkService

上表中的極速精英套餐SuperSpeedCombo和金融專網套餐FinanceCombo屬於兩種定製的服務介面,它們可以通過繼承以上5個精粒度的介面而形成,這樣的介面也稱為複合介面。

服務介面定製好以後,接下來的問題是如何實現這些介面。為了提高程式碼的可重用性,類的粒度也應該儘可能小

,所以首先為精粒度的介面提供實現類。

以下列出其中的一個服務實現類:

public class BroadbandServiceImpl implements BroadbandService{
    private int speed;//網速
    public BroadbandServiceImpl(int speed){
        this.speed = speed;
    }
    //連線網路
    public void connect(String username,String password){...}

    //斷開網路
    public void disconnect(){...}
}

同上,將精粒度的介面一一建立實現類,得到精粒度的類。

那麼對於SuperSpeedCombo 和 FinanceCombo 複合介面,如何實現它們呢?以 SuperSpeedCombo介面的實現類 SuperSpeedComboImpl為例,可以採用組合手段,複用 BroadbandService介面、VirusKillingService介面和MailboxService介面的實現類的程式程式碼。

那麼什麼是組合關係呢?在這再複習一下,所謂的組合和繼承都是提高程式碼可重用性的手段,繼承最大的弱點就是破壞封裝,子類和父類之間緊密耦合,子類依賴於父類的實現,子類缺乏獨立性,而組合關係不會破壞封裝,整體類與區域性類之間鬆耦合,彼此相互獨立。當然組合關係也有缺點:建立整體類的物件時需要建立所有區域性類的物件,而繼承關係在建立子類的物件時無須建立父類的物件。

比如要在SuperSpeedComboImpl採用組合手段加入寬頻上網服務BroadbandService:

public class SuperSpeedComboImpl implements SuperSpeedCombo{
    private BroadbandServiceImpl BroadbandService;
    public SuperSpeedComboImpl(BroadbandServiceImpl BroadbandService){
        this.BroadbandService = BroadbandService;
    }
}

此外,對於極速精英套餐和金融專網套餐,都有付費方式和價格這些屬性,可以把這些屬性放到同一個Payment中,這符合構建精粒度的物件模型的原則,下面是Payment的源程式:

public class Payment{
    public static final String TYPE_PER_YEAR="按年付費";
    public static final String TYPE_PER_MONTH="按月付費";
    private String type;//付費方式
    private double price;//價格
    public Payment(String type, double price) {
        this.type = type;
        this.price = price;
    }
    //省略type屬性和price屬性的get/set方法
    ...
}

SuperSpeedComboImpl類的源程式如下:

public class SuperSpeedComboImpl implements SuperSpeedCombo{
    private BroadbandServiceImpl BroadbandService;
    private VirusKillingService virusKillingService;
    private MailboxService mailboxService;
    private Payment payment;
    public SuperSpeedComboImpl(BroadbandServiceImpl broadbandService, VirusKillingService virusKillingService,
            MailboxService mailboxService, Payment payment) {
        super();
        BroadbandService = broadbandService;
        this.virusKillingService = virusKillingService;
        this.mailboxService = mailboxService;
        this.payment = payment;
    }
    public BroadbandServiceImpl getBroadbandService() {
        return BroadbandService;
    }
    public void setBroadbandService(BroadbandServiceImpl broadbandService) {
        BroadbandService = broadbandService;
    }
    public VirusKillingService getVirusKillingService() {
        return virusKillingService;
    }
    public void setVirusKillingService(VirusKillingService virusKillingService) {
        this.virusKillingService = virusKillingService;
    }
    public MailboxService getMailboxService() {
        return mailboxService;
    }
    public void setMailboxService(MailboxService mailboxService) {
        this.mailboxService = mailboxService;
    }
    public Payment getPayment() {
        return payment;
    }
    public void setPayment(Payment payment) {
        this.payment = payment;
    }
}

下面建立一個極速精英套餐服務的一個例項:

//建立付費資訊,按年付費,價格1555
Payment payment = new Payment(Payment.TYPE_PER_MONTH,1555);

//建立寬頻上網服務,網速2Mbps
BroadbandService broadbandService = new BroadbandServiceImpl(2);

//建立郵箱服務,50MB容量
MailboxService mailboxService = new MialboxServiceImpl(50);

//建立線上防毒服務
VirusKillingService virusKillingService = new VirusKillingServiceImpl();

//創建極速精英套餐服務
SuperSpeedCombo superSpeedCombo = 
    new SuperSpeedComboImpl(broadbandService,mailboxService,virusKillingService,payment);

- 介面卡模式

鬆耦合的系統之間通過介面來互動,當兩個系統之間的介面不匹配時,就需要用介面卡來把一個系統的介面轉換為與另一個系統匹配的介面。可見,介面卡的作用是進行介面轉換。

在面向物件領域,也採用介面卡模式來進行介面的轉換。介面卡模式有三種實現方式:

  1. 類的介面卡模式(繼承實現方式)
  2. 物件的介面卡模式(組合實現方式)
  3. 介面的介面卡模式(抽象類實現方式)

首先我們來看類的介面卡模式,以下分別為兩個介面:

package com.adapter;

public interface SourceIFC {//源介面
    public int add(int a,int b);
}
package com.adapter;

public interface TargetIFC {//目標介面
    public int addOne(int a);
}

顯然這兩個介面是不匹配的,就像220V的電源和膝上型電腦需要的15V的電源,中間需要一個電源介面卡,這兩個介面之間也需要一個介面卡才能匹配。介面卡會將其中的一個系統的介面轉換為與另一個系統匹配的介面。

其中一個系統的介面實現如下:

package com.adapter;

public class SourceImpl implements SourceIFC{

    @Override
    public int add(int a, int b) {
        return a + b;
    }
}

下面就要考慮如何實現TargetIFC介面,才能使之與SourceIFC介面匹配呢? 程式碼如下:

package com.adapter;

public class TargetImpl extends SourceImpl implements TargetIFC{

    @Override
    public int addOne(int a) {
        return add(a,1);//呼叫父類SourceImpl的add(int a,int b)方法
    }

}

上面的TargetImpl就是介面卡,它實現了TargetIFC介面,並且繼承SourceImpl類,從而能重用SourceImpl類的add()方法。

我們來思考一下,SourceIFC的實現類SourceImpl是否有必要存在呢?我們的目的是為了轉換介面使之匹配,對客戶來說只需要呼叫TargetIFC介面,而將TargetIFC介面轉換為SourceIFC介面的工作對客戶來說是透明的。而上面的轉換方法需要先實現SourceIFC介面,再讓介面卡去繼承SourceIFC實現類,而繼承關係只能把一個源介面轉換為一個目標介面。那麼有沒有別的更好的方法來實現介面的轉換呢?

下面來介紹物件的介面卡模式,物件的介面卡模式是通過組合方式實現的。同樣TargetImpl為介面卡,它只實現了TargetIFC介面,並不需要繼承SourceIFC的實現類,而是在TargetImpl介面卡類的內部對SourceImpl類進行包裝,從而生成新的介面。以下是TargetImpl的源程式:

package com.adapter;

public class TargetImpl implements TargetIFC{

    private SourceIFC source = new SourceImpl();

    @Override
    public int addOne(int a) {
        return source.add(a, 1);
    }

}

上面這個方法只是我個人的思考,在教科書中在TargetIFC介面中聲明瞭addOne(int a)方法和add(int a,int b)方法,然後在物件的介面卡模式中TargetImpl類中程式碼為下:

package com.adapter;

public class TargetImpl implements TargetIFC{

    private SourceIFC source ;
    public TargetImpl(SourceIFC source){
        this.source = source;
    }
    public int add(int a,int b){return source.add(a, b);}
    @Override
    public int addOne(int a) {
        return source.add(a, 1);
    }

}

如果按照書上來寫介面卡的內容的話,在例項化介面卡的之前,還要例項化一個SourceImpl類,然後將SourceImpl例項作為構造引數例項化介面卡。

關於組合關係與繼承關係的優劣,總的來說,組合關係比繼承關係更有利於系統的維護和擴充套件,而且組合關係能夠將多個源介面轉換為一個目標介面,在上文介紹定製服務模式的SuperSpeedComboImpl介面卡就是一個例子,而繼承關係只能把一個源介面轉換為一個目標介面,因此應該優先考慮用組合關係來實現介面卡。

繼續來看第三種介面卡模式:介面的介面卡模式,介面的介面卡模式通過抽象類來實現,十分簡單。比如在java.awt.event包下,MouseListener介面的定義如下:

public interface MouseListener extends EventListener {

    public void mouseClicked(MouseEvent e);
    public void mousePressed(MouseEvent e);
    public void mouseReleased(MouseEvent e);
    public void mouseEntered(MouseEvent e);
    public void mouseExited(MouseEvent e);
}

如果使用者想要處理按下滑鼠鍵的事件,就要建立MouseListener介面的實現類,然後必須實現所有方法,在mousePressed方法下編寫處理按下滑鼠鍵的事件,其他方法則為空方法不加處理。

儘管僅僅想要實現mousePressed()方法,但是不得不為其他的方法提供空的方法體。那麼怎麼來解決這個問題呢?我們可以再定義一個抽象類實現MouseListener介面:

package com.adapter;

import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

public abstract class MyMouseListener implements MouseListener {

    @Override
    public void mouseClicked(MouseEvent e) {}

    @Override
    public void mousePressed(MouseEvent e) {}

    @Override
    public void mouseReleased(MouseEvent e) {}

    @Override
    public void mouseEntered(MouseEvent e) {}

    @Override
    public void mouseExited(MouseEvent e) {}

}

然後如果你想處理滑鼠按下這一個事件,就可以這樣寫:

package com.adapter;

import java.awt.event.MouseEvent;

public class MyMouseListenerAdapter extends MyMouseListener{
    @Override
    public void mousePressed(MouseEvent e) {

    }

}

實際上JDK已經為MouseListener提供了一個預設介面卡MouseAdapter,不同的是MouseAdapter並不是抽象類。但是一般情況下我們都會去繼承MouseAdapter,所以個人認為將這個介面卡定義為抽象類或許更好。