Modern C++ 学习笔记——C++面向对象编程

往期精彩:

  • Modern C++ 学习笔记——易用性改进篇
  • Modern C++ 学习笔记 —— 右值、移动篇
  • Modern C++ 学习笔记 —— 智能指针篇
  • Modern C++ 学习笔记 —— lambda表达式篇
  • Modern C++ 学习笔记 —— C++面向对象编程
  • Modern C++ 学习笔记 —— C++函数式编程

Modern C++ 学习笔记——C++中的面向对象编程

关键字:面向对象、设计模式、面向接口、IoC

文章目录

  • Modern C++ 学习笔记——C++中的面向对象编程
    • 面向对象编程(OOP)
      • 示例一:桥接模式
      • 示例二:策略模式
      • 示例三:资源管理——代理模式
      • 小结
    • IoC 控制反转
    • 面向对象的优缺点
    • 参考资料

面向对象编程(OOP)

在谈起C++函数式编程复兴之前,先聊一下大家更熟悉的“面向对象编程”(Object-oriented programming,OOP)这个编程范式。
学习过C++之后一定都知道面向对象的三大特性:封装、继承、多态,正是C++提供了如此强大的特性,让C++程序员更熟悉面向对象命令式的编程。面向对象编程是中具有对象概念的编程范式,同时也是一种程序开发的抽象方针。它将对象作为程序的基本单元,将程序和数据封装其中,以提高软件的重用性、灵活性和扩展性,对象里的程序可以访问及经常修改对象相关连的数据。

面向对象程序设计可以看作一种在程序中包含各种独立而又互相调用的对象的思想,这与传统的思想刚好相反:传统的程序设计主张将程序看作一系列函数的集合,或者直接就是一系列对电脑下达的指令。面向对象程序设计中的每一个对象都应该能够接受数据、处理数据并将数据传达给其它对象,因此它们都可以被看作一个小型的“机器”,即对象。

说起面向对象无论如何都绕不过GoF的著作《设计模式:可复用面向对象软件的基础》,在书中最开始的部分就对面向对象的核心做出了总结,这也是书中23种经典设计模式的核心理念:

  • 面向接口编程,而不是面向实现编程
    • 使用值不需要知道数据类型、结构、算法细节。
    • 使用者不需要知道实现细节,只需要知道提供的接口。
    • 利用抽象、封装、动态绑定、多态。
    • 符合面向对象的特质和理念。
  • 优先使用对象组合,而不是类继承
    • 继承需要给子类暴露一些父类的设计和实现细节。
    • 父类实现的改变会造成子类也需要改变。
    • 我们以为继承主要是为了代码重用,但实际上在子类中需要重新实现很多父类的方法。
    • 继承更多的应该是为了多态。

示例一:桥接模式

假设有如下描述:

  • 家具:木头桌子、木头椅子、塑料桌子、塑料桌子
  • 属性:燃点、密度、价格、重量

我们如下所展示的一样设计类:

  • 有“材质类”Material,具有属性燃点和密度
  • 有“家具类”Furniture,具有属性价格和体积
  • 在 Furniture 中耦合了 Material。而具体的 Material 是 Wood 还是 Plastic,这在构造对象的时候注入到 Furniture 里就好了。
Material -double destiny -double burning +GetDestiny() +GetBurnPoint() Wood Plastic Furniture double volume double price +GetWeight() +GetBurnPoint() Desk +GetWeight() +GetBurnPoint() Table +GetWeight() +GetBurnPoint()

这样设计的优点显而易见,它能和现实世界相对应起来,对于未来的变化也只过加对应的类就可以了。比如说,日后材料升级了,你可以用钢材生产家具了,就不用一个个写出“钢材桌子”、“钢材椅子”等类型,以后又有订单生产柜子,也只需要写出继承后的“柜子类”,而其使用什么材料只需要在构造对象的时候注入组合就好了。

在庞杂的代码中, 即使是很小的改动都非常难以完成, 因为你必须要在整体上对代码有充分的理解。 而在较小且定义明确的模块中, 进行修改则要容易得多。有张图很生动的解释了桥接模式的优点
Modern C++ 学习笔记——C++面向对象编程_第1张图片

桥接模式将抽象部分与它的实现部分分离,是它们可以独立地应对变化。同时也表现了面向对象的一个精髓——更喜欢组合,而不是继承。也有很多其他例子,比如说购买一杯咖啡(摩卡还是拿铁,实现部分),有大杯小杯(抽象部分),未来可能有美式咖啡,杯子还有尊享杯、女神杯(未来变化)。再比如更常见的是,在面对不同平台不同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++中的一种惯用手法,利用了面向对象的技术,在设计模式也有对应——代理模式。把一些控制资源分配和释放的逻辑交由代理类管理,然后只需要关注业务逻辑代码,在其中减少了资源管理这些和业务逻辑不相关的程序控制代码。

小结

在上面的示例中,我们可以看到几个面向对象的事情:

  • 使用接口抽象了具体的实现类
  • 与其他类耦合的是接口,而非实现类,即多态,增加了程序的扩展性。
  • 面向接口编程,所谓接口就是一种“协议”,就像HTTP协议一般,无论浏览器还是后端程序都遵循这一协议,而不是具体的实现。
  • 这不仅是面向对象的编程范式的精髓,也是IoC(控制反转)的本质。

IoC 控制反转

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)的一种表现形式,或者称为依赖注入(翻译方式很多)。

生活中也存在着类似的例子:在交易过程中,买卖双方一手交钱一手交货,卖家与买家强耦合(必需见面)。这个时候银行跳出来作担保了,买家把钱先垫到银行,银行让卖家发货,买家验货后,银行再把钱打给卖家。这就是反转控制。买卖双方把对对方的直接控制,反转到了让对方来依赖一个标准的交易模型的接口。

可见,控制反转和依赖倒置不单单的一种设计模式,反而更是一种管理模式。

在团队工作管理中,如果依赖和控制的东西过多了,就需要制定标准,倒置依赖,反转控制。

面向对象的优缺点

优点

  • 能合真实世界呼应,符合人的直觉
  • 强调与“名词”而不是“动词”,更多地关注对象和对象之间的接口。
  • 高内聚的对象有效地分离了抽象和具体实现,增强可重用性、可扩展性、易于维护。
  • SOLID(单一功能,开闭原则,里氏替换,接口隔离,依赖反转,是面向对象设计的五个基本原则)
  • 拥有大量非常优秀的设计原则和设计模式

缺点

  • 代码都需要附着在一个类上,其鼓励了类型。
  • 存在大量的封装,以及数据的状态,导致了在并发下出现很多问题。
  • 通过对象达到抽象,然后在组合起来让它们执行,导致了“代码粘合层”。

相比于面向对象,函数式编程更强调于“动词”,更容易写出无状态的代码(并发场景不会有问题)。

参考资料

[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

你可能感兴趣的:(学习笔记,设计模式,面向对象编程,c++,后端,面试)