往期精彩:
关键字:面向对象、设计模式、面向接口、IoC
在谈起C++函数式编程复兴之前,先聊一下大家更熟悉的“面向对象编程”(Object-oriented programming,OOP)这个编程范式。
学习过C++之后一定都知道面向对象的三大特性:封装、继承、多态,正是C++提供了如此强大的特性,让C++程序员更熟悉面向对象命令式的编程。面向对象编程是中具有对象概念的编程范式,同时也是一种程序开发的抽象方针。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性,对象里的程序可以访问及经常修改对象相关连的数据。
面向对象程序设计可以看作一种在程序中包含各种独立而又互相调用的对象的思想,这与传统的思想刚好相反:传统的程序设计主张将程序看作一系列函数的集合,或者直接就是一系列对电脑下达的指令。面向对象程序设计中的每一个对象都应该能够接受数据、处理数据并将数据传达给其它对象,因此它们都可以被看作一个小型的“机器”,即对象。
说起面向对象无论如何都绕不过GoF的著作《设计模式:可复用面向对象软件的基础》,在书中最开始的部分就对面向对象的核心做出了总结,这也是书中23种经典设计模式的核心理念:
假设有如下描述:
我们如下所展示的一样设计类:
这样设计的优点显而易见,它能和现实世界相对应起来,对于未来的变化也只过加对应的类就可以了。比如说,日后材料升级了,你可以用钢材生产家具了,就不用一个个写出“钢材桌子”、“钢材椅子”等类型,以后又有订单生产柜子,也只需要写出继承后的“柜子类”,而其使用什么材料只需要在构造对象的时候注入组合就好了。
在庞杂的代码中, 即使是很小的改动都非常难以完成, 因为你必须要在整体上对代码有充分的理解。 而在较小且定义明确的模块中, 进行修改则要容易得多。有张图很生动的解释了桥接模式的优点
桥接模式将抽象部分与它的实现部分分离,是它们可以独立地应对变化。同时也表现了面向对象的一个精髓——更喜欢组合,而不是继承。也有很多其他例子,比如说购买一杯咖啡(摩卡还是拿铁,实现部分),有大杯小杯(抽象部分),未来可能有美式咖啡,杯子还有尊享杯、女神杯(未来变化)。再比如更常见的是,在面对不同平台不同API的条件下实现相同的功能,将实现和抽象分离,再通过讲解模式为两者之间搭起桥梁。
面向对象更强调名词,面向对象更多的关注接口间的关系,而通过多态来适配不同的具体实现。相反函数式编程更强调动词,后续会谈到。
在超时购买商品的时候,有一个关键的动作就是计算价格,是否打折,如何打折?
使用面向对象编程的思想,先写一个接口,输入原始价格,输出相应策略计算后的价格。
class Strategy {
public:
virtual double GetPrice(double rawPrice) = 0;
};
这个接口是抽象的,而具体的实现是在具体的策略类中实现。
class NormalStrategy : public Strategy {
public:
double GetPrice(double rawPrice) override {
return rawPrice;
}
};
class FridayStrategy : public Strategy {
public:
double GetPrice(double rawPrice) override {
return rawPrice * 0.8;
}
};
以上代码实现了两个策略,一个是不打折的——NormalStrategy ,一个是打八折——FridayStrategy 。
再对单项商品进行封装,包含原始价格和相对应的策略。
class Item {
public:
Item(string name, double price, Strategy s) : itemName(name), itemPrice(price), ItemStrategy(s) {}
private:
string itemName;
double itemPrice;
Strategy strategy;
};
然后在订单类中维护一个Item列表,即商品列表。
class Order {
public:
void AddItem() {
//添加商品
}
double Pay() {
//遍历容器计算价格
}
private:
vector<Item> orderItem;
};
最终,在Pay()函数中,把整个订单的价格明细和总价打印出来。
以上的示例中,把定价策略和订单处理流程锋利开来,带来的好处就是我们随时可以给不同商品注入不同的价格计算策略,同时也保证了开闭原则,现实中还有会员折、打折卡等等,还可以叠加不同的打折策略。此示例仅仅为了说明面向对象的编程范式,故有简化。
实际上,在设计模式中对此类设计给出了总结——策略模式,基于组合机制,通过对相应行为提供不同的策略来改变对象的部分行为。策略模式充分体现了面向对象编程的方式,面向接口编程,并且运用对象组合方式替代类继承,保证灵活性以及动态绑定的可能。
在智能指针篇提到过一项技术,RAII(Resource Acquisition Is Initialization,资源获取就是初始化),不了解的同学可以再去看看。RAII是C++中的一种惯用手法,利用了面向对象的技术,在设计模式也有对应——代理模式。把一些控制资源分配和释放的逻辑交由代理类管理,然后只需要关注业务逻辑代码,在其中减少了资源管理这些和业务逻辑不相关的程序控制代码。
在上面的示例中,我们可以看到几个面向对象的事情:
IoC 控制反转是一种被用于面向对象的设计,与依赖倒置原则(DIP Dependency Inversion Principle)是同义的,其还有另一个昵称“好莱坞原则”(不要调用我,让我来调用你)。那到底什么是控制反转呢?举例来说明。
假如我们要生产一辆汽车Car,依赖轮子Tires和发动机engine的设计生产。代码如下:
class Engine;
class Tire;
class Car {
public:
Car() {
engine = new Engine();
tire = Tires.Instance();
}
private:
Engine engine;
Tire tire;
};
上面这个Car类中有对Engine和Tire的依赖,在构造car对象时,还需要将依赖赋值到当前类的内部属性上,再把依赖实例化。这样做并不满足SOLID中的开闭原则,会带来问题。假如我很多车辆类,他们都有依赖项Engine,时代发展,产能进步了,要把所有的Engine升级为EngineV8,难不成要将所有的Car类中Engine()替换为EngineV8(),想想都麻烦头疼。
我们尝试另一种思路,假设有个工厂BuildEngine是专门来生产发动机的,当你生产Car需要发动机时告诉我需要什么型号就好。Car类不再依赖Engine。
class BuildEngine {
public:
Engine&& GetEngine(string engineName) {} //工厂提供接口,返回发动机对象
}
auto engineBuilder = new BuildEngine();
class Car {
public:
Car() {
engine = engineBuilder.GetEngine("EngineV8");
tire = tireBuilder.GetTire("tires");
}
private:
Engine engine;
Tire tire;
};
现在发动机和轮胎的创建不再直接依赖它们的构造,而是通过工厂生产(也就是所谓IoC容器),使得Car类与Engine类没有了强耦合。不再依赖具体构造,而是依赖IoC容器,即针对于接口编程,不针对实现编程。
所谓控制反转的意思是,发动机是由Car类构造生产,转变到了由专门生产发动机的工厂生产。而以前的Car类自己设计生产Engine反过来要依赖于Engine工厂生产发动机的接口。也就是说,Engine依赖Car类构造生成,变成了,Car类反过来依赖于Engine所定义的接口。
这是依赖倒转(DIP)的一种表现形式,或者称为依赖注入(翻译方式很多)。
生活中也存在着类似的例子:在交易过程中,买卖双方一手交钱一手交货,卖家与买家强耦合(必需见面)。这个时候银行跳出来作担保了,买家把钱先垫到银行,银行让卖家发货,买家验货后,银行再把钱打给卖家。这就是反转控制。买卖双方把对对方的直接控制,反转到了让对方来依赖一个标准的交易模型的接口。
可见,控制反转和依赖倒置不单单的一种设计模式,反而更是一种管理模式。
在团队工作管理中,如果依赖和控制的东西过多了,就需要制定标准,倒置依赖,反转控制。
优点
缺点
相比于面向对象,函数式编程更强调于“动词”,更容易写出无状态的代码(并发场景不会有问题)。
[1]https://en.wikipedia.org/wiki/Object-oriented_programming
[2]《设计模式:可复用面向对象软件的基础》
[3]https://refactoringguru.cn/design-patterns/strategy
[4]https://design-patterns.readthedocs.io/zh_CN/latest/structural_patterns/proxy.html
[5]https://coolshell.cn/articles/9949.html
[6]https://coolshell.cn/articles/8961.html