编程原则探讨

0.引言

代码的要求往往是可读性高、高聚合、低耦合、逻辑清晰、测试驱动……但这些往往变成了政治正确,而不是一个可以执行和检查的动作。
接下来从OOP的三大特征——封装、继承、多态来逐一讨论。

1.封装

Encapsulation refers to the bundling of data with the methods that operate on that data.
Encapsulation is used to hide the values or state of a structured data object inside a class, preventing unauthorized parties' direct access to them.
--Wikipedia

封装在工程中往往是单元化的一种手段,让独立的逻辑单元和其他部分解耦合。封装提供了一个box,在设计时要定义好input和output,变成工程的一个组件(component)。好处是既能复用组件,减少代码量,同时又能灵活的支持业务的变化,仅在组件内部进行修改,而不需要在整个工程项目中修改。
例如设计一个人作为工程组件,名字、性别、工资作为输入。

class Person:
    def __init__(self, name, sex, pay=0):
        self.name = name
        self.sex = sex
        self.pay = pay

这样的设计是一个相对好的逻辑,因为代码中默认的逻辑是,一个人的实例化必须指定名字和性别,pay是选填内容,且三个信息彼此之间是独立的。

class Person:
    def __init__(self, brithday, sex, age):
        self.birthday = brithday
        self.sex = sex
        self.age = age

上例的输入birthday和age是互相关联的,不应该同时作为输入。
同时,以上的两个例子都有一个默认逻辑,设定好的值支持修改,但一个人的性别或者出生日期是物理属性不可修改,于是可以做下面的保护设计。

from datetime import date

class Person:
    def __init__(self, birthday, name, sex, children, pay=0):
        self._birthday = birthday  # 存储一个元组,年月日,(1995, 1, 1)
        self._name = name
        self._sex = sex
        self.children = children  # 存储一个list,存放每一个孩子的名字
        self.pay = pay
        
    @property
    def birthday(self):
        return self. _birthday
        
    @property
    def name(self):
        return self._name
        
    @property
    def sex(self):
        return self._sex  
        
    @property
    def age(self):
        cur = date.today()
        bir = date(*self._birthday)
        if cur > bir:
            return cur.year - bir.year
        else:
            return "Invalid age"

继续分析,发现如果一个人有了新的孩子,要么需要把所有的孩子取出做修改,要么通过person.children.append实现,前者效率低,后者暴露了系统内部的数据结构。用下面方法解决

class Person:
    def __init__(self, birthday, name, sex, children, pay=0):
        self._birthday = birthday  # 存储一个元组,年月日,(1995, 1, 1)
        self._name = name
        self._sex = sex
        self.children = _children  # 存储一个list,存放每一个孩子的名字
        self.pay = pay
        
    @property
    def birthday(self):
        return self. _birthday
        
    @property
    def name(self):
        return self._name
        
    @property
    def sex(self):
        return self._sex  
        
    @property
    def age(self):
        cur = date.today()
        bir = date(*self._birthday)
        if cur > bir:
            return cur.year - bir.year
        else:
            return "Invalid age"
            
    @property
    def children(self):
        return self._children
    
    def add_child(self, child):
        self._children.append(child)
        

综上,一种工程化的封装方法有如下特征:

  • 输入变量彼此独立,输出变量彼此独立
  • 实现必须赋值和选择性赋值变量的默认逻辑
  • 实现不可赋值变量的私有保护
  • 实现内部数据结构的保护,通过外部接口的方式修改,例如上例中的增加一个孩子的add_child接口

2.继承

Inheritance is the mechanism of basing an object or class upon another object (prototypical inheritance) or class (class-based inheritance), retaining similar implementation.
--Wikipedia

继承主要思想是代码复用,建立class的关联,比如python就是通过自下而上、自左而右的对继承的父类进行检索的,默认检索所有class的__init__信息。继承是一种is-a或者is-a-kind-of的关系。
大多数的程序都可以实现代码复用的功能,但对class关联关注不足。

class Person:
    def __init__(self, birthday, name, sex, children, pay=0):
        self._birthday = birthday  # 存储一个元组,年月日,(1995, 1, 1)
        self._name = name
        self._sex = sex
        self.children = _children  # 存储一个list,存放每一个孩子的名字
        self.pay = pay
        
    @property
    def birthday(self):
        return self. _birthday
        
    @property
    def name(self):
        return self._name
        
    @property
    def sex(self):
        return self._sex  
        
    @property
    def age(self):
        cur = date.today()
        bir = date(*self._birthday)
        if cur > bir:
            return cur.year - bir.year
        else:
            return "Invalid age"
            
    @property
    def children(self):
        return self._children
    
    def add_child(self, child):
        self._children.append(child)
        
    def makeup(self):
        pass

class Man(Person):
    def __init__(self, *args):
        args = args[:2] + ('M',) + args[3:]  # 无论设置性别是什么,都修改为'M'
        super().__init__(*args)
        
    def farm(self):
        pass

上例Man继承Person,同时增加了farm的能力,可是Person与Man的关联不合理,Man同时继承了Person的makeup能力,但这是Man没有的能力。
综上,一种工程化的继承方法有如下特征:

  • 符合种is-a或者is-a-kind-of的关系
  • reuse codes
  • 关注继承中各个部分的关系正确性

3.多态

Polymorphism is the provision of a single interface to entities of different types or the use of a single symbol to represent multiple different types
--Wikipedia

多态是基于继承发展出的概念,既要复用代码又想要有灵活性,于是一个继承父的子,可以重写父的方法,实现子独立的方式。

4.模块化

模块化原则一般是SOLID

  • Single responsibility principle
    单一模块应该服务单一功能,只对单一功能进行定义和修改

  • Open–closed principle
    模块的依赖应该是抽象的内容,而不能依赖于low-level的模块

  • Liskov substitution principle
    使用可替换的组件来构建软件系统,同一层次的组件遵守同一个约定,以方便替换

  • Interface segregation principle
    在设计中避免不必要的依赖,软件系统不应该依赖其不直接使用的组件

  • Dependency inversion principle
    通过新增代码修改系统行为,而非修改原来的代码

在工程实践中,总是会遇到功能迭代和重构的问题。
迭代时要同时支持迭代前后的功能,比较好的一种实现方法是在代码中增加flag和if判断,通过前后版本走不通的if分支来同时支持,当迭代后的版本开发都完成时统一跃迁到新的版本。
重构时更加困难,同时支持重构前后版本往往非常困难,很难通过上述的flag和if分支的方式解决,这时可以开两个底层module实现前后重构,在higher level通过flag和if分支来调用不同的底层module来实现。

参考:技术 01 - 聊聊编程原则(OOP,SOLID),https://zhuanlan.zhihu.com/p/46830303

你可能感兴趣的:(编程原则探讨)