如何将代码重构为一些通用代码?


16

背景

我正在进行中的C#项目。我不是C#程序员,主要不是C ++程序员。因此,我基本上被分配了简单的重构任务。

该代码是一团糟。这是一个巨大的项目。由于我们的客户要求频繁发布具有新功能和错误修复的程序,因此所有其他开发人员在编码时都被迫采取暴力手段。该代码极难维护,所有其他开发人员都同意。

我不是在这里辩论他们是否做对了。在重构时,我想知道我是否以正确的方式进行操作,因为重构后的代码似乎很复杂!这是我作为简单示例的任务。

问题

有六类:ABCDEF。所有的类都有一个功能ExecJob()。所有六个实现都非常相似。基本上,起初A::ExecJob()是写的。然后需要一个稍有不同的版本,该版本B::ExecJob()通过对的复制粘贴修改进行实现A::ExecJob()。当需要另一个略有不同的版本时,C::ExecJob()编写此类代码。所有六个实现都有一些通用代码,然后有一些不同的代码行,再有一些通用代码,依此类推。这是实现的一个简单示例:

A::ExecJob()
{
    S1;
    S2;
    S3;
    S4;
    S5;
}

B::ExecJob()
{
    S1;
    S3;
    S4;
    S5;
}

C::ExecJob()
{
    S1;
    S3;
    S4;
}

SN一组完全相同的语句在哪里。

为了使它们通用,我创建了另一个类并将通用代码移到函数中。使用参数控制应执行的语句组:

Base::CommonTask(param)
{
    S1;
    if (param.s2) S2;
    S3;
    S4;
    if (param.s5) S5;
}

A::ExecJob() // A inherits Base
{
    param.s2 = true;
    param.s5 = true;
    CommonTask(param);
}

B::ExecJob() // B inherits Base
{
    param.s2 = false;
    param.s5 = true;
    CommonTask(param);
}

C::ExecJob() // C inherits Base
{
    param.s2 = false;
    param.s5 = false;
    CommonTask(param);
}

请注意,此示例仅使用三个类和过分简化的语句。实际上,CommonTask()通过所有这些参数检查,该函数看起来非常复杂,并且还有更多的语句。同样,在实际代码中,有几个CommonTask()外观函数。

尽管所有实现都共享通用代码,并且ExecJob()功能看起来更可爱,但仍然存在两个困扰我的问题:

  • 对于中的任何更改CommonTask(),都需要测试所有六个(将来可能会更多)功能。
  • CommonTask()已经很复杂了。随着时间的推移,它将变得更加复杂。

我做对了吗?


马丁·福勒(Martin Fowler)的《重构》一书中有许多用于重构代码的特定技术,这些技巧可能对您有用。
艾伦2014年

Answers:


14

是的,您绝对走对了!

以我的经验,我注意到当事情变得复杂时,这些变化会一步步发生。您要做的是演化过程(或重构过程)中的步骤1。这是步骤2和步骤3:

第2步

