C++学习笔记----5、重用之设计(二)---- 为最优化重用结构化你的代码(1)

        一定要在所有的层面在设计的一开始就考虑重用,也就是说,从一个独立的函数到一个类,直到整个库与框架。我们以后就把这些统称为部件。下面的策略会帮助你正确地组织代码。记住所有的这些策略关注的是你的代码的的通用目的。设计可重用代码第二个方面就是提供易用性,与接口设计关联度更大,以后会详细讨论。

1、避免将无关或者逻辑上独立的概念组合在一起

        当设计一个组件时,应该将重点放在一个单独的任务或者一组任务上,也就是说,应该努力做到高内聚。这就是著名的单一职责原则SRP。不要将例如随机数生成器与XML解析器这样的无关的概念组合在一起。

        即使不是将代码设计用来重用的,也要牢记这个原则。整个程序很少自己重用。事实是,程序的部分或者子系统会直接整合进其他应用中或者应用于轻微不同的程序。这样,你应该设计程序使其逻辑上分隔成单独的功能组件,以便在不同的程序中重用。每个这样的部件应该具有定义好的责任。

        这种程序策略模型化了真实世界的独立的、可交换部件的设计原则。例如,你可以写一个Car类,将引擎的所有属性与行为放置其中。然而,引擎是单独的部件,没有与汽车的其他方面绑在一起。引擎可以从一辆汽车中移出,再把它放到其他汽车中。合适的设计应该是Engine类包含了引擎特定的功能。Car实例只是包含了一个Engine的一个实例。

2、将程序划分为逻辑子系统

        应该将子系统设计成独立的部件,可以独立重用,也就是说,要做到低耦合。例如,如果你在设计一个网络游戏,切将网络与图形用户界面划分在不同的子系统中。这样的话,你就可以重用任一部件而不用把其他的代码拉进来。例如,你可能想要写一个非网络的游戏,这样的话你可以重用图形用户界面子系统而不需要网络部分。同样的,你可以设计一个点对点的文件共享程序,这种情况下你就可以重用网络子系统而不需要图形用户界面的功能。

        要确保在每一个子系统中遵从抽象原则。把每一个子系统想象成一个原型的库,都要提供一个清楚易懂又易用的界面。即使是只有你一个人使用这些原型库,也会从这些设计良好的界面与单一功能逻辑分离的实现中获益非浅。

3、使用类层次结构来分隔逻辑概念

        将程序分隔成逻辑子系统,应该避免在类层次上将无关概念组合在一起。例如,假设你要为自动驾驶汽车写一个类。决定以一个基本的汽车类开始,然后将自动驾驶的逻辑直接整合进来。然而,如果你只是要写一个非自动驾驶的汽车的程序呢?这种情况下,所有与自动驾驶的逻辑都没有用了,并且要求你的程序去与本来可以避免的类库相连接,比如vision库、LIDRAR库等等。一个可能的解决方案就是生成一个类层次结构,一个自动驾驶的汽车派生于一个通用的汽车。这样的话,就可以在程序中使用汽车基类,而不需要自动驾驶的能力,也就不需要加上这种算法的支出了。下图所示即为该层次结构:

        这种策略在有两个逻辑概念时会比较好使,比如自动驾驶和汽车。当有三个或更多的概念时就会变得非常复杂。例如,假设你要提供卡车与汽车,每个都会要求自动驾驶或者不自动驾驶。逻辑上,卡车与汽车都是交通工具,所以它们可以是vehicle类的派生类,如下图所示:

        同样的,自动驾驶类可以是非自动驾驶类的派生类。无法将其进行线性层次结构的划分。一种可能就是将自动驾驶功能做成一个混合类。前面的博文使用多重继承在C++中实现混合类。例如,一个PictureButton可以是继承自Image类与Clickable类的混合类。然而,对于自动驾驶设计来说,最好是使用一种不同的混合类实现,使用类模板。

        对于类模板,我们还没有详细讨论过,不过,对于我们目前讨论的东西,其细节并不重要。当然了,对于继承的讨论也不够详细,同样的,其细节对于我们这个案例的讨论也不重要。你只需要知道Derived类继承/派生自Base类就够了:

class Derived : public Base {};

        SelfDrivable混合类模板定义如下:

