一、Decorator 编程范式
1.1、Python Decorator
Python 的 Decorator 在使用上和 Java 的 Annotation(以及 C# 的 Attribute)很相似,就是在方法名前面加一个 @XXX 注解来为这个方法装饰一些东西。但是,Java/C# 的 Annotation 也很让人望而却步,太过于复杂了。你要玩它,需要先了解一堆 Annotation 的类库文档,感觉几乎就是在学另外一门语言。
而 Python 使用了一种相对于 Decorator Pattern 和 Annotation 来说非常优雅的方法,这种方法不需要你去掌握什么复杂的 OO 模型或是 Annotation 的各种类库规定,完全就是语言层面的玩法:一种函数式编程的技巧。
js
def hello(fn):
def wrapper():
print("hello, %s" % fn.__name__)
fn()
print("bye, %s" % fn.__name__)
return wrapper
@hello
def Hao():
print("i am Liu Mr")
Hao()
代码的执行结果如下:
js
hello, Hao i am Liu Mr bye, Hao
- 函数 Hao 前面有个 @hello 的 "注解",hello 就是我们前面定义的函数 hello;
- 在 hello 函数中,其需要一个 fn 的参数(这就是用来做回调的函数);
- hello 函数中返回了一个 inner 函数 wrapper,这个 wrapper 函数回调了传进来的 fn,并在回调前后加了两条语句。
对于 Python 的这个 @注解语法糖(Syntactic sugar)来说,当你在用某个 @decorator 来修饰某个函数 func 时,如下所示:
js
@decorator
def func():
print("hello world")
其解释器会解释成下面这样的语句:
js
func = decorator(func)
就是把一个函数当参数传到另一个函数中,然后再回调。但是,我们需要注意,那里还有一个赋值语句,把 decorator 这个函数的返回值赋值回了原来的 func。
1.2、Golang Decorator
Python 有语法糖,所以写出来的代码比较酷。但是对于没有修饰器语法糖Go语言,写出来的代码会是怎么样呢?
js
package main
import "fmt"
func decorator(f func(s string)) func(s string) {
return func(s string) {
fmt.Println("Started")
f(s)
fmt.Println("Done")
}
}
func Hello(s string) {
fmt.Println(s)
}
func main() {
hello := decorator(Hello)
hello("Hello World!!!")
}
Golang动用了一个高阶函数 decorator(),在调用的时候,先把 Hello() 函数传进去,然后其返回一个匿名函数。这个匿名函数中除了运行自己的代码,也调用了被传入的 Hello() 函数。
1.3、Decorator 总结
通过Python 和 Go 修饰器的例子,修饰器模式主要做了下面的几件事。
- 表面上看,修饰器模式就是扩展现有的一个函数的功能,让它可以干一些其他的事,或是在现有的函数功能上再附加上一些别的功能。
- 除了我们可以感受到函数式编程下的代码扩展能力,我们还能感受到函数的互相和随意拼装带来的好处。
- Decorator 这个函数其实是可以修饰几乎所有的函数的。于是,这种可以通用于其它函数的编程方式,可以很容易地将一些非业务功能的、属于控制类型的代码给抽象出来(所谓的控制类型的代码就是像 for-loop,或是打日志,或是函数路由,或是求函数运行时间之类的非业务功能性的代码)。
二、面向对象编程
函数式编程总结起来就是把一些功能或逻辑代码通过函数拼装方式来组织的玩法。这其中涉及最多的是函数,也就是编程中的代码逻辑。但我们知道,代码中还是需要处理数据的,这些就是所谓的"状态",函数式编程需要我们写出无状态的代码。
对于状态和数据的处理,我们有必要提一下"面向对象编程"(Object-oriented programming,OOP)这个编程范式了。我们知道,面向对象的编程有三大特性:封装、继承和多态。
面向对象编程是一种具有对象概念的程序编程范型,同时也是一种程序开发的抽象思想,它包含属性与方法。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的可重用性、灵活性和可扩展性,对象里的程序可以访问及修改对象相关联的数据。
说起面向对象,就不得不提由 Erich Gamma、Richard Helm、Ralph Johnson 和 John Vlissides 合作出版的《设计模式:可复用面向对象软件的基础》(Design Patterns - Elements of Reusable Object-Oriented Software)一书,在此书中共收录了 23 种设计模式。
-
"Program to an 'interface', not an 'implementation'."
- 调用方无需知道程序的数据类型、结构、算法的细节。
- 调用方无需知道实现细节、只需知道提供的接口规范。
- 可以利用抽象、封装、动态绑定、多态这些语法糖。
- 符合面向对象的特质和理念。
-
"Favor 'object composition' over 'class inheritance'"
- 继承需要给子类暴露父类的设计和实现细节。
- 父类的改变会导致子类也需要变化。
- 继承主要为了代码重用,但实际子类中需要重新实现很多父类的方法。
- 继承更多的应该是为了多态。
控制反转(IoC)
IoC的概念已经提出来很多年了,其被用于一种面向对象的设计IoC/DIP(控制反转/依赖倒置)。控制反转和面向接口编程其实如出一辙,就是把以前底层强依赖于实现的方式,变成了底层提供接口和规范,调用方依赖接口就可以。
生活例子也有很多,比如说:
- 货币的出现就是一个很好的实例。以前大家都是"以物换物",物品之间的交易换算就会非常复杂,于是货币出现了,其实就是一种交易协议,所有的商品都依赖这个协议,而不用彼此间相互依赖,于是整个世界的运作简单了,交易高效了。
- 银行的出现也是这样。买家和卖家进行交易,一手交钱一手交货,也就是说买家和卖家是强耦合的关系。这个时候,银行可以出来担保,买家把钱先垫到银行,银行让卖家发货,买家验货后,银行再把钱打给卖家,这个就是反转控制。买卖双方把跟对方的直接依赖和控制,反转到了让对方来依赖一个标准的交易模型接口。
在我们工作中,经常会在标准化和定制化中纠结,我们痛苦于哪些应该是平台要做的,哪些应该要丢出去。这里就会出现很多与业务无关的软件或中间件,包括协议、数据、接口...,基于面向对象的方式,我们可以通过抽象、中间件来解耦,来降低软件的复杂度。
面向对象小结
优点:
- 与真实世界交相辉映,符合人的直觉。
- 面向对象和数据库模型设计类似,更多地关注对象间的模型设计。
- 更多的关注对象和对象间的接口。
- 根据业务特质形成一个高内聚的对象,有效地分离抽象和具体的实现,增强可重用性和可扩展性。
- 拥有大量非常优秀的设计原则和设计模式。(单一功能、开闭原则、里氏替换、接口隔离以及依赖反转)
缺点:
- 代码都需要依附于类,其鼓励了类型。
- 代码需要通过对象来达到抽象的效果,导致了相当厚重的"代码粘合层"。
- 太多的封装以及对状态的鼓励,导致了大量不透明在并发下出现很多问题。
Java里很多注入方式,像Spring那些注入、鼓励黏合,导致了大量的封装,完全不知道里面在干什么事情。而且封装屏蔽了细节,具体发生啥事你根本不知道。这些都是面向对象不太好的地方。