万物之中, 希望至美.

设计模式之代理模式

2018.10.22

代理模式是通过一层间接保护层实现更安全的接口访问,例如:访问敏感信息——在允许用户访问敏感信息之前,我们希望确保用户具备足够的权限。

有四种常用的代理模式:

  • 远程代理:实际存在于不同地址空间(例如,某个网络服务器)的对象在本地的代理者。使得访问远程对象就像访问本地一样,隐藏了复杂性,如:ORM。
  • 虚拟代理:用来实现延迟访问,比如一些需要复杂计算的对象,Python 里可以实现 lazy_property,改善性能。
  • 保护/防护代理:用于控制敏感对象的访问。
  • 智能(引用)代理:在对象被访问时执行额外的动作。例如引用计数和线程安全检查。

延迟初始化

我们先创建一个LazyProperty类,用作一个装饰器。当它修饰某个特性时,LazyProperty 惰性地(首次使用时)加载特性,而不是立即进行。

class LazyProperty:
    def __init__(self, method):
        self.method = method
        self.method_name = method.__name__

    def __get__(self, instance, owner):
        if not instance:
            return None
        value = self.method(instance)
        setattr(owner, self.method_name, value)
        return value


class Test:
    def __init__(self):
        self.x = 'foo'
        self.y = 'bar'
        self._resource = None

    @LazyProperty
    def resource(self):
        # 构造函数里没有初始化,第一次访问才会被调用
        print('initializing self._resource which is: {}'.format(self._resource))
        self._resource = tuple(range(5))  # 模拟一次耗时计算
        return self._resource


def main():
    t = Test()
    print(t.x)
    print(t.y)
    # 访问LazyProperty, resource里的print语句只执行一次,实现了延迟加载和一次执行
    print(t.resource)
    print(t.resource)


if __name__ == '__main__':
    main()

LazyProperty类实际上是一个描述符。 描述符(descriptor)是Python中重写类属性访问方法(__get__()__set__()__delete__())的默认行为要使用的一种推荐机制。 Test类演示了我们可以如何使用LazyProperty类。其中有三个属性, x、 y 和 _resource。我们想懒加载 _resource 变量,因此将其初始化为None。

在OOP中有两种基本的、不同类型的懒加载,如下所示:

  • 在实例级:这意味着会一个对象的特性进行懒初始化,但该特性有一个对象作用域。同一个类的每个实例(对象)都有自己的(不同的)特性副本。
  • 在类级或模块级:在这种情况下,我们不希望每个实例都有一个不同的特性副本,而是所有实例共享同一个特性,而特性是懒初始化的。

保护代理

这里实现一个简单的保护代理来查看和添加用户,该服务提供以下两个选项:

  • 查看用户列表:这一操作不要求特殊权限。
  • 添加新用户:这一操作要求客户端提供一个特殊的密码。
class SensitiveInfo:
    def __init__(self):
        self.users = ['nick', 'tom', 'ben', 'mike']

    def read(self):
        result = 'There are {} users: {}'
        print(result.format(len(self.users), ' '.join(self.users)))

    def add(self, user):
        self.users.append(user)
        print('Added user {}'.format(user))


class Info:
    def __init__(self):
        self.protected = SensitiveInfo()
        self.secret = '0xdeadbeef'

    def read(self):
        self.protected.read()

    def add(self, user):
        sec = input('what is the secret? ')
        self.protected.add(user) if sec == self.secret else print("That's wrong!")


def main():
    info = Info()
    while True:
        print('1. read list |==| 2. add user |==| 3. quit')
        key = input('choose option: ')
        if key == '1':
            info.read()
        elif key == '2':
            name = input('choose username: ')
            info.add(name)
        elif key == '3':
            exit()
        else:
            print('unknown option: {}'.format(key))


if __name__ == '__main__':
    main()

该示例有几个重大的安全缺陷:

  1. 没有什么能阻止客户端代码通过直接创建一个 SensitiveInfo 实例来绕过应用的安全设置,疑使用 abc 模块来禁止直接实例化 SensitiveInfo。
  2. 密钥直接写死在代码里,应该用安全性较高密钥写到配置或者环境变量里。

这里使用抽象基类来修复第一个问题,只需要修改类代码而不用修改 main() 函数里面的使用代码:

from abc import ABCMeta, abstractmethod


class SensitiveInfo(metaclass=ABCMeta):
    def __init__(self):
        self.users = ['nick', 'tom', 'ben', 'mike']

    @abstractmethod
    def read(self):
        pass

    @abstractmethod
    def add(self, user):
        pass


class Info(SensitiveInfo):
    '''protection proxy to SensitiveInfo'''
    def __init__(self):
        # self.protected = SensitiveInfo()
        super().__init__()
        self.secret = '0xdeadbeef'    # 为了方便示例这里直接写死在代码里

    def read(self):
        print('There are {} users: {}'.format(len(self.users), ' '.join(self.users)))

    def add(self, user):
        """ 给add操作加上密钥验证,保护add操作"""
        sec = input('what is the secret? ')
        self.users.append(user) if sec == self.secret else print("That's wrong!")
comments powered by Disqus