“面向对象”反模式

2020-05-28 14:21:13

对于那些只读以上内容的人:我不是说所有的面向对象都是坏的!OOP,特别是经典的多态OOP,在实际代码中有一个当之无愧的位置。我要谈一谈我经常看到的一个非常具体的反模式:对应该是简单的自由函数的东西使用类。

我最初在Code Review StackExchange(2018年2月)上写了关于这段特定代码的文章。2018年4月,我根据同样的材料做了一次无录音的闪电演讲。

如果这类东西对你有吸引力,我建议你尽可能买一本Kernighan和Plauger的“编程风格的元素”(1974,1978年再版)。

在特定的学术环境中学习过C++的学生通常会抱着“一切都是对象”的心态来对待它。假设分配给他们的任务是计算值,那么他们的第一步是使用成员函数vc.culteResult()创建一个ValueComputer对象。

例如:一名学生被要求使用动态编程来计算一个矩形的不同多米诺骨牌拼图的数量。学生写道:

int main(){DominoTilingCounter tc(4,7);//在4x7矩形std::cout<;<;tc.count()<;<;';\n';;}。

在框定问题之后,他们继续实现DominoTilingCounter类。聪明的学生甚至添加了Mememory,这样count()成员函数在第二次调用时就不会太慢:

class DominoTilingCounter{int Height,width;bool Done=false;int tilingCount;int ComputeCount(int h,int w,std::string_view prevedRow,int rowIdx){[.递归解决方案省略.]}public:显式DominoTilingCounter(int h,int w):Height(H),width(W){if(h=0||w=0||(w*h)%2!=0){。}}int count(){if(!Done){tilingCount=ComputeCount(Height,Width,";";,0);Done=true;}return tilingCount;}};

不幸的是,这段代码没有通过常量正确性测试:count()成员函数听起来应该是非修改的,但实际上它需要更新成员数据,因此不能是常量。

看:当您构造DominoTilingCounter对象TC时,它专门用于计算tc.count(),对吗?TC没有其他用途了?

再说一次:当您构造DominoTilingCounter对象时,它专门用于计算tc.count()。

class DominoTilingCounter{int Height,width;int tilingCount;int ComputeCount(int h,int w,std::string_view prevedRow,int rowIdx){[.递归解决方案省略.]}public:显式DominoTilingCounter(int h,int w):Height(H),width(W){if(h=0||w=0||w=0||(w*h)%2!=0){tilingCount=0。}}int count()const{return tilingCount;}};

它消除了Done数据成员,其全部目的是跟踪该空状态。(在C++17中,我们可能会为此使用std::Optional;但是现在我们不必这样做了!)。

事实上,私有数据成员Height和Width现在也未使用。事实证明,我们使用它们只是为了将数据从构造函数传递到count()方法中的计算;现在计算在构造函数中进行,我们不再需要这些数据成员。我们的代码急剧缩减。

在原始代码中,学生的ComputeCount成员函数碰巧接受w,并且具有函数参数,而不是从Height和Width数据成员中读取它们。这是一个幸运的意外:ComputeCount没有使用DominoTilingCounter对象的任何数据成员,因此我们可以将其标记为静态。我们的代码现在如下所示:

class DominoTilingCounter{int tilingCount;static int culteCount(int h,int w,std::string_view prevRow,int rowIdx){if(h==0||w==0|(w*h)%2!=0){return 0;}[.递归解决方案省略.]}public:显式DominoTilingCounter(int h,int w){tilingCount=ComputeCount(h,w,&#

最后一步是观察到整个类除了将赋值包装到int之外什么也不做!

int countDominoTilingsImpl(int h,int w,std::string_view premisRow,int rowIdx){if(h==0||w==0|(w*h)%2!=0){return 0;}[.递归解决方案省略.]}int countDominoTilings(int h,int w){return countDominoTilingsImpl(h,w,";";,0。tc<;<;&39;\n&39;;}。

不再上课,不再担心Const,不再担心记忆(不管是好是坏,这都成了学生的问题)。我们最初的DominoTilingCounter对象不是线程安全的,但是现在我们也不必担心这一点。我们的代码大约短了十几行。

再说一次,这并不是说所有的课程都不好!事实上,这里讨论的反模式与生成器模式非常接近,生成器模式没有什么问题--只要需要,就是这样。我要说的是:

当您必须计算值时,不要编写ValueComputer类。改为编写COMPUTE_VALUE函数。