最近在重构色谱工作站程序,发现有几个类有重复性的代码(为了方便说明问题,去掉了不相关的代码):
class CAnalysisSample { protected: CAnalysisSample(CAnalysisSystem* pSystem):m_pSystem(pSystem){} public: inline CAnalysisSystem* GetSystem() { return m_pSystem; } inline const CAnalysisSystem* GetSystem() const { return m_pSystem; } private: CAnalysisSystem* m_pSystem; }; class CAnalysisMethod { protected: CAnalysisMethod(CAnalysisSystem* pSystem):m_pSystem(pSystem){} public: inline CAnalysisSystem* GetSystem() { return m_pSystem; } inline const CAnalysisSystem* GetSystem() const { return m_pSystem; } private: CAnalysisSystem* m_pSystem; }; class CAnalysisTask { protected: CAnalysisTask(CAnalysisSystem* pSystem):m_pSystem(pSystem){} public: inline CAnalysisSystem* GetSystem() { return m_pSystem; } inline const CAnalysisSystem* GetSystem() const { return m_pSystem; } private: CAnalysisSystem* m_pSystem; }; class CAnalysisInstrument { protected: CAnalysisInstrument(CAnalysisSystem* pSystem):m_pSystem(pSystem){} public: inline CAnalysisSystem* GetSystem() { return m_pSystem; } inline const CAnalysisSystem* GetSystem() const { return m_pSystem; } private: CAnalysisSystem* m_pSystem; };
很容易可以看出,对成员变量m_pSystem的声明和初始化以及两个get操作,完全都是一样的;并且这种完全一样的代码,还出现在了四个地方。
重复是可耻的,要遵循DRY。自然而然地,就是考虑将这些操作提取出来,放到一个公共类中(注意,我暂且还没说是公共基类)。
首先,要为这个公共类取一个名字,叫CHasAAnalysisSystemPointer如何?听起来不错,但太明确了。你是有一个系统,但你能保证永远都是系统Pointer?为什么不能是系统ref?抽象一点,或者从业务上理解,叫CHasAAnalysisSystem已足够。那能不能再简单点,叫CHasASystem?这个名字太过简单和抽象了。啥,等等,你说这个名字太过简单和抽象了?那为什么在上面的四个类中,get函数的名称是GetSystem而不是GetAnalysisSystem?好吧,能够提出这个问题的话,说明你有心了。这个问题的解答很简单,想想实际的使用情况即可:是谁在调用GetSystem?是上面的四个类的对象。具体点,以CAnalysisSample为例,大概是这样:
CAnalysisSample* pSample = ...; pSample->GetSystem()...
如果pSample的声明处距离GetSystem的调用处很近,我们就很容易看到pSample是CAnalysisSample类型,有这个上下文为基础,你还会下意识地觉得GetSystem会返回其它System吗?调用CAnalysisSample的GetSystem,却返回FileSystem或FinancialSample?哦,简直难以想象,除非CAnalysisSample也和FileSystem或FinancialSample有所关联。也只有在这种情况下,才有必要将GetSystem更名为GetAnalysisSystem。如果你觉得上面的解释仍然不具说服力,那我就再打个比方:假如你是一名老师,需要统计学生的数学成绩。然而不幸的是,数学成绩表丢了,你因此只好按着学号依次询问学生的成绩。你会怎么问呢?
XXX,你多少分?
XXX,你数学多少分?
这两种问法都没有错。但为节省口水,老师一般会问前者(甚至连“XXX,你”这几个字都省了)。编码当然不需要节省口水,但需要节省篇幅。如果一件事能用两个字说清楚,就不要用三个字,否则多少会加重阅读者的负担。记住,我们捣鼓的是程序代码,而不是写小说,按字收费。
如果pSample的声明处距离GetSystem的调用处不近,那可以考虑重构一下,让它近:
CAnalysisSample* pSample = ...; ...//一大堆代码 pSample->GetSystem()//有上面的一大堆代码相隔,看不到pSample的声明
重构为:
CAnalysisSample* pSample = ...; CAnalysisSystem* pSystem = pSample->GetSystem(); ...//一大堆代码 pSystem->...//虽有上面的一大堆代码相隔,但现在也没关系了
或者重构为:
CAnalysisSample* pSample = ...; BigBlock(pSample);//提取一大堆代码到一个或几个函数中 pSample->GetSystem()->...//根据需要也可考虑提取到函数UsingAnalysisSystem(pSample->GetSystem())
扯远了,回到名字问题上来。
那为什么说CHasASystem太过简单和抽象了呢?这是因为它和GetSystem不同:GetSystem是一个成员函数名称,被调用时必然会有一个隐含的上下文;而类,在被使用时,隐含的上下文只有一个命名空间。如果CHasASystem和CAnalysisSample是处于一个命名空间,那......说明CAnalysisSample等需要重构(更名为CSample,并且命名空间应当为Analysis)了。如果都是在全局命名空间,那好吧,CHasASystem中的System到底是什么系统呢?有什么隐含的语境或线索吗?什么地方能够得知这个System是AnalysisSystem而不是操作系统、文件系统、财务系统、人事系统...?
好了,名字确定为CHasAAnalysisSystem,下面来实现它。这个是简单的,拷贝上面四个类中任意一个,稍作修改即可:
class CHasAAnalysisSystem { protected: CHasAAnalysisSystem(CAnalysisSystem* pSystem); public: CAnalysisSystem* GetSystem() { return m_pSystem; } const CAnalysisSystem* GetSystem() const { return m_pSystem; } private: CAnalysisSystem* m_pSystem; };
现在,来考虑一下,是让上面的四个类继承CHasAAnalysisSystem,还是拥有一个CHasAAnalysisSystem对象?
有个设计原则,优先使用组合,其次才是继承。
我们先使用组合看看,更改后的代码大概如下(以CAnalysisSample为例):
class CAnalysisSample { protected: CAnalysisSample(CAnalysisSystem* pSystem):m_oSystem(pSystem){} public: inline CHasAAnalysisSystem& GetSystem() { return m_oSystem; } inline const CHasAAnalysisSystem& GetSystem() const { return m_oSystem; } private: CHasAAnalysisSystem m_oSystem; };
…… …… …… …… …… ……
你的舌头是不是和上面的省略号一样,无语了?
在使用组合的情况下,无论代码怎么写,都逃不了原来的样式,并且代码量只会比原来多,而不是少,根本没起到重用的目的。
组合无用,就只能继承了。(如果还要向CHasAAnalysisSystem中添加其它的功能,这个时候就可以考虑使用组合了)
继承有三种,public、protected和private。我们需要让CHasAAnalysisSystem的派生类继承接口和实现(get函数)。我们似乎应当理所当然地选择public继承:
class CAnalysisSample : public CHasAAnalysisSystem { protected: CAnalysisSample(CAnalysisSystem* pSystem):CHasAAnalysisSystem(pSystem){} };
代码明显少了,确实起到了简化和重用的目的,而且可读性也没有降低。似乎很完美了。
等等,等等。回忆一下继承的相关知识:public表示is-a;private:use-a(用...实现);protected不知道用途。还有一个原则:继承是为了多态,如果不是为了多态,请使用组合。还有句话:继承分为接口继承和实现继承。
我们干了什么?我们需要CHasAAnalysisSystem的接口,也需要CHasAAnalysisSystem的实现(注意,这个和实现继承是两回事),这没有问题(刚好可以用public继承解决,当然,也只能用public继承解决)。但是,如果说CAnalysisSample is a CHasAAnalysisSystem,这个我绝对不能接受(概念上完全不通)。我相信所有看到这篇文章的人也一样不能接受。怎么办?实现上必须使用public,概念上却又不能使用public......
静下心,让我们回到起始点。我们搞出CHasAAnalysisSystem类的目的,是为了简化四个类的代码,实现重用。为此,我们使用了public继承,而在设计概念上,public继承表示is-a关系。可以肯定的有两点:1.CAnalysisSample is not a CHasAAnalysisSystem!2. 我们需要使用CHasAAnalysisSystem简化代码,实现重用。
要解决这个问题,只有一种办法:不使用public继承,但仍然可以将CHasAAnalysisSystem的接口暴露为public。万幸的是,C++提供了这个功能:
class CAnalysisSample : private CHasAAnalysisSystem { protected: AnalysisSample(CAnalysisSystem* pSystem):CHasAAnalysisSystem(pSystem){} public: using CHasAAnalysisSystem::GetSystem; };
这个方法不大好。每当向CHasAAnalysisSystem中添加接口函数,也就要相应地向四个类中添加using声明,重用目的大打折扣。
如果广大读者有什么好主意,请不吝相告,再次先表示感谢!