template <typename T>
class SelfDrivable : public T
{
};

        SelfDrivable混合类提供了实现自动驾驶功能的所有需要的算法。一旦有了SelfDrivable混合类模板,你就可以如下所示实例化一辆汽车各一辆卡车:

SelfDrivable<Car> selfDrivingCar;
SelfDrivable<Truck> selfDrivingTruck;

        这两行代码的结果就是编译器使用SelfDrivable混合类模板生成一个实例,类模板中的T被Car替换,因此就从Car派生,另一个T被Truck替换,因此派生自Truck。以后我们再详细讨论混合类。

        这种解决方案要求你写四个不同的类(Vehicle,Car,Truck和SelfDrivable),但是对于功能清晰的划分值得这种努力 。

        同样的,也应该避免无关的概念组合在一起,也就是说,高内聚,不仅仅是在类的层次,在设计的任何层次,都应该避免。例如,在成员函数的层次,一个单独的成员函数不应该执行逻辑无关的事情,混合交互(set)与检测(get)等等。

4、使用聚合划分逻辑概念

        聚合,我们前面讨论过,模型化了组合关系:对象包含了其他对象以执行一些功能特点。前面也不止一次强调,如果可以选择的话,尽量使用组合关系,而避免使用继承关系。

        例如,假设你要写一个FamilyTree类来保存家庭成员。很明显,树结构对于保存此类信息是理想的。不要将树结构的代码直接整合进FamilyTree类中,应该写一个单独的Tree类。FamilyTree类可以包含并且使用Tree实例。用面向对象的术语,就是FamilyTree组合了Tree。使用此技巧,树结构可以在另外的程序中更容易地重用。

5、消除用户接口依赖性

        如果你的库是一个数据操作库,想要将数据操作与用户界面分隔开。这就意味着这类库你永远不要假设哪类用户界面会用到此类库。库也不要使用任何标准控制台输出与输入功能,像std::println()或者cin,因为如果该库用于图形用户界面,这样做就毫无意义。例如,一个Windows GUI的应用通常不会有任何形式的控制台I/O。如果你认为库只用于GUI应用,依然不要弹出任何类型的消息框或者其他类型的提示给到最终用户,因为这是用户代码要实现的功能。是用户代码要决定怎么将信息展示给用户。这种类型的依赖不但使得可重用性差,也使得用户代码无法正确响应错误,例如,静态处理错误信息等。

        MVC模式,我们以前讨论过,是一个非常著名的将数据与展示分隔开的设计模式。使用此模式,模型可以位于库中,用户代码可以提供视图与控制器。

6、对于通用数据结构和算法使用模板

        C++有一个概念叫做模板,允许你生成通用类型与类的结构。例如,你可能写过一个整型数组的代码,后来想要一个双浮点数数组,你就需要重写并复制所有用双浮点数的代码。模板就能将刚才所说的整型或双浮点型的规格变成参数,可以生成一个用于任何类型的代码块。模板允许你写用于任何类型的数据结构与算法。

        我们前面讨论过std::vector类,它是C++标准库的一部分。要生成一个整型的vector,可以写std::vector<int>;要生成双浮点型的vector,可以写std::vector<double>。一般来说,模板编程,非常强大但比较复杂。幸运的是,生成较简单的根据类型进行参数化的模板使用还是可能的。与我们今天所讨论的主题无关的模板的细节以后再讨论,比如写我们自己的模板等。

        只要有可能,就应该使用数据结构与算法的通用设计,而不要将其代码化到特定的程序中。不要写一个只保存书对象的平衡二叉树结构。要使其通用化,使其可以保存任何类型的对象。这样的话,你就可以将其用于书店,音响店,操作系统,或者任何需要平衡二叉树的地方。该策略在标准库中大量存在,提供了用于任何类型的通用数据结构与算法。

        然而,同时要记住的是,实现通用数据结构相比于非通用数据结构的实现要花费更多的时间。要更多地考虑一下需求,要用许多不同的类型进行更广泛的通用实现的测试。如果你的数据结构过于特殊,可能会得不偿失,这样的话,是否考虑用一个更简单的非通用数据结构的实现会更好。所以,在编程领域,没有金科玉律,只有不断实践,在实践中总结出来的,什么是性价比最好 的,性价比最好的,就是你想要的最终的代码设计。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

王俊山IT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值