使用組合的設計模式 | 追女孩要用的遠端代理模式

唐子玄發表於2019-06-03

這是設計模式系列的第三篇,系列文章目錄如下:

  1. 一句話總結殊途同歸的設計模式:工廠模式=?策略模式=?模版方法模式

  2. 使用組合的設計模式 —— 美顏相機中的裝飾者模式

  3. 使用組合的設計模式 —— 追女孩要用的遠端代理模式

  4. 用設計模式去掉沒必要的狀態變數 —— 狀態模式

上一篇講了一個使用組合的設計模式:裝飾者模式。它通過繼承複用了型別,通過組合複用了行為,最終達到擴充套件類功能的目的。

這一篇的代理模式也運用了組合的實現方法,它和裝飾者模式非常像,比較它們之間微妙的差別非常有意思。

幹嘛要代理?

代理就是幫你做事情的物件,為啥要委託它幫你做?因為做這件事太複雜,有一些你不需要了解的細節,所以將它委託給一個專門的物件來處理。

就好比現實生活中的簽證代理,各國辦簽證的需要的材料和流程不盡相同,有一些及其複雜。所以委託給瞭解這些細節的簽證代理幫我們處理(畢竟還有一大推bug等著我們)。

在實際程式設計中,複雜的事情可能有這麼幾種:遠端物件訪問(遠端代理)、建立昂貴物件(虛擬代理)、快取昂貴物件(快取代理)、限制物件的訪問(保護代理)等等。

本地 & 遠端

java 中,遠端和本地劃分的標準是:“它們是否執行在同一個記憶體堆中”

  • 一臺計算機上的應用通過網路呼叫另一臺計算機應用的方法叫做遠端呼叫,因為兩個應用程式執行在不同計算機的記憶體堆中。
  • Android 系統中,每個應用執行在各自的程式中,每個程式有獨立的虛擬機器,所以它們執行在同一臺計算機記憶體的不同堆中,跨程式的呼叫也稱為遠端呼叫。

遠端呼叫 & 遠端代理

遠端呼叫比本地呼叫複雜,因為需要處理本地和遠端的通訊(網路或跨程式呼叫)。

呼叫的發起者其實沒必要了解這些細節,它最好只是簡單地發起呼叫然後拿到想要的結果。所以將這些複雜的事情交給代理來做。(當然也可以將發起遠端呼叫的細節和呼叫發起的業務邏輯寫在一起,程式導向的程式碼就是這樣做的)

就以 Android 中的跨程式通訊為例:發起呼叫的應用稱為客戶端,響應呼叫的應用稱為服務端。服務以介面的形式定義在一個字尾為aidl的檔案中:

//以下是IMessage.aidl檔案的內容
package test.taylor.com.taylorcode;
interface IMessage {
    //系統自己生成的介面
    void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat,double aDouble, String aString);
    //這是我們定義的服務介面
    int getMessageType(int index) ;
}
複製程式碼

系統會自動為IMessage.aidl檔案生成對應的IMessage.java檔案:

public interface IMessage extends android.os.IInterface {
    //樁
    public static abstract class Stub extends android.os.Binder implements test.taylor.com.taylorcode.IMessage {
        public Stub() {
            this.attachInterface(this, DESCRIPTOR);
        }

        //客戶端呼叫這個介面獲取服務
        public static test.taylor.com.taylorcode.IMessage asInterface(android.os.IBinder obj) {
            //建立代理物件(注入遠端物件obj)
            return new test.taylor.com.taylorcode.IMessage.Stub.Proxy(obj);
        }
        
        //代理
        private static class Proxy implements test.taylor.com.taylorcode.IMessage {
            //通過組合持有遠端物件
            private android.os.IBinder mRemote;
            //注入遠端物件
            Proxy(android.os.IBinder remote) {
                mRemote = remote;
            }
            
            //代理物件對服務介面的實現
            @Override
            public int getMessageType(int index) throws android.os.RemoteException {
                android.os.Parcel _data = android.os.Parcel.obtain();
                android.os.Parcel _reply = android.os.Parcel.obtain();
                int _result;
                try {
                    //包裝呼叫引數
                    _data.writeInterfaceToken(DESCRIPTOR);
                    _data.writeInt(index);
                    //發起遠端呼叫(通過一些natvie層方法最終會呼叫服務端實現的stub中的方法)
                    mRemote.transact(Stub.TRANSACTION_getMessageType, _data, _reply, 0);
                    _reply.readException();
                    _result = _reply.readInt();
                } finally {
                    _reply.recycle();
                    _data.recycle();
                }
                return _result;
            }
        }
    }
}
複製程式碼

