Python设计模式之外观模式(7)

外观模式(Facade Pattern):简化复杂对象的访问。

隐藏系统的复杂性,并向客户端提供了一个客户端可以访问系统的接口。这种类型的设计模式属于结构型模式,它向现有的系统添加一个接口,来隐藏系统的复杂性。

这种模式涉及到一个单一的类,该类提供了客户端请求的简化方法和对现有系统类方法的委托调用。

1 介绍

系统会随着演化变得非常复杂,最终形成大量的(并且有时是令人迷惑的)类和交互,这种情况并不少见。许多情况下,我们并不想把这种复杂性暴露给客户端。外观设计模式有助于隐藏系统的内部复杂性,并通过一个简化的接口向客户端暴露必要的部分。

本质上,外观(Facade)是在已有复杂系统之上实现的一个抽象层。

意图:为子系统中的一组接口提供一个一致的界面,外观模式定义了一个高层接口,这个接口使得这一子系统更加容易使用。

主要解决:降低访问复杂系统的内部子系统时的复杂度,简化客户端与之的接口。

何时使用: 

  • 客户端不需要知道系统内部的复杂联系,整个系统只需提供一个"接待员"即可。
  • 定义系统的入口。

如何解决:客户端不与系统耦合,外观类与系统耦合。

关键代码:在客户端和复杂系统之间再加一层,这一层将调用顺序、依赖关系等处理好。

优点: 

  • 减少系统相互依赖。
  • 提高灵活性。
  • 提高了安全性。

缺点:不符合开闭原则,如果要改东西很麻烦,继承重写都不合适。 

注意事项:在层次化结构中,可以使用外观模式定义系统中每一层的入口。

 

2 使用场景

使用外观模式的最常见理由是为一个复杂系统提供单个简单的入口点。引入外观之后,客户端代码通过简单地调用一个方法/函数就能使用一个系统。同时,内部系统并不会丢失任何功能,外观只是封装了内部系统。

不把系统的内部功能暴露给客户端代码有一个额外的好处:我们可以改变系统内部,但客户端代码不用关心这个改变,也不会受这个改变的影响。客户端代码不需要进行任何改变。

主要用于以下场景:

  • 为复杂的模块或子系统提供外界访问的模块。
  • 子系统相对独立。
  • 预防低水平人员带来的风险。

 

3 使用步骤

外观模式包含如下角色: Facade: 外观角色 SubSystem:子系统角色

使用步骤具体也是包含两个步骤:

步骤一:创建子系统角色

步骤二:创建外观角色,该角色为子系统中的一组接口提供一个一致界面

案例:

假设有一组火警报警系统,由三个子元件构成:一个警报器,一个喷水器,一个自动拨打电话的装置

"""
步骤1:定义子系统,即三个子元件:一个警报器,一个喷水器,一个自动拨打电话的装置
"""
class AlarmSender(object):
    def run(self, msg):
        return "产生了高温告警: {}".format(msg)

class WaterSprinker(object):
    def run(self):
        return "洒水降温"

class Dialer(object):
    def run(self, name, phone):
        return "拨打值班人:{} 电话: {}".format(name, phone)

"""
步骤二:定义外观类,封装子系统的操作
"""
class EmergencyFacade(object):
    def __init__(self):
        self.alarm = AlarmSender()
        self.water = WaterSprinker()
        self.dialer = Dialer()

    def run(self, name, phone, msg):
        data = []
        data.append(self.alarm.run(msg))
        data.append(self.water.run())
        data.append(self.dialer.run(name, phone))
        return data

if __name__ == "__main__":
    name = "Bruce"
    phone = "210-123456"
    msg = "高温告警,请立即处理"
    emergency = EmergencyFacade()
    resp = emergency.run(name, phone, msg)
    print(resp)

代码输出:

['产生了高温告警: 高温告警,请立即处理', '洒水降温', '拨打值班人:Bruce 电话: 210-123456']

 

4 代码实践

案例1:假设我们想使用多服务进程方式实现一个操作系统
我们从Server接口(这里的“接口”并非指语法上的interface,而是指一个不能直接实例化的类)开始实现。
使用一个Enum类型变量来描述一个服务进程的不同状态,使用abc模块来禁止对Server接口直接进行初始化,
并强制子类实现关键的boot()和kill() 方法。
这里假设每个服务进程的启动、关闭及重启都相应地需要不同的动作
from enum import Enum
from abc import ABCMeta, abstractmethod
State = Enum("State", "new running sleeping restart zombie")

