設計模式之(9)——介面卡 模式

2022-09-05 18:00:38

  定義:介面卡模式是將一個類的介面轉換成客戶希望的另一個介面,介面卡模式使得原本由於介面不相容而不能一起工作的類可以一起工作,在軟體設計中我們需要將一些「現存的物件」放到新的環境中,而新環境要求的介面是現物件所不能滿足的,我們可以使用這種模式進行介面適配轉換,使得「老物件」符合新環境的要求。

  使用場景:1、系統需要使用現有的類,而此類的介面不符合系統的需要;2、通過介面轉換,將一個類插入另一個類中,用電器來打個比喻:有一個電器的插頭是三腳的,而現有的插座是兩孔的,要使插頭插上插座,我們需要一個插頭轉換器,這個轉換器即是介面卡。

  介面卡模式涉及三個角色:

  1、源(Adaptee):需要被適配的物件或型別,相當於插頭;

  2、介面卡(Adapter):連線源和目標物件的中間物件,相當於轉換器;

  3、目標角色(Target):定義了使用者端期望的介面,相當於插座;

  如何實現呢?

  使用繼承(類介面卡)或者聚合(物件介面卡)已有的物件實現想要的目標介面,優先推薦使用物件介面卡(基於組合優先於繼承)。

  介面卡模式結構圖:

  

  以下我們先以物件適配為例來分析,以下是原始碼:

package cn.com.pep.model.adapter.a1;
/**
 * 
 * @Title: AdvancedMediaPlayer  
 * @Description:  源物件的介面
 * @author wwh 
 * @date 2022-9-5 10:10:14
 */
public interface AdvancedMediaPlayer {

	/**
	 * @Title: playVlc 
	 * @Description:  
	 * @param filename
	 */
	public void playVlc(String filename);
	
	/**
	 * @Title: playMp4 
	 * @Description:  
	 * @param filename
	 */
	public void playMp4(String filename);
}
package cn.com.pep.model.adapter.a1;
/**
 * 
 * @Title: MeidaPlayer  
 * @Description:  目標物件介面
 * @author wwh 
 * @date 2022-9-5 10:08:30
 */
public interface MeidaPlayer {
    
    /**
     * @Title: play 
     * @Description:  
     * @param audioType
     * @param filename
     */
    public void play(String audioType,String filename);

}
package cn.com.pep.model.adapter.a1;

/**
 * 
 * @Title: Mp4MediaPlayer
 * @Description: 源物件
 * @author wwh
 * @date 2022-9-5 10:13:48
 */
public class Mp4MediaPlayer implements AdvancedMediaPlayer {

	@Override
	public void playVlc(String filename) {
		
	}

	@Override
	public void playMp4(String filename) {
		System.err.println("Playing mp4 and filename is:" + filename);
	}
} 
package cn.com.pep.model.adapter.a1;

/**
 * 
 * @Title: VLCMediaPlayer
 * @Description: 源物件
 * @author wwh
 * @date 2022-9-5 10:12:07
 */
public class VLCMediaPlayer implements AdvancedMediaPlayer {

    @Override
    public void playVlc(String filename) {
        System.err.println("Playing vlc and filename is:" + filename);
    }

    @Override
    public void playMp4(String filename) {
        
    }

}
package cn.com.pep.model.adapter.a1;

/**
 * 
 * @Title: MediaPlayerAdapter
 * @Description: 物件介面卡,通過聚合持有一個源物件的參照
 * @author wwh
 * @date 2022-9-5 10:19:10
 */
public class MediaPlayerAdapter implements MeidaPlayer {
    
    /**
     *     通過聚合的方式持有一個源物件的參照
     */
    private AdvancedMediaPlayer player;

    public MediaPlayerAdapter(String filetype) {
        if ("mp4".equalsIgnoreCase(filetype)) {
            player = new Mp4MediaPlayer();
        } else if ("vlc".equalsIgnoreCase(filetype)) {
            player = new VLCMediaPlayer();
        }
    }

    @Override
    public void play(String audioType, String filename) {
        System.err.println("執行了介面卡中的play()方法");
        if ("mp4".equalsIgnoreCase(audioType)) {
            player.playMp4(filename);
        } else if ("vlc".equalsIgnoreCase(audioType)) {
            player.playVlc(filename);
        }
    }

}
package cn.com.pep.model.adapter.a1;
/**
 * @Title: AudioMediaPlayer
 * @Description: 目標物件
 * @author wwh
 * @date 2022-9-5 10:49:33
 */
public class AudioMediaPlayer implements MeidaPlayer {

    private MediaPlayerAdapter adapter;

    @Override
    public void play(String audioType, String filename) {
        if ("mp3".equalsIgnoreCase(audioType)) {
            System.out.println("Playing mp3 and filename is: " + filename);
        } else if ("mp4".equalsIgnoreCase(audioType) || "vlc".equalsIgnoreCase(audioType)) {
            adapter = new MediaPlayerAdapter(audioType);
            adapter.play(audioType, filename);
        }
    }
}
package cn.com.pep.model.adapter.a1;
/**
 * 
 * @Title: AdapterPatternDemo  
 * @Description:  測試程式碼
 * @author wwh 
 * @date 2022-9-5 10:56:33
 */
public class AdapterPatternDemo {
    
