【问题标题】:Refactoring Service Layer classes重构服务层类
【发布时间】:2010-01-24 20:33:26
【问题描述】:

我的公司正在进行单元测试,我在重构服务层代码时遇到了一些麻烦。这是我编写的一些代码的示例:

public class InvoiceCalculator:IInvoiceCalculator
{
   public CalculateInvoice(Invoice invoice)
   {
      foreach (InvoiceLine il in invoice.Lines)
      {
          UpdateLine(il);
      }
      //do a ton of other stuff here
   }

   private UpdateLine(InvoiceLine line)
   {
      line.Amount = line.Qty * line.Rate;
      //do a bunch of other stuff, including calls to other private methods
   }
}

在这个简化的例子中(它从一个有 1 个公共方法和大约 30 个私有方法的 1,000 行类减少),我的老板说我应该能够分别测试我的 CalculateInvoice 和 UpdateLine(UpdateLine 实际上调用了 3 个其他私有方法,并执行数据库调用)。但是我该怎么做呢?他建议的重构对我来说似乎有点令人费解:

//Tiny part of original code
public class InvoiceCalculator:IInvoiceCalculator
{
   public ILineUpdater _lineUpdater;

   public InvoiceCalculator (ILineUpdater lineUpdater)
   {
      _lineUpdater = lineUpdater;
   }

   public CalculateInvoice(Invoice invoice)
   {
      foreach (InvoiceLine il in invoice.Lines)
      {
          _lineUpdater.UpdateLine(il);
      }
      //do a ton of other stuff here
   }
}

public class LineUpdater:ILineUpdater
{
   public UpdateLine(InvoiceLine line)
   {
      line.Amount = line.Qty * line.Rate;
      //do a bunch of other stuff
   }
}

我可以看到依赖关系现在是如何被破坏的,我可以测试这两个部分,但这也会从我的原始类中创建 20-30 个额外的类。我们只在一个地方计算发票,所以这些部分真的不能重复使用。这是进行此更改的正确方法,还是建议我做一些不同的事情?

谢谢!

杰斯