"""
步骤1:定义子系统抽象类。
主要用于约束子系统代码必须实现相应的方法函数,用于规范子系统的设计。
"""
class Server(metaclass=ABCMeta):

    @abstractmethod
    def __init__(self):
        pass

    @abstractmethod
    def boot(self):
        pass

    @abstractmethod
    def kill(self, restart=True):
        pass

"""
步骤2:实现子系统功能。
"""
class ProcessServer(Server):
    def __init__(self):
        self.name = "ProcessServer"
        self.state = State.new

    def boot(self):
        print("boot the system: {}".format(self.name))
        self.state = State.running

    def kill(self, restart=True):
        print("kill the system: {} with restart: {}".format(self.name, restart))
        self.state = State.restart if restart else State.zombie

    def create_process(self, p_name, pid):
        print("create process with name: {}, pid: {}".format(p_name, pid))

class FileServer(Server):
    def __init__(self):
        self.name = "FileServer"
        self.state = State.new

    def boot(self):
        print("boot the system: {}".format(self.name))
        self.state = State.running

    def kill(self, restart=True):
        print("kill the system: {} with restart: {}".format(self.name, restart))
        self.state = State.restart if restart else State.zombie

    def create_file(self, file_name):
        print("create file with name: {}".format(file_name))

"""
步骤3:定义外观类,封装子系统。
init()中创建所有需要的服务进程实例。start()方法是系统的入口点,供客户端代码使用。
如果需要,可以添加更多的包装方法作为服务的访问点,比如包装方法create_file()和create_process()。
从客户端的角度来看,所有服务都是由OperatingSystem类提供的。客户端并不应该被不必要的细节所干扰,比如,服务进程的存在和每个服务进程的责任。
"""
class OperatingSystem(object):
    def __init__(self):
        self.process_s = ProcessServer()
        self.file_s = FileServer()

    def start(self):
        for se in (self.process_s, self.file_s):
            se.boot()

    def start_process(self, p_name, pid):
        self.process_s.create_process(p_name,pid)

    def start_file(self, f_name):
        self.file_s.create_file(f_name)

"""
客户端调用代码
"""
class Client(object):
    def run(self, f_name, p_name, pid):
        os = OperatingSystem()
        os.start()
        os.start_process(p_name, pid)
        os.start_file(f_name)

if __name__ == "__main__":
    client = Client()
    client.run("/data/message.log", "system_daemon", 1)

代码输出:

boot the system: ProcessServer
boot the system: FileServer
create process with name: system_daemon, pid: 1
create file with name: /data/message.log

 

5 软件示例

django-oscar-datacash模块是Django的一个第三方组件,用于集成DataCash支付网关。该组件有一个Gateway类,提供对多种DataCash API的细粒度访问。在那之上,它也包含一个Facade类,提供粗粒度API(提供给那些不需要处理细节的人),并针对审计目的提供保存事务的能力(请参考网页[t.cn/RqrlgCG])。

Caliendo是一个用于模拟Python API的的接口,它包含一个facade模块。该模块使用外观模式来完成许多不同但有用的事情(比如缓存方法),并基于传给顶层Facade方法的输入对象决定返回什么方法(请参考网页[t.cn/RqrlkiU])。

代码片段:caliendo/facade.py


#外观模式,简单入口
def Facade( some_instance=None, exclusion_list=[], cls=None, args=tuple(), kwargs={}  ):
    """
    Top-level interface to the Facade functionality. Determines what to return when passed arbitrary objects.

    :param mixed some_instance: Anything.
    :param list exclusion_list: The list of types NOT to wrap
    :param class cls: The class definition for the object being mocked
    :param tuple args: The arguments for the class definition to return the desired instance
    :param dict kwargs: The keywork arguments for the class definition to return the desired instance

    :rtype instance: Either the instance passed or an instance of the Wrapper wrapping the instance passed.
    """
    if not USE_CALIENDO or should_exclude( some_instance, exclusion_list ):
        if not util.is_primitive(some_instance):
            # Provide dummy methods to prevent errors in implementations dependent
            # on the Wrapper interface
            some_instance.wrapper__unwrap = lambda : None
            some_instance.wrapper__delete_last_cached = lambda : None
        return some_instance # Just give it back.
    else:
        if util.is_primitive(some_instance) and not cls:
            return some_instance
        return Wrapper(o=some_instance, exclusion_list=list(exclusion_list), cls=cls, args=args, kwargs=kwargs )

