[Python设计模式]代理模式(Proxy)


设计模式网上的版本各种各样,但是有两个问题:

1、因为设计模式最早的理论是基于JAVA,所以很多实现和讲解都以JAVA作为源码;

2、大量的代码都是依葫芦画瓢,基本上都比较鸡肋,在工程代码中没有参考价值。


所以决心在遇到实际问题的时候自己总结一下,以上算是给这一系列做个简单的序。


一、起因

最近因为工程代码中的历史遗留问题,让一些API用起来非常不顺手,跟同事出现了一些分歧,工程中大致代码是这样的:


# -*- coding: utf-8 -*-
import redis

class RedisClient(object):
    def __init__(self, host, port, password, db):
        self._client = redis.Redis(host=host, port=port, password=password, db=db)
        
    def _make_key(self, key):
        return 'prefix_{0}'.format(key)
        
    def get(self, key):
        return self._client.get(self._make_key(key))
    
    '''省略一堆需要封装self._client中的方法'''
    
mc = RedisClient()

代码逻辑非常简单,对redis库进行二次封装(即RedisClient类为Redis类的代理类),并在操作每个redis的key时,给key再加上prefix_的前缀。


同事认为这样的写法有两个好处:

1、能够隐藏redis库中的细节,外部调用的时候更加便捷

2、统一封装,修改简单,如果以后nosql数据库更换时,不需要去外部修改,只需要修改RedisClient类中的方法就可以满足需求


而作者却认为这样的想法有待商榷,首先GOF提出代理模式的目的(下一节会展开描述)并不涵盖对外部库的二次封装,其次以上所提及的两个优点其实均是伪命题,redis本身提供了种类丰富的数据结构,使用者几乎不可能在不理解的情况下调用函数,由于数据结构多,使得函数传参也变得多样,统一封装基本是痴人说梦,基于以上几个原因,想要换nosql而仅仅修改代理类,也是吃人说明。重中之重是,每当你需要用到Redis类中的方法时,你都需要手动向RedisClient中添加一个同名函数,平添很多工作量。这样的代理类,显然是弊大于利的。


二、代理模式

代理模式的目的:

1、远程代理:假如一个对象有同的地址空间,则需要一个代理类来提供局部代表

假如需要的数据在远端或者数据库中,使用代理类隐藏数据传输过程逻辑,直接获取数据本身

2、虚代理:延迟实例化代价很高的类

原著中以图片对象为例子,实例化图片对象的开销很大,代理类中只会预先存储一些实例化图片需要的参数,只有在确定要显示图片的时候,再根据代理类中的参数实例化图片类

3、保护代理:需要对访问对象进行权限控制,代理类作为权限控制层存在

这个比较好理解,就不再展开了

4、智能指引:取代指针

其实1、4在工程代码中是最常见的存在,代码中经常会出现诸如‘a.b.c.d’这样的引用,倘若这个指针要被反复使用,那就会出现多次,繁琐而又不好修改。


“反向”代理(智能指针):

因为上一段代码在工程中引用非常多,不能直接修改代码逻辑,而我们又希望不重新实例化redis对象的情况下调用使用redis,于是代码的管理层出现了这样的代理模式:

class UserManager(object):
    @property
    def redis_client(self):
        return mc._client
    
    def getUserInfo(self):
        return self.redis_client.hget('User', 1001)

以上代码其实就是目的4的体现,为了能够直接使用redis中的方法,我们在自己的类中设置了一个代理函数redis_client,为了避免函数调用打上@property标签,这样就可以像在getUserInfo中那样流畅的使用被代理对象的API了,这也是为什么标题上会加上“反向”的原因。


延伸-代理的结合:

# -*- coding: utf-8 -*-
import redis

class RedisClient(object):
    REDIS_CONFIG = {'host': '127.0.0.1', 'port': 6381, 
                    'password': 'password', 'db':0}
    def __init__(self):
        self._client = None 

    def __getattribute__(self, *args, **kwargs):
        '''优先使用代理类中的函数,如果没有则使用被代理类的函数'''
        if self._client is None:
            '''这里假设Redis是一个开销比较大的类,预先储存配置,
            需要的时候再实例化常驻内存'''
            self._client = redis.Redis(**self.REDIS_CONFIG)
        try:
            return object.__getattribute__(self, *args, **kwargs)
        except AttributeError:
            return object.__getattribute__(self._client, *args, **kwargs)
        
    def get(self, key):
        return self._client.get(self._make_key(key))

mc = RedisClient()

class UserManager(object):  
    def getUserInfo(self):
        return mc.hget('User', 1001)


示例中假设RedisClient是一个需要频繁实例化、但开销很大的类。这样去使用代理类,就免去了没有重写过的方法需要逐一去重写的麻烦,其次又使用了延后的代理运用,在第一次查找的时候才实例化redis连接。


注意:虽然相比第一节中的做法已经改进很多,但这样使用代理仍有一个问题,即当代码中大量使用被代理类的函数的时候,该函数就不适合再被代理,而需要通过在代理类中新增函数来实现,以免修改过多;但归根揭底就是,含有数量庞杂的API时,不适合使用代理模式。

三、其他

1、理论上工程代码中不应出现大量的代理类,一般仅在数据结构复杂、指针过多又需要反复使用的时候,或者做权限控制时才考虑使用
2、不应简单的将代理类理解为覆盖重写被代理类或者是被代理类的父级类,否则引入代码会导致代码复用性及易用性降低
3、因为Python的动态特性,代理的对象与被代理对象均可以是类、对象、函数或属性


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值