class Base {
  method ExecJob() {
    S1();
    S2();
    S3();
    S4();
    S5();
  }
  method S1() { //concrete implementation }
  method S3() { //concrete implementation }
  method S4() { //concrete implementation}
  abstract method S2();
  abstract method S5();
}

class A::Base {
  method S2() {//concrete implementation}
  method S5() {//concrete implementation}
}

class B::Base {
  method S2() { // empty implementation}
  method S5() {//concrete implementation}
}

class C::Base {
  method S2() { // empty implementation}
  method S5() { // empty implementation}
}

这是“模板设计模式”,它是重构过程中的第一步。如果基类发生更改,则子类(A,B,C)不需要受影响。您可以相对轻松地添加新的子类。但是,从上图可以立即看到抽象已损坏。是否需要“空实施”是一个很好的指标;它表明您的抽象有问题。对于短期而言,这可能是一个可接受的解决方案,但似乎有更好的解决方案。

第三步

interface JobExecuter {
  void executeJob();
}
class A::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S2();
     helper->S3();
     helper->S4();
     helper->S5();
  }
}

class B::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S3();
     helper->S4();
     helper->S5();
  }
}

class C::JobExecuter {
  void executeJob(){
     helper = new Helper();
     helper->S1();
     helper->S3();
     helper->S4();
  }
}

class Base{
   void ExecJob(JobExecuter executer){
       executer->executeJob();
   }
}

class Helper{
    void S1(){//Implementation} 
    void S2(){//Implementation}
    void S3(){//Implementation}
    void S4(){//Implementation} 
    void S5(){//Implementation}
}

这是“策略设计模式”,似乎很适合您的情况。执行作业的策略不同,每个类(A,B,C)的执行方式也不同。

我确信此过程中有第4步或第5步,或者有很多更好的重构方法。但是,这一步将使您消除重复的代码,并确保所做的更改已本地化。


我在“步骤2”中概述的解决方案中看到的主要问题是S5的具体实现存在两次。
2011年

1
是的,不会消除代码重复!这是抽象不起作用的另一个指标。我只是想将第2步放在那儿,以展示我对流程的看法;逐步寻找更好的方法。
Guven

1
+1非常好的策略(我不是在谈论这种模式)!
Jordão酒店

7

您实际上在做正确的事。我说这是因为:

  1. 如果您需要更改公共任务功能的代码,则无需在所有6个类中都进行更改,如果您不在公共类中编写代码,则这6个类将包含该代码。
  2. 代码行数将减少。

3

您会看到这种代码与事件驱动的设计(尤其是.NET)共享很多。最可维护的方法是将您的共享行为尽可能地小。

让高级代码重用一堆小方法,将高级代码放在共享库之外。

叶/混凝土实现中将包含很多样板。别着急,没关系。所有这些代码都是直接的,易于理解。当内容中断时,您有时需要重新安排它,但是它很容易更改。

您会在高级代码中看到很多模式。有时它们是真实的,大多数时候不是。上面五个参数的“配置” 看起来很相似,但是却不一样。它们是三种完全不同的策略。

还需要注意的是,您可以通过合成来完成所有这些操作,而不必担心继承。您将减少耦合。


3

如果我是您,我可能会在开始时再增加1个步骤:基于UML的研究。

重构将所有通用部分融合在一起的代码并不总是最好的选择,听起来更像是一种临时解决方案,而不是一种好的方法。

绘制一个UML方案,使事情简单但有效,请记住有关项目的一些基本概念,例如“该软件应该做什么?” “使该软件抽象,模块化,可扩展……等等的最佳方法是什么?” “我如何才能最好地实现封装?”

我只是在说:现在不关心代码,只需要关心逻辑,当您有了清晰的逻辑时,其余所有事情就可以变成一件真正容易的事,最终这一切您所面对的问题仅仅是由错误的逻辑引起的。


在进行任何重构之前,这应该是第一步。直到对代码的理解足以被映射(uml或其他一些荒野的地图)之前,重构将在黑暗中进行。
Kzqai 2015年

3

无论走到哪里,第一步都应该将看似较大的方法A::ExecJob分解为较小的部分。

因此,代替

A::ExecJob()
{
    S1; // many lines of code
    S2; // many lines of code
    S3; // many lines of code
    S4; // many lines of code
    S5; // many lines of code
}

你得到

A::ExecJob()
{
    S1();
    S2();
    S3();
    S4();
    S5();
}

A:S1()
{
   // many lines of code
}

A:S2()
{
   // many lines of code
}

A:S3()
{
   // many lines of code
}

A:S4()
{
   // many lines of code
}

A:S5()
{
   // many lines of code
}

从这里开始,有许多可能的方法。我的看法:将A作为您的类的基类和ExecJob虚拟化,并且可以轻松地创建B,C ...,而无需过多地复制粘贴-只需将ExecJob(现在是五层)替换为经过修改的版。

B::ExecJob()
{
    S1();
    S3();
    S4();
    S5();
}

但是,为什么有这么多的课程呢?也许您可以将它们全部替换为一个具有构造函数的类,该类可以告知哪些动作是必需的ExecJob



1

首先,你应该确保继承是真的在这里正确的工具的工作-只因为你需要为你的类使用功能的公共场所A,以F并不意味着一个公共基类是这里的正确的事情-有时单独帮手上课做得更好。可能是,可能不是。这取决于A到F与您的普通基类之间是否存在“ is-a”关系,这在人工名称AF中是不可能的。在这里,您可以找到有关此主题的博客文章。

假设您确定通用基类在您的情况下是正确的。然后我会做的第二件事是要确保你的代码片段S1至S5分别在单独的方法来实现S1(),以S5()你的基类的。然后,“ ExecJob”函数应如下所示:

A::ExecJob()
{
    S1();
    S2();
    S3();
    S4();
    S5();
}

B::ExecJob()
{
    S1();
    S3();
    S4();
    S5();
}

C::ExecJob()
{
    S1();
    S3();
    S4();
}

如您现在所见,由于S1到S5只是方法调用,不再有代码块,代码重复几乎被完全删除,并且您不再需要任何参数检查,从而避免了可能增加的复杂性的问题除此以外。

最后,但仅作为第三步(!),您可能会考虑将所有这些ExecJob方法组合到您的基类中,在这些基类中,这些部分的执行可以通过参数,按照您建议的方式或通过使用模板方法模式。您必须根据实际代码来决定自己是否值得为此付出努力。

但是恕我直言,将大方法分解为小方法的基本技术对于避免代码重复比应用模式更为重要。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.