最近看了Head First 設計模式一書,開篇的故事講述了設計模式的原則:封裝變化與面向接口編程.
基本需求
故事從編寫一個模擬鴨子的遊戲開始,遊戲要求:
遊戲裏有許多鴨子,一邊游泳戲水,一邊呱呱叫…
該遊戲內部使用面向對象設計,有一個鴨子的超類Duck:
public abstract class Duck{
public void swim(){
//游泳的方法
}
public void quack(){
//呱呱叫的方法
}
public abstract void display(){
//子類要實現的顯示的方法
}
}
因爲所有的鴨子都會游泳和叫,所以在超類中實現了swim()和qucak()方法,而具體顯示出什麼樣和具體的鴨子有關,所以display()方法爲抽象方法.
現在有種鴨子是紅頭鴨RedHeadDuck和綠頭鴨MallardDuck.
紅頭RedHeadDuck代碼:
public class RedHeadDuck extends Duck {
public void display() {
System.out.println("我是紅頭鴨...");
}
}
綠頭鴨MallardDuck代碼:
public class MallardDuck extends Duck {
public void display() {
System.out.println("我是綠頭鴨...");
}
}
需求變化
現在需求發生了變化,想要鴨子能飛行…那不是很簡單嘛,給Duck類加個飛行的方法不就可以了,如下:
public abstract class Duck{
public void swim(){
//游泳的方法
}
public void quack(){
//呱呱叫的方法
}
public void fly(){
//飛行的方法
}
public abstract void display(){
//子類要實現的顯示的方法
}
}
這樣一來,確實綠頭鴨和紅頭鴨都會飛行了.
出現問題
由於公司業務需要,增加橡皮鴨這一角色RubberDuck,如下:
public class RubberDuck extends Duck {
public void display() {
System.out.println("我是橡皮鴨...");
}
}
等等,上面的橡皮鴨貌似不對啊,橡皮鴨不會飛啊!而且橡皮鴨是吱吱叫不是呱呱叫.這該怎麼辦呢?
這還不簡單,直接覆蓋方法不就行了.
public class RubberDuck extends Duck {
public void qucak(){
//吱吱叫...
}
public void fly(){
//什麼也不做...
}
public void display() {
System.out.println("我是橡皮鴨...");
}
}
這樣貌似是解決了,但是問題又來來,如果後來需要增加誘餌鴨DecoyDuck,誘餌鴨不會叫不會飛.怎麼辦?難道還要繼續覆蓋方法麼?
解決問題
既然無法確定以後的鴨子是什麼類型,乾脆抽取公共的部分,不同的寫成接口.
比如會飛的實現Flyable接口,會叫的實現Qucakable接口.
//會飛的接口
public interface Flyable{
void fly();
}
//會叫的接口
public interface Quackable{
void quack();
}
//新的Duck類
public abstract class Duck{
public void swim(){
//游泳的方法
}
public abstract class display(){
//顯示的方法
}
}
//新的綠頭鴨
public class MallardDuck extend Duck implements Flyable, Qucakable {
public void fly(){
//我會飛...
}
public void quack(){
//我會呱呱叫...
}
public void display(){
//我是綠頭鴨
}
}
//新的紅頭鴨類
public class RedHeadDuck extend Duck implements Flyable, Qucakable {
public void fly(){
//我會飛...
}
public void quack(){
//我會呱呱叫...
}
public void display(){
//我是紅頭鴨
}
}
//橡皮鴨
public class RubberDuck extend Duck implements Quackable {
public void quack(){
//我會吱吱叫...
}
public void display(){
//我是橡皮鴨
}
}
//誘餌鴨
public class DecoyDuck extends Duck {
public void display(){
//我是誘餌鴨
}
}
這樣一來,問題就解決了.
新問題
上面的問題是解決了,好像代碼有重複:
綠頭鴨和紅頭鴨的會飛的方法和會呱呱叫的方法是重複的.
如果以後有更多類型的方法,重複的代碼會更多,而且會埋下一個隱患:
如果以後飛行的動作有所改變,難道一個一個類的去修改?
如果需求還有變化,不是更難維護嗎?
解決問題
有沒有好的方法解決這個問題呢?答案是肯定的.我們需要將代碼中的變化的部分與不變的部分拆分出來.這就是封裝變化的原則
封裝變化
找出應用中可能需要變化之處,把它們獨立出來,不要和那些不需要變化的代碼混在一起。
下面就建立兩組類,變化的和不會變化的.
上面的案例中什麼是變化的呢?
飛行和叫聲是變化的.那麼就將飛行和叫聲與Duck類分開.
如何設計鴨子的飛行行爲和叫聲行爲呢?
我們希望一切有彈性,因爲你無法確定以後的飛行行爲會有什麼變化,也無法確定以後的綠頭鴨會有什麼行爲.
這就涉及到第二個原則:面向接口編程
面向接口編程
針對接口編程,而不是針對實現編程
那麼現在的需求有兩個行爲:飛和叫.
接口就爲飛行行爲接口和叫的行爲接口:
//飛行行爲接口
public interface FlyBehaviour{
void fly();
}
//叫的行爲接口
public interface QuackBehaviour{
void quack();
}
現在飛行有種不同的行爲:飛和不會飛.
//普通的飛
public class FlyWithWings implements FlyBehaviour {
public void fly(){
System.out.println("我會飛...");
}
}
//不會飛
public class FlyNoWay implements FlyBehaviour {
public void fly(){
//我不會飛...
}
}
現在叫也有三種行爲:呱呱叫和吱吱叫和不會叫
//呱呱叫
public class Quack implements QuackBehaviour {
public void quack(){
System.out.println("我會呱呱叫...");
}
}
//吱吱叫
public class Squack implements QuackBehaviour {
public void quack(){
System.out.println("我會吱吱叫...");
}
}
//不會叫
public class MuteQuack implements QuackBehaviour {
public void quack(){
//我不會叫...
}
}
這樣寫的好處就在於,使用飛行行爲時只需指定會飛行,不需綁定具體飛行的動作,彈性空間較大.而且此處的面向接口編程,並不是狹義上指Java中的接口,而是指超類型,可以是接口也可以是抽象類.
那麼如何將行爲和Duck類組合到一起呢?
將行爲轉爲屬性
即將飛行和叫的行轉爲鴨子的一個變量
public abstract class Duck {
//鴨子不處理飛的行爲,將飛的行爲委託給FlyBehaviour接口
FlyBehaviour flyBehaviour;
//鴨子不處理叫的行爲,將飛的行爲委託給QucakBehaviour接口
QucakBehaviour quackBehaviour;
public void performFly(){
flyBehaviour.fly();
}
public void performQuack(){
quackBehaviour.quack();
}
public void swim(){
System.out.println("我會游泳...");
}
public abstract void display();
}
再來看看綠頭鴨,
public class MallardDuck extends Duck {
public MallardDuck(){
flyBehaviour = new FlyWithWings();
quackBehaviour = new Quack();
}
public class void display(){
System.out.println("我是綠頭鴨...");
}
}
現在測試一下:
public class Client{
public static void main(String[] args){
Duck duck = new MallardDuck();
duck.display();
duck.performFly();
duck.performQuack();
duck.swim();
}
}
執行後結果如下:
我是綠頭鴨...
我會飛...
我會呱呱叫...
我會游泳...
如何實現動態改變鴨子的行爲呢?修改Duck類如下:
public abstract class Duck {
//鴨子不處理飛的行爲,將飛的行爲委託給FlyBehaviour接口
FlyBehaviour flyBehaviour;
//鴨子不處理叫的行爲,將飛的行爲委託給QucakBehaviour接口
QucakBehaviour quackBehaviour;
public void setFlyBehaviour(FlyBehaviour flyBehaviour){
this.flyBehaviour = flyBehaviour;
}
public void setQucakBehaviour(QucakBehaviour quackBehaviour){
this.quackBehaviour = quackBehaviour;
}
public void performFly(){
flyBehaviour.fly();
}
public void performQuack(){
quackBehaviour.quack();
}
public void swim(){
System.out.println("我會游泳...");
}
public abstract void display();
}
現在構建一個模型鴨ModelDuck
public class ModelDuck extends Duck{
public ModelDuck(){
flyBehaviour = new FlyNoWay(); //一開始不會飛
quackBehaviour = new Quack();
}
public void display(){
System.out.println("我是模型鴨...");
}
}
新建一個新的飛行行爲:FlyRocketPowered
public class FlyRocketPowered implements FlyBehaviour{
public void fly(){
System.out.println("我能像火箭一樣飛...");
}
}
現在測試一下動態改變飛行行爲:
public class Client{
public static void main(String[] args){
Duck duck = new ModelDuck();
duck.display();
duck.performFly();
duck.setFlyBehaviour(new FlyRocketPowered());
duck.performFly();
}
}
測試結果:
我是模型鴨...
我會向火箭一樣飛...
這樣就實現了行爲與類分開,及變化的部分與不變化的部分分開了.
小總結
變化的部分
飛行的行爲和叫的行爲
不變的部分
鴨子會有用,擁有飛行和叫的行爲.
總結
封裝變化和麪向接口編程能讓代碼有很大的彈性,在代碼不變或者很小的改變的情況下滿足需求的變化,也易於維護.