(為了聚焦在代理這個概念上,程式碼省略了大量無關細節。)

系統自動生成了兩個跨程式通訊關鍵類:Stub樁Proxy代理。它們是 Android 跨程式通訊中成對出現的概念。是服務端對服務介面的實現,代理是客戶端對於樁的代理。 暈了。。為啥要整出這麼多概念,搞這麼複雜?

其實是為了簡化跨程式通訊的程式碼,將跨程式通訊的細節封裝在代理中,客戶端可以直接呼叫代理類的方法(代理和客戶端處於同一記憶體堆,所以也稱為遠端的本地代理),由代理髮起跨程式呼叫並將結果返回給客戶端。代理扮演著遮蔽複雜跨程式通訊細節的作用,讓客戶端以為自己直接呼叫了遠端方法。

樁和代理擁有相同的型別,它們都實現了服務介面IMessage,但樁是抽象的,具體的實現會放在服務端。服務端通常會在 Android 系統元件 Service 中實現樁:

public class RemoteServer extends Service {
    public static final int MESSAGE_TYPE_TEXT = 1;
    public static final int MESSAGE_TYPE_SOUND = 2;
    
    //實現樁
    private IMessage.Stub binder = new IMessage.Stub() {
        @Override
        public void basicTypes(int anInt, long aLong, boolean aBoolean, float aFloat, double aDouble, String aString) throws RemoteException {}

        //定義服務內容
        @Override
        public int getMessageType(int index) throws RemoteException {
            return index % 2 == 0 ? MESSAGE_TYPE_SOUND : MESSAGE_TYPE_TEXT;
        }
    };

    //將服務例項返回給客戶端
    @Override
    public IBinder onBind(Intent intent) {
        return binder;
    }
}
複製程式碼

客戶端通過繫結服務來獲取服務例項:

IMessage iMessage;
Intent intent = new Intent(this, RemoteServer.class);
ServiceConnection serviceConnection = new ServiceConnection() {
    @Override
    public void onServiceConnected(ComponentName componentName, IBinder iBinder) {
        //將服務例項(樁)傳遞給asInterface(),該方法會建立本地代理並將樁注入
        iMessage = IMessage.Stub.asInterface(iBinder);
    }

    @Override
    public void onServiceDisconnected(ComponentName componentName) {
        iMessage = null;
    }
};
//繫結服務
this.bindService(intent, serviceConnection, BIND_AUTO_CREATE);
複製程式碼

當繫結服務成功後,onServiceConnected()會被回撥,本地代理會被建立,然後客戶端就可以通過iMessage.getMessageType()請求遠端服務了。

遠端代理模式 vs 裝飾者模式

遠端代理運用了和裝飾者模式一摸一樣的實現方式,將它們倆的描述放在一起會顯得很有趣:

  • 裝飾者和被裝飾者具有相同的型別,裝飾者通過組合持有被裝飾者
  • 代理和被代理者具有相同的型別,代理通過組合持有被代理者

頭大。。。既然一樣為啥還要區分成兩種模式?但如果結合它們的意圖進行比較就能發現細微的差別:

  • 裝飾者模式通過 繼承 + 組合 的方式,在複用原有型別和行為的基礎上為其擴充套件功能
  • 遠端代理模式通過 繼承 + 組合 的方式,實現對代理物件的訪問控制

如果硬要用裝飾者模式的臺詞來形容代理模式也沒有什麼不可以:“代理通過裝飾被代理者,為其擴充套件功能,使得它能夠被遠端物件訪問”。這句話完全說得通,但是有點怪怪的。

如果試著新增一點擬人色彩,遠端代理模式和裝飾者模式就變得很好區分!

  • 使用代理模式就好像在說:“我喜歡你,但是我夠不到你。所以我需要代理(可能是你的閨蜜)”。
  • 使用裝飾者模式就好像在說:“我喜歡和你相同型別的另一個人。所以我需要把你裝飾成它。”(好了,你找不到女朋友了)

後續

本打算用 1篇文章來總結那些使用組合的設計模式,其中包括裝飾者模式、代理模式、介面卡模式、外觀模式、狀態模式。

千千沒想到寫著寫著就變成了n 篇。。。。

萬萬沒有想到,這一篇代理模式寫著寫著就發現,如果把所有應用場景講完,篇幅就太長了,無奈之下只能在此留白。代理模式的變種特別多,它們之間在實現方式上和意圖上有微妙的差別,待下回分析。

相關文章