语言特性与对设计的影响

187 阅读7分钟
原文链接: zhuanlan.zhihu.com

正文之前先插播一段广告,随着VLCP 1.3.0的即将发布,也终于有时间整理一下相关的文档了,预览版地址为 VLCP - New generation SDN framework ,还在更新中。

目前这个项目已经在现公司的docker环境中小规模投入实用,稳定运行了4个月左右。OpenvSwitch新版本有许多振奋人心的功能,包括conntrack的支持,DPDK的支持等,这些功能全部都通过OpenFlow以及其扩展对外提供,这意味着基于现有框架实现一个高性能的SDN网络、多租户环境、甚至高性能的负载均衡层都是完全有可能的。控制平面用快速开发、易于上手的Python,转发平面交给功能强大、优秀稳定、高性能的OpenvSwitch,未来的SDN网络可以充满想象力。

1.3.0版本的主要新功能包括:

  1. 三层(IP)网络的完整支持,通过创建Virtual Router,在虚拟机(容器)之间创建完全分布式无中心的三层转发服务,并可以通过边界网关与传统网络连通。
  2. 对硬件交换机OVSDB VTEP功能的支持。这是NSX推广的一种技术,将硬件交换机作为VXLAN的终结点,通过硬件交换能力大大提高VXLAN的性能。从1.3版本开始,既可以使用以前的OpenvSwitch作为VXLAN端点的方案,也可以在硬件条件支持的情况下启用硬件交换机作为端点,而且对上层和配置完全透明,也可以在保持现有网络和端点的情况下随时进行从软件到硬件的升级(或降级)

====================================================================

回到正题。今天有人回复了一个半年多之前撕PHP的回答,看到过去自己写的内容,不禁思绪万千。一个语言的特性,有人会觉得只是单纯的“语法糖”,有没有并不带来本质区别,其实并不是这样的,许多特性会对你软件的设计与演进带来巨大的帮助。

今天要举的例子是VLCP中对于模块管理的一段逻辑。VLCP中,许多组件可以以独立模块的方式进行加载,每个模块从代码上来说是Module的一个子类。模块有的时候需要其他的模块帮助才能正常运行,这叫做依赖关系,在VLCP中,模块在定义的时候用依赖关系注解来声明自己需要依赖哪些模块来运行,这个注解将依赖项列表写进类的一个成员变量depends里,我们知道注解在import的过程中随着类的创建一起执行,整理起来大致是这样一个顺序:

  1. import,生成依赖关系,存进类的depends属性
  2. 读取配置文件
  3. 根据配置文件进行依赖分析,排序
  4. 所有模块的__init__,按顺序进行
  5. 所有模块的load(),按顺序进行

实际环境中1和2的执行顺序是不确定的,因为import的执行跟实际代码有关,没有办法保证读取文件之前没有先进行某些import。 因此,import的过程中我们不能随意去使用配置。

----------小分隔-----------

这套机制平稳地执行了一年左右,某一天,我希望引入一个新的机制叫做ProxyModule。以前每个模块申明的依赖关系都是依赖一个实际存在的模块,但是现在我们有一些模块并不依赖于某个特定的模块,而是需要任意一个实现了特定接口的模块,这样我们就可以自己编写不同的模块实现相同的接口,然后让用户在配置文件中指定具体加载哪一个。这是一个典型的多态的需求,与Spring之类的框架中的接口注入有些类似,但使用起来更简单一些。

要怎么实现这个功能呢?一个很直接的想法,ProxyModule是Module的一个子类,它在运行时,动态通过读取配置,决定自己的目标模块,将目标模块设置为自己的依赖项(depends),然后将发往自己的调用转发到目标模块上。

现在问题就来了,我们前面说过,import的过程中,不能随意去使用配置。然而depends这个类的成员变量,是在import过程中生成的,这时候配置文件可能还没有读取,要怎么办呢?

第一个方法是放弃原有的depends成员变量的机制,在相应的位置加分支,特殊处理。对这个方案我是断然拒绝的,需要进行依赖分析的地方远远不止一处,模块的动态载入、卸载、重新加载过程都需要重新进行依赖分析,如果每个地方都要去加个if,不仅仅是美学上无法接受,代码维护上也要出大问题,以后如果又要增加新的机制呢?再加新分支?与其引入这个祸根,还不如最开始就不要这个功能。

这时候我们自然会联想起多态的机制,很简单,在派生类里面重载基类的接口就可以了。对于Java来说,我们可能会定义一个getDepend()的接口,然后在Module和ProxyModule中提供不同的实现;对于Python来说就更简单了,可以用@property注解,利用descriptor机制引入一个property访问器,来代替原有的成员变量。这样,是不是问题就解决了呢?

然而并没有。

我们前面说了,depends并不是一个实例的成员变量,而是一个类的成员变量。 它在__init__之前就需要被访问。这个变量是不能够通过在目标类当中增加访问器的方式来拦截的。将depends修改为实例的成员变量是不可接受的,__init__过程当中有许多重要的初始化操作,它们必须按照依赖关系按顺序执行,而这个顺序本身是依靠depends来计算的。修改为使用classmethod而不是成员变量算是一个方法,但毕竟要大量修改之前的代码。

那么,有没有别的方法了呢?留三分钟给读者思考。

-------------------解答的小分隔---------------------

答案其实很简单,但是这个答案是Python特有的。

访问一个类的成员变量,跟访问一个实例的成员变量,并没有太本质的区别。类自己也是一个实例,它是元类(metaclass)的实例。我们要用访问器拦截对这个成员变量的访问,只需要为ProxyModule提供一个特殊的metaclass就可以了,这就是VLCP中的实现:

class _ProxyMetaClass(type):
    '''
    Metaclass for delayed depend
    '''
    @property
    def depends(self):
        if not hasattr(self, '_depends'):
            self._depends = []
            path = manager.get('proxy.' + self.__name__.lower())
            if path is not None:
                try:
                    _, module = findModule(path, True)
                except KeyError as exc:
                    raise ModuleLoadException('Cannot load module ' + repr(path) + ': ' + str(exc) + 'is not defined in the package')
                except Exception as exc:
                    raise ModuleLoadException('Cannot load module ' + repr(path) + ': ' + str(exc))
                if module is None:
                    raise ModuleLoadException('Cannot find module: ' + repr(path))
            else:
                module = self._default
                if module is None:
                    raise ModuleLoadException('Cannot find module: proxy.' + self.__name__ + ' is not specified')
            self._proxytarget = module
            self.depends.append(module)
            if not hasattr(module, 'referencedBy'):
                module.referencedBy = []
            module.referencedBy.append(self)
        return self._depends

元类 + 访问器,将depends的计算延迟到了第一次实际访问的时刻,这样就完成了我们要求的在第三阶段计算依赖顺序时再通过配置文件生成实际的depends的要求。


我们可以看到,实际上一个语言的特性,对我们用这种语言开发的项目的影响是很深远的。如果一个语言强行划分了哪些部分是可以多态的,哪些是不可以多态的;哪些是面向对象的,哪些不是面向对象的;哪些是可以重写的,哪些是不可以重写的。那么我们在用这个语言进行开发的过程中,就会处处受制于这些限制,要么在进行到某个阶段的时候突然碰壁,不得不重构原有的代码;要么在一开始的时候就为了避免将来遇到这些问题而进行过度设计,反而让项目在一开始就举步维艰。

反过来,如果一门编程语言能保持充分的灵活性和一致性,让我们用最自然的方式去设计就能达到最佳的效果,那么项目的开发和演进自然就会无往不利。