    public static void main(String[] args) {
        AudioMediaPlayer player = new AudioMediaPlayer();
        player.play("mp3", "紅日.mp3");
        player.play("mp4", "天下無賊.mp4");
        player.play("vlc", "平凡的世界.vlc");
    }
}

  UML類圖:

  

 

  以上就是物件適配模式,目標介面MediaPlayer中有一個play(String,String)方法,而源物件介面AdvancedMediaPlayer中並沒有這個方法,我們想通過目標介面的物件AudioMediaPlayer實現播放VCL、Mp4格式的檔案,就需要一個MediaPlayerAdapter來對源物件(Mp4MediaPlayer、VLCMedaiPlayer)進行適配,它通過聚合的方式持有一個AdvancedMediaPlayer型別的參照,並且它還實現了與目標物件相同的介面,自然也就包含了和目標物件相同的方法,目標物件AudioMediaPlayer關聯了MediaPlayerAdapter物件的參照,當然就可以實現播放VCL、Mp4格式的檔案了;

  接下來我們說說類介面卡模式

  類適配是通過Adapter類繼承Adaptee(被適配類),同時實現Target介面(因為Java不支援多繼承,所以只能通過實現介面的方式來實現多繼承)來實現的,類介面卡的重點在於類,是通過構造一個繼承Adaptee類來實現介面卡的功能的,而上面提到的物件介面卡重點在於物件,是通過直接在Adapter中聚合Adaptee類來實現的,當需要呼叫特殊功能的時候,直接使用Adapter中聚合的那個Adaptee物件來呼叫特殊的功能即可;

  以下是類介面卡的測試程式碼:

package cn.com.pep.model.adapter.a2;
/**
 * 
 * @Title: Adaptee  
 * @Description:  需要適配的類
 * @author wwh 
 * @date 2022-9-5 15:29:20
 */
public class Adaptee {
    
    public void specificRequest() {
        System.err.println("執行適配類的方法");
    }
}
package cn.com.pep.model.adapter.a2;
/**
 * 
 * @Title: Target  
 * @Description:  目標物件的介面
 * @author wwh 
 * @date 2022-9-5 15:31:05
 */
public interface Target {
    
    /**
     * @Title: calculate 
     * @Description:
     */
    public void calculate();
}
package cn.com.pep.model.adapter.a2;
/**
 * 
 * @Title: ConcreteTarget  
 * @Description:  目標物件
 * @author wwh 
 * @date 2022-9-5 15:49:50
 */
public class ConcreteTarget implements Target{

    @Override
    public void calculate() {
        System.err.println("執行目標類的方法");
    }
}
package cn.com.pep.model.adapter.a2;
/**
 * 
 * @Title: Adapter  
 * @Description:  類介面卡,繼承了需要適配的類,並且實現了目標物件的介面
 * @author wwh 
 * @date 2022-9-5 15:59:37
 */
public class Adapter extends Adaptee implements Target{

    @Override
    public void calculate() {
        specificRequest();
    }
}
package cn.com.pep.model.adapter.a2;

/**
 * 
 * @Title: ClassAdapterDemo
 * @Description: 測試類
 * @author wwh
 * @date 2022-9-5 15:57:58
 */
public class ClassAdapterDemo {

    public static void main(String[] args) {
        // 使用普通功能類
        Target concreteTarget = new ConcreteTarget();// 範例化一個普通類
        concreteTarget.calculate();

        // 使用特殊功能類,即適配類
        Target adapter = new Adapter();
        adapter.calculate();
    }
}

  類適配和物件適配的比較:

  1、類適配使用繼承,是靜態定義的;而物件適配採用的是物件聚合的方式,是動態定義的;

  2、對於類介面卡,由於介面卡Adapter直接繼承了Adaptee,使得Adapter不能和Adaptee的子類一起工作,因為繼承是靜態關係,當介面卡繼承了Adaptee之後,就不可能再去處理Adaptee的子類了;

  3、對於物件介面卡,一個介面卡可以把多種不同的源適配到同一個目標,換言之,同一個介面卡可以把源類和它的子類都適配到目標介面,因為物件介面卡採用的是物件聚合的方式,只要型別正確,是不是子類都無所謂;

  4、對於類適配,介面卡可以重新定義Adaptee的部分行為,相當於子類覆蓋父類別的部分方法實現;

  5、對於物件介面卡,想要直接重新定義Adaptee的行為比較困難,我們可以通過一個Adaptee的子類來重新定義Adaptee的行為,然後讓介面卡聚合這個子類來完成Adaptee類行為的重新定義;

  介面卡模式的優缺點:

  1、更好的複用,系統需要使用現有類的時候,而此類的介面不符合系統的要求,我們就可以使用介面卡模式讓這些功能得到更好的複用;

  2、更好的擴充套件性,在實現介面卡功能的時候,可以呼叫自己開發的功能,從而自然地擴充套件系統的功能;

  3、過多的使用介面卡,會讓系統非常零亂,不易整體進行把握。比如,明明看到呼叫的是A介面,其實內部被適配成了B介面的實現,一個系統如果太多出現這種情況,無異於一場災難。因此如果不是很有必要,可以不使用介面卡,而是直接對系統進行重構。

  建議儘量使用物件介面卡的實現方式,多用合聚合/組合、少用繼承。當然,具體問題具體分析,根據需要來選用實現方式,最適合的才是最好的。介面卡不是在詳細設計時新增的,而是解決正在服役的專案的問題。