#外观模式类,隐藏内部复杂性,对外提供简单入口Wrapper()
class Wrapper( dict ):
    """
    The Caliendo facade. Extends the Python object. Pass the initializer an object
    and the Facade will wrap all the public methods. Built-in methods
    (__somemethod__) and private methods (__somemethod) will not be copied. The
    Facade actually maintains a reference to the original object's methods so the
    state of that object is manipulated transparently as the Facade methods are
    called.
    """
    last_cached = None
    __exclusion_list = [ ]

    def wrapper__ignore(self, type_):
        """
        Selectively ignore certain types when wrapping attributes.

        :param class type: The class/type definition to ignore.

        :rtype list(type): The current list of ignored types
        """
        if type_ not in self.__exclusion_list:
            self.__exclusion_list.append(type_)
        return self.__exclusion_list

    def wrapper__unignore(self, type_):
        """
        Stop selectively ignoring certain types when wrapping attributes.

        :param class type: The class/type definition to stop ignoring.

        :rtype list(type): The current list of ignored types
        """
        if type_ in self.__exclusion_list:
            self.__exclusion_list.remove( type_ )
        return self.__exclusion_list

    def wrapper__delete_last_cached(self):
        """
        Deletes the last object that was cached by this instance of caliendo's Facade
        """
        return delete_io( self.last_cached )

    def wrapper__unwrap(self):
        """
        Returns the original object passed to the initializer for Wrapper

        :rtype mixed:
        """
        return self['__original_object']

    def __get_hash(self, args, trace_string, kwargs ):
        """
        Returns the hash from a trace string, args, and kwargs

        :param tuple args: The positional arguments to the function call
        :param str trace_string: The serialized stack trace for the function call
        :param dict kwargs: The keyword arguments to the function call

        :rtype str: The sha1 hashed result of the inputs plus a thuper-sthecial counter incremented in the local context of the call

        """
        return get_hash(args, trace_string, kwargs)


    def __cache( self, method_name, *args, **kwargs ):
        """
        Store a call descriptor

        """
        trace_string           = util.get_stack(method_name)
        call_hash              = self.__get_hash(args, trace_string, kwargs)
        cd                     = call_descriptor.fetch( call_hash )
        if not cd:
            c  = self.__store__['callables'][method_name]
            if hasattr( c, '__class__' ) and c.__class__ == LazyBones:
                c = c.init()
            returnval = c(*args, **kwargs)
            cd = call_descriptor.CallDescriptor( hash      = call_hash,
                                                 stack     = trace_string,
                                                 method    = method_name,
                                                 returnval = returnval,
                                                 args      = args,
                                                 kwargs    = kwargs )
            cd.save()
            if not call_hash:
                raise Exception("CALL HASH IS NONE")

            util.last_hash = call_hash
            self.last_cached = call_hash
        else:
            returnval = cd.returnval

        if inspect.isclass(returnval):
            returnval = LazyBones( c, args, kwargs )

        return returnval

    def __wrap( self, method_name ):
        """
        This method actually does the wrapping. When it's given a method to copy it
        returns that method with facilities to log the call so it can be repeated.

        :param str method_name: The name of the method precisely as it's called on
        the object to wrap.

        :rtype lambda function:
        """
        return lambda *args, **kwargs: Facade( self.__cache( method_name, *args, **kwargs ), list(self.__exclusion_list) )

    def __getattr__( self, key ):
        if key not in self.__store__: # Attempt to lazy load the method (assuming __getattr__ is set on the incoming object)
            try:
                oo = self['__original_object']
                if hasattr( oo, '__class__' ) and oo.__class__ == LazyBones:
                    oo = oo.init()
                val = eval( "oo." + key )
                self.__store_any(oo, key, val)
            except:
                raise Exception( "Key, " + str( key ) + " has not been set in the facade and failed to lazy load! Method is undefined." )

        val = self.__store__[key]

        if val and type(val) == tuple and val[0] == 'attr':
            return Facade(val[1])

        return self.__store__[ key ]

    def wrapper__get_store(self):
        """
        Returns the method/attribute store of the wrapper

        """
        return self.__store__

    def __store_callable(self, o, method_name, member):
        """
        Stores a callable member to the private __store__

        :param mixed o: Any callable (function or method)
        :param str method_name: The name of the attribute
        :param mixed member: A reference to the member

        """
        self.__store__['callables'][method_name] = eval( "o." + method_name )
        self.__store__['callables'][method_name[0].lower() + method_name[1:]] = eval( "o." + method_name )
        ret_val = self.__wrap( method_name )
        self.__store__[ method_name ] = ret_val
        self.__store__[ method_name[0].lower() + method_name[1:] ] = ret_val

    def __store_class(self, o, method_name, member):
        """
        Stores a class to the private __store__

        :param class o: The class to store
        :param str method_name: The name of the method
        :param class member: The actual class definition
        """
        self.__store__['callables'][method_name] = eval( "o." + method_name )
        self.__store__['callables'][method_name[0].lower() + method_name[1:]] = eval( "o." + method_name )
        ret_val = self.__wrap( method_name )
        self.__store__[ method_name ] = ret_val
        self.__store__[ method_name[0].lower() + method_name[1:] ] = ret_val

    def __store_nonprimitive(self, o, method_name, member):
        """
        Stores any 'non-primitive'. A primitive is in ( float, long, str, int, dict, list, unicode, tuple, set, frozenset, datetime.datetime, datetime.timedelta )

        :param mixed o: The non-primitive to store
        :param str method_name: The name of the attribute
        :param mixed member: The reference to the non-primitive

        """
        self.__store__[ method_name ] = ( 'attr', member )
        self.__store__[ method_name[0].lower() + method_name[1:] ] = ( 'attr', member )

    def __store_other(self, o, method_name, member):
        """
        Stores a reference to an attribute on o

        :param mixed o: Some object
        :param str method_name: The name of the attribute
        :param mixed member: The attribute

        """
        self.__store__[ method_name ] = eval( "o." + method_name )
        self.__store__[ method_name[0].lower() + method_name[1:] ] = eval( "o." + method_name )

    def __save_reference(self, o, cls, args, kwargs):
        """
        Saves a reference to the original object Facade is passed. This will either
        be the object itself or a LazyBones instance for lazy-loading later

        :param mixed o: The original object
        :param class cls: The class definition for the original object
        :param tuple args: The positional arguments to the original object
        :param dict kwargs: The keyword arguments to the original object

        """
        if not o and cls:
            self['__original_object'] = LazyBones( cls, args, kwargs )
        else:
            while hasattr( o, '__class__' ) and o.__class__ == Wrapper:
                o = o.wrapper__unwrap()
            self['__original_object'] = o

    def __store_any(self, o, method_name, member):
        """
        Determines type of member and stores it accordingly

        :param mixed o: Any parent object
        :param str method_name: The name of the method or attribuet
        :param mixed member: Any child object

        """
        if should_exclude( eval( "o." + method_name ), self.__exclusion_list ):
            self.__store__[ method_name ] = eval( "o." + method_name )
            return

        if hasattr( member, '__call__' ):
            self.__store_callable( o, method_name, member )
        elif inspect.isclass( member ):
            self.__store_class( o, method_name, member ) # Default ot lazy-loading classes here.
        elif not util.is_primitive( member ):
            self.__store_nonprimitive( o, method_name, member )
        else:
            self.__store_other( o, method_name, member )

    def __init__( self, o=None, exclusion_list=[], cls=None, args=tuple(), kwargs={} ):
        """
        The init method for the Wrapper class.

        :param mixed o: Some object to wrap.
        :param list exclusion_list: The list of types NOT to wrap
        :param class cls: The class definition for the object being mocked
        :param tuple args: The arguments for the class definition to return the desired instance
        :param dict kwargs: The keywork arguments for the class definition to return the desired instance

        """
        self.__store__            = {'callables': {}}
        self.__class              = cls
        self.__args               = args
        self.__kwargs             = kwargs
        self.__exclusion_list     = exclusion_list

        self.__save_reference(o, cls, args, kwargs)

        for method_name, member in inspect.getmembers(o):
            self.__store_any(o, method_name, member)

        try: # try-except because o is mixed type
            if o.wrapper__get_store: # For wrapping facades in a chain.
                store = o.wrapper__get_store()
                for key, val in store.items():
                    if key == 'callables':
                        self.__store__[key].update( val )
                    self.__store__[key] = val
        except:
            pass

