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