前面我们谈了函数式编程,函数式编程总结起来就是把一些功能或逻辑代码通过函数拼装方式来组织的玩法。这其中涉及最多的是函数,也就是编程中的代码逻辑。但我们知道,代码中还是需要处理数据的,这些就是所谓的“状态”,函数式编程需要我们写出无状态的代码。 而这天下并不存在没有状态没有数据的代码,如果函数式编程不处理状态这些东西,那么,状态会放在什么地方呢?总是需要一个地方放这些数据的。 对于状态和数据的处理,我们有必要提一下“面向对象编程”(Object-oriented programming,OOP)这个编程范式了。我们知道,**面向对象的编程有三大特性:封装、继承和多态**。 面向对象编程是一种具有对象概念的程序编程范型,同时也是一种程序开发的抽象方针,它可能包含数据、属性、代码与方法。对象则指的是类的实例。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的可重用性、灵活性和可扩展性,对象里的程序可以访问及修改对象相关联的数据。在面向对象编程里,计算机程序会被设计成彼此相关的对象。 面向对象程序设计可以看作一种在程序中包含各种独立而又互相调用的对象的思想,这与传统的思想刚好相反:传统的程序设计主张将程序看作一系列函数的集合,或者直接就是一系列对计算机下达的指令。面向对象程序设计中的每一个对象都应该能够接受数据、处理数据并将数据传达给其它对象,因此它们都可以被看作一个小型的“机器”,即对象。 目前已经被证实的是,面向对象程序设计推广了程序的灵活性和可维护性,并且在大型项目设计中广为应用。此外,支持者声称面向对象程序设计要比以往的做法更加便于学习,因为它能够让人们更简单地设计并维护程序,使得程序更加便于分析、设计、理解。 现在,几乎所有的主流语言都支持面向对象,比如:Common Lisp、Python、C++、Objective-C、Smalltalk、Delphi、Java、Swift、C#、Perl、Ruby与PHP等。 说起面向对象,就不得不提由Erich Gamma、Richard Helm、Ralph Johnson和John Vlissides合作出版的《[设计模式:可复用面向对象软件的基础](https://book.douban.com/subject/1052241/)》(Design Patterns - Elements of Reusable Object-Oriented Software)一书,在此书中共收录了23种设计模式。 这本书的23个经典的设计模式,基本上就是说了两个面向对象的核心理念:
- 图的左边是“材质类” Material。其属性有燃点和密度。
- 图的右边是“家具类” Furniture。其属性有价格和体积。
- 在Furniture中耦合了Material。而具体的Material是Wood还是Plastic,这在构造对象的时候注入到Furniture里就好了。
- 这样,在家具类中,通过材料的密度属性和家具的体积属性就可以计算出重量属性。
这样设计的优点显而易见,它能和现实世界相对应起来,而且,材料类是可以重用的。这个模式也表现了面向对象的拼装数据的另一个精髓——喜欢组合,而不是继承。这个模式在设计模式里叫“桥接(Bridge)模式”。
和函数式编程来比较,函数式强调动词,而面向对象强调名词,面向对象更多的关注接口间的关系,而通过多态来适配不同的具体实现。
# 示例二:拼装功能
再来看一个示例。我们的需求是:处理电商系统中的订单,处理订单有一个关键的动作就是计算订单的价格。有的订单需要打折,有的则不打折。
在进行面向对象编程时,假设我们用Java语言,我们需要先写一个接口—— `BillingStrategy`,其中一个方法就是`GetActPrice(double rawPrice)`,输入一个原始的价格,输出一个根据相应的策略计算出来的价格。
```
interface BillingStrategy {
    public double GetActPrice(double rawPrice);
}
```
这个接口很简单,只是对接口的抽象,而与实现无关。现在我们需要对这个接口进行实现。
```
// Normal billing strategy (unchanged price)
class NormalStrategy implements BillingStrategy {
    @Override
    public double GetActPrice(double rawPrice) {
        return rawPrice;
    }
}
// Strategy for Happy hour (50% discount)
class HappyHourStrategy implements BillingStrategy {
    @Override
    public double GetActPrice(double rawPrice) {
        return rawPrice * 0.5;
    }
}
```
上面的代码实现了两个策略,一个是不打折的:`NormalStrategy`,一个是打了5折的:`HappyHourStrategy`。
于是,我们先封装订单项 `OrderItem`,其包含了每个商品的原始价格和数量,以及计算价格的策略。
```
class OrderItem {
    public String Name;
    public double Price;
    public int Quantity;
    public BillingStrategy Strategy;
    public OrderItem(String name, double price, int quantity, BillingStrategy strategy) {
        this.Name = name;
        this.Price = price;
        this.Quantity = quantity;
        this.Strategy = strategy;
    }
}
```
然后,在我们的订单类—— `Order` 中封装了 `OrderItem` 的列表,即商品列表。并在操作订单添加购买商品时,加入一个计算价格的 `BillingStrategy`。
```
class Order {
    private List<OrderItem> orderItems = new ArrayList<OrderItem>();
    private BillingStrategy strategy = new NormalStrategy();
    public void Add(String name, double price, int quantity, BillingStrategy strategy) {
        orderItems.add(new OrderItem(name, price, quantity, strategy));
    }
    
    // Payment of bill
    public void PayBill() {
        double sum = 0;
        for (OrderItem item : orderItems) {
            
            actPrice = item.Strategy.GetActPrice(item.price * item.quantity);
            sum += actPrice;
            
            System.out.println("%s -- %f(%d) - %f", 
            		item.name, item.price, item.quantity, actPrice);
        }
        System.out.println("Total due: " + sum);
    }
}
```
最终,我们在 `PayBill()` 函数中,把整个订单的价格明细和总价打印出来。
在上面这个示例中,可以看到,我把定价策略和订单处理的流程分开了。这么做的好处是,我们可以随时给不同的商品注入不同的价格计算策略,这样一来就有很高的灵活度了。剩下的事就交给我们的运营人员来配置不同的商品使用什么样的价格计算策略了。
注意:现实社会中,订单价格计算会比这个事复杂得多,比如:有会员价,有打折卡,还有商品的打包价等,而且还可以叠加不同的策略(叠加策略用前面说的函数式的pipeline或decorator就可以实现)。我们这里只是为了说明面向对象编程范式,所以故意简单化了。
其实,这个设计模式叫——策略模式。我认为,这是设计模式中最为经典的模式了,其充分体现了面向对象编程的方式。
# 示例三:资源管理
先看一段代码:
```
mutex m;
void foo() {
    m.lock();
    Func();
    if ( ! everythingOk() ) return; 
    ...
    ...
    m.unlock();
}
```
可以看到,上面这段代码是有问题的,原因是:那个 `if` 语句返回时没有把锁给unlock掉,这会导致锁没有被释放。如果我们要把代码写对,需要在return前unlock一下。
```
mutex m;
void foo() {
    m.lock();
    Func();
    if ( ! everythingOk() ) {
        m.unlock();
        return;
    } 
    ...
    ...
    m.unlock();
}
```
但是,在所有的函数退出的地方都要加上 `m.unlock();` 语句,这会让我们很难维护代码。于是可以使用面向对象的编程模式,我们先设计一个代理类。
```
class lock_guard {
	private: 
		mutex &_m;
	public:
		lock_guard(mutex &m):_m(m) { _m.lock(); }
		~lock_guard() { _m.unlock(); }
};
```
然后,我们的代码就可以这样写了:
```
mutex m;
void foo() {
	lock_guard guard(m);
	Func();
	if ( ! everythingOk() ) {
		return;
	} 
	...
	...
}
```
这个技术叫RAII(Resource Acquisition Is Initialization,资源获取就是初始化), 是C++中的一个利用了面向对象的技术。这个设计模式叫“代理模式”。我们可以把一些控制资源分配和释放的逻辑交给这些代理类,然后,只需要关注业务逻辑代码了。而且,在我们的业务逻辑代码中,减少了这些和业务逻辑不相关的程序控制的代码。
从上面的代码中,我们可以看到下面几个面向对象的事情。
然后,有一天,我们发现需要对灯泡扩展一下,于是做了个抽象类:
但是,如果有一天,我们发现这个开关可能还要控制别的不单单是灯泡的东西,就会发现这个开关耦合了灯泡这种类别,非常不利于扩展,于是反转控制出现了。
就像现实世界一样,造开关的工厂根本不关心要控制的东西是什么,它只做一个开关应该做好的事,就是把电接通,把电断开(不管是手动的,还是声控的,还是光控,还是遥控的)。而我们造的各种各样的灯泡(不管是日光灯、白炽灯)的工厂也不关心你用什么样的开关,反正我只管把灯的电源接口给做出来。然后,开关厂和电灯厂依赖于一个标准的通电和断电的接口。于是产生了IoC控制反转,如下图:
所谓控制反转的意思是,开关从以前设备的专用开关,转变到了控制电源的开关,而以前的设备要反过来依赖于开关厂声明的电源连接接口。只要符合开关厂定义的电源连接的接口,这个开关可以控制所有符合这个电源连接接口的设备。也就是说,开关从依赖设备这种情况,变成了设备反过来依赖于开关所定义的接口。
这样的例子在生活中太多见了,比如说: