使用组合的设计模式 | 追女孩要用的远程代理模式

3,325 阅读7分钟

这是设计模式系列的第三篇,系列文章目录如下:

  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 篇。。。。

万万没有想到,这一篇代理模式写着写着就发现,如果把所有应用场景讲完,篇幅就太长了,无奈之下只能在此留白。代理模式的变种特别多,它们之间在实现方式上和意图上有微妙的差别,待下回分析。