【问题讨论】:

    标签: c# unit-testing refactoring


    【解决方案1】:

    这是Feature Envy的一个例子:

    line.Amount = line.Qty * line.Rate;
    

    它应该看起来更像:

      var amount = line.CalculateAmount();
    

    大量的小类并没有什么问题,这与可重用性无关,而与适应性有关。当您有许多单一职责类时,更容易查看系统的行为并在您的需求发生变化时对其进行更改。大类的职责交织在一起,很难改变。

    【讨论】:

    • 我们所有的实体对象基本上都是DTO,所以所有的业务逻辑都在服务类中。但是发票行计算器中还有很多其他的逻辑,我只是​​展示了一个。
    【解决方案2】:

    IMO 这一切都取决于 UpdateLine() 方法的“重要性”。如果它只是一个实现细节(例如,它可以很容易地内联在 CalculateInvoice() 方法中,并且它们唯一会损害的是可读性),那么您可能不需要将它与主类分开进行单元测试。

    另一方面,如果 UpdateLine() 方法对业务逻辑有一些价值,如果您可以想象需要独立于类的其余部分更改此方法(并因此单独测试)的情况,那么您应该继续将其重构为单独的 LineUpdater 类。

    您可能不会以这种方式结束 20-30 个类,因为大多数私有方法实际上只是实现细节,不值得单独测试。

    【讨论】:

      【解决方案3】:

      嗯,你的老板在单元测试方面走的更正确:
      他现在能够在不测试 UpdateLine() 函数的情况下测试 CalculateInvoice()。他可以传递模拟对象而不是真正的 LineUpdater 对象,并且只测试CalculateInvoice(),而不是一大堆代码。
      这样对吗?这取决于。你的老板想要进行真正的单元测试。第一个示例中的测试不是单元测试,而是集成测试。

      在集成测试之前进行单元测试有什么优势?
      1) 单元测试允许您只测试一种方法或属性,而不受其他方法/数据库等的影响。
      2)第二个优势 - 单元测试执行得更快(例如,您说 UpdateLine 使用数据库),因为它们不测试所有嵌套方法。嵌套方法可以是数据库调用,因此如果您有数千个测试,您的测试可能会运行缓慢(几分钟)。
      3)第三个优点:如果您的方法进行数据库调用,那么有时您需要设置数据库(用测试所需的数据填充它)并且这并不容易 - 也许您必须编写几页代码才能准备数据库进行测试。通过单元测试,您可以将数据库调用与被测试的方法分开(使用模拟对象)。

      但是!我并不是说单元测试更好。他们只是不同。正如我所说,单元测试允许您单独快速地测试一个单元。集成测试更容易,并允许您测试不同方法和层的联合工作的结果。老实说,我更喜欢集成测试:)

      另外,我有几个建议给你:
      1)我不认为有金额字段是一个好主意。金额字段似乎是额外的,因为它的值可以基于其他 2 个公共字段来计算。如果您仍然想这样做,我会将其作为一个返回 Qty * Rate 的只读属性。
      2) 通常,拥有一个包含 1000 行的类可能意味着它的设计很糟糕,应该重构。

      现在,我希望你能更好地了解情况并做出决定。另外,如果你了解情况,你可以和你的老板谈谈,你们可以一起决定。

      【讨论】:

        【解决方案4】:

        是的,不错。我不确定 InvoiceLine 对象是否还包含一些逻辑,否则您可能还需要一个 IInvoiceLine。 我有时也有同样的问题。一方面,您想正确地做事并对代码进行单元测试,但是当涉及到数据库调用和文件写入时,它会导致大量额外的工作来设置所有测试对象的第一个测试,这些测试对象在文件写入和数据库 io 时介入发生,接口,断言,您还想测试数据层不包含任何错误。因此,比“单元”更“过程”的测试通常更容易构建。 如果您的项目(将来)将发生很大变化并且此代码有很多依赖项(可能其他程序读取文件或数据库数据),那么对代码的所有部分进行可靠的单元测试会很好,而且投资时间是值得的。 但如果这个项目是,就像我最近的客户说的那样'让我们让它上线,也许明年我们会稍微调整一下,明年会有新的东西',那么我就不会那么难得到所有单元测试启动并运行。

        米歇尔

        【讨论】:

          【解决方案5】:

          你老板的例子在我看来是合理的。

          在针对任何场景进行设计时,我尝试牢记的一些关键注意事项是:

          1. 单一职责原则

            一个类应该只因一个原因而改变。

          2. 每个新类是否证明了它的存在

            创建类只是为了它,还是封装了有意义的逻辑部分?

          3. 您能否单独测试每段代码?

          在您的场景中,仅查看名称,您似乎就偏离了单一职责 - 您有一个 IInvoiceCalculator,但该类还负责更新 InvoiceLines。您不仅使测试更新行为变得非常困难,而且您现在需要在计算业务规则更改时更改您的 InvoiceCalculator 类当有关更新的规则更改时。

          然后是关于更新逻辑的问题 - 逻辑是否证明单独的类是合理的?这真的取决于,如果不看代码就很难说,但你的老板想要测试该逻辑这一事实肯定表明它不仅仅是一个简单的对数据层的线性调用。

          您说这种重构创建了大量额外的类,(我认为您的意思是跨所有业务实体,因为在您的示例中我只看到几个新类及其接口)但您必须考虑你从中得到什么。看起来您获得了代码的完全可测试性,能够单独引入新的计算和新的更新逻辑,并且更清晰地封装了独立的业务逻辑。

          上面的收益当然要经过成本效益分析,但既然你的嘘声要求他们,听起来他很高兴他们会得到回报,而不是额外的实施工作这样的代码。

          关于独立测试的最后一点,也是你老板设计的方式的一个关键好处——你的公共方法越接近实际工作的代码,就越容易注入存根或模拟对于系统中未测试的部分。例如,如果您正在测试一个调用数据层的更新方法,您不想测试数据层,因此您通常会注入一个模拟。如果您需要首先通过所有计算器逻辑传递模拟数据层,那么您的测试设置将会复杂得多,因为模拟现在需要满足许多其他潜在要求,与实际测试无关。

          虽然这种方法最初是额外的工作,但我想说的是,大部分工作是考虑设计的时间,在此之后,在您熟悉更多基于注入的代码风格之后,以这种方式构建的软件的原始实现时间实际上是可比的。

          【讨论】:

            【解决方案6】:

            您的 hoss 方法是依赖注入的一个很好的例子,并且这样做可以让您使用模拟 ILineUpdater 来有效地进行测试。

            【讨论】:

              猜你喜欢
              • 2014-04-13
              • 2014-02-05
              • 1970-01-01
              • 2016-08-11
              • 2012-02-12
              • 1970-01-01
              • 2016-10-12
              • 1970-01-01
              • 2011-02-06
              相关资源
              最近更新 更多