#将一系列复杂过程封装在一起,对外提供简单的调用入口
def cache(handle=lambda *args, **kwargs: None, args=UNDEFINED, kwargs=UNDEFINED, ignore=UNDEFINED, call_stack=UNDEFINED, callback=UNDEFINED, subsequent_rvalue=UNDEFINED):
    """
    Store a call descriptor

    :param lambda handle: Any callable will work here. The method to cache.
    :param tuple args: The arguments to the method.
    :param dict kwargs: The keyword arguments to the method.
    :param tuple(list(int), list(str)) ignore: A tuple of arguments to ignore. The first element should be a list of positional arguments. The second should be a list of keys for keyword arguments.
    :param caliendo.hooks.CallStack call_stack: The stack of calls thus far for this patch.
    :param function callback: The callback function to execute each time there is a cache hit for 'handle' (actually mechanism is more complicated, but this is what it boils down to)
    :param mixed subsequent_rvalue: If passed; this will be the return value each time this method is run regardless of what is returned when it is initially cached. Caching for this method will be skipped. This is useful when the method returns something unpickleable but we still need to stub it out.

    :returns: The value of handle(*args, **kwargs)
    """
    if args == UNDEFINED:
        args = tuple()
    if kwargs == UNDEFINED:
        kwargs = {}
    if not USE_CALIENDO:
        return handle(*args, **kwargs)

     
    filtered_args = ignore.filter_args(args) if ignore is not UNDEFINED else args
    filtered_kwargs = ignore.filter_kwargs(kwargs) if ignore is not UNDEFINED else args

    trace_string      = util.get_stack(handle.__name__)
    call_hash         = get_hash(filtered_args, trace_string, filtered_kwargs, ignore)
    cd                = call_descriptor.fetch(call_hash)
    modify_or_replace = 'no'

    util.set_current_hash(call_hash)

    if config.CALIENDO_PROMPT:
        display_name = ("(test %s): " % caliendo.util.current_test) if caliendo.util.current_test else ''
        if hasattr(handle, '__module__') and hasattr(handle, '__name__'):
            display_name += "%s.%s" % (handle.__module__, handle.__name__)
        else:
            display_name += handle

        if cd:
            modify_or_replace = prompt.should_modify_or_replace_cached(display_name)

    if not cd or modify_or_replace == 'replace':
        returnval = handle(*args, **kwargs)
    elif cd and modify_or_replace == 'modify':
        returnval = prompt.modify_cached_value(cd.returnval,
                                               calling_method=display_name,
                                               calling_test='')

    if cd and subsequent_rvalue != UNDEFINED:
        return subsequent_rvalue
    elif subsequent_rvalue != UNDEFINED:
        original_rvalue = returnval
        returnval = subsequent_rvalue

    if not cd or modify_or_replace != 'no':
        if isinstance(handle, types.MethodType):
            filtered_args = list(filtered_args)
            filtered_args[0] = util.serialize_item(filtered_args[0])
            filtered_args = tuple(filtered_args)

        cd = call_descriptor.CallDescriptor( hash      = call_hash,
                                             stack     = trace_string,
                                             method    = handle.__name__,
                                             returnval = returnval,
                                             args      = filtered_args,
                                             kwargs    = filtered_kwargs )

        cd.save()

    util.set_last_hash(cd.hash)

    if call_stack != UNDEFINED:
        call_stack.add(cd)
        if callback != UNDEFINED:
            call_stack.add_hook(Hook(call_descriptor_hash=cd.hash,
                                     callback=callback))

    if subsequent_rvalue == UNDEFINED:
        return cd.returnval
    else:
        return original_rvalue



参考文献:

https://www.jianshu.com/p/ef6f5616451a

https://searchcode.com/codesearch/view/74620473/

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值