如何避免类自用



我有以下类:

public class MyClass
{
    public void deleteOrganization(Organization organization)
    {
        /*Delete organization*/
        /*Delete related users*/
        for (User user : organization.getUsers()) {
            deleteUser(user);
        }
    }
    public void deleteUser(User user)
    {
        /*Delete user logic*/
    }
}

此类表示一种自用,因为它的公共方法deleteOrganization使用其其他公共方法deleteUser。就我而言,此类是我开始添加单元测试的遗留代码。因此,我首先针对第一种方法添加了一个单元测试deleteOrganization,最终发现该测试已扩展到也测试deleteUser方法。

问题

问题是这个测试不再是孤立的(它应该只测试deleteOrganization方法(。我不得不处理与deleteUser方法相关的不同条件,以便通过测试,这大大增加了测试的复杂性。

溶液

解决方案是监视被测试的类和存根deleteUser方法:

@Test
public void shouldDeleteOrganization()
{
    MyClass spy = spy(new MyClass());
    // avoid invoking the method
    doNothing().when(spy).deleteUser(any(User.class));
    // invoke method under test
    spy.deleteOrganization(new Organization());
}

新问题

虽然前面的解决方案解决了这个问题,但不建议这样做,因为spy方法的javadoc指出:

像往常一样,您将阅读部分模拟警告:对象 面向编程通过划分 复杂性为单独的、特定的 SRPy 对象。如何部分 模拟适合这种范式吗?好吧,它只是没有...部分模拟 通常意味着复杂性已转移到其他方法 在同一对象上。在大多数情况下,这不是您想要的方式 设计您的应用程序。

deleteOrganization方法的复杂性已移至deleteUser方法,这是由类自用引起的。除了 In most cases, this is not the way you want to design your application 语句之外,不建议使用此解决方案这一事实表明存在代码异味,并且确实需要重构来改进此代码。

如何删除这种自用?是否有可以应用的设计模式或重构技术?

类自用不一定是问题:我还不相信"它应该只测试deleteOrganization方法",除了个人或团队风格之外的任何原因。尽管将deleteUserdeleteOrganization保留为独立和孤立的单元很有帮助,但这并不总是可行或实用的 - 特别是当这些方法相互调用或依赖于一个共同状态时。测试的重点是测试"最小的可测试单元",这不要求方法独立或可以独立测试。

有几种选择,具体取决于您的需求以及您期望代码库如何发展。其中两个来自您的问题,但我在下面用优点重申它们。

  • 测试黑匣子。

    如果你把MyClass看作一个不透明的接口,你可能不会期望或要求deleteOrganization重复调用deleteUser,你可以想象实现的变化使得它不会这样做。(例如,将来的升级可能会让数据库触发器处理级联删除,或者单个文件删除或 API 调用可能会处理组织删除。

    如果您将deleteOrganizationdeleteUser的调用视为私有方法调用,那么您将测试MyClass的合约而不是其实现:创建一个包含一些用户的组织,调用该方法,并检查组织是否消失以及用户是否也消失了。它可能很冗长,但它是最正确和灵活的测试。

    如果您希望MyClass发生巨大变化,或者获得全新的替代实现,这可能是一个有吸引力的选择。

  • 将类水平拆分为 OrganizationDeleter 和 UserDeleter。

    为了使类更"SRPy"(即更好地符合单一责任原则(,正如 Mockito 文档所暗示的那样,您会看到deleteUserdeleteOrganization是独立的。通过将它们分成两个不同的类,您可以让 OrganizationDeleter 接受模拟 UserDeleter,从而消除对部分模拟的需要。

    如果您希望用户删除业务逻辑发生变化,或者您希望编写另一个 UserDeleter 实现,则这可能是一个有吸引力的选项。

  • 将类垂直拆分为 MyClass 和 MyClassService/MyClassHelper。

    如果低级别基础结构和高级调用之间存在足够的差异,则可能需要将它们分开。MyClass将保持deleteUserdeleteOrganization,执行所需的任何准备或验证步骤,然后对MyClassService中的原语进行一些调用。 deleteUser可能是一个简单的委托,而deleteOrganization可以在不调用其邻居的情况下调用服务。

    如果你有足够的低级调用来保证这种额外的分离,或者如果 MyClass 同时处理高级和低级问题,这可能是一个有吸引力的选择——特别是如果它是你之前发生的重构。不过,要小心避免果仁蜜饼代码的模式(太多的薄而可渗透的层,无法跟踪(。

  • 继续使用部分模拟。

    虽然这确实泄露了内部调用的实现细节,并且这确实违背了 Mockito 警告,但总的来说,部分模拟可以提供最好的务实选择。如果一个方法多次调用其同级,则内部方法可能是帮助程序还是对等方,可能没有明确定义,并且部分模拟将使您无需创建该标签。

    如果您的代码具有到期日期,或者具有足够的实验性,无法保证完整的设计,则这可能是一个有吸引力的选择。

(旁注:确实,除非MyClass是最终的,否则它可以对子类开放,这是使部分模拟成为可能的部分原因。如果你要记录deleteOrganization的总契约涉及对deleteUser的多次调用,那么创建一个子类或部分模拟将是完全公平的游戏。如果未记录,则它是实现详细信息,应按此处理。

deleteOrganization()的合同是否说该组织中的所有User也将被删除?
如果是这样,则必须至少保留部分删除用户逻辑deleteOrganization()因为类的客户端可能依赖于该功能。

在我进入选项之前,我还要指出这些删除方法中的每一个都是public的,并且类和方法都不是最终的。 这将允许某人扩展类并重写可能很危险的方法。

解决方案 1 - 删除组织仍必须删除其用户

考虑到我对压倒一切的危险的评论,我们拉出实际上从deleteUser()中删除用户的部分。 我假设公共方法deleteUser进行额外的验证,也许还会执行deleteOrganization()不需要的其他业务逻辑。

public void deleteOrganization(Organization organization)
{
    /*Delete organization*/
    /*Delete related users*/
    for (User user : organization.getUsers()) {
        privateDeleteUser(user);
    }
}
private void privateDeleteUser(User user){
   //actual delete logic here, without anything delete organization doesn't need
}

public void deleteUser(User user)
{
    //do validation
   privateDeleteUser(user);
  //perform any extra business locic
}

这将重用执行删除的实际代码,并避免子类更改deleteUser()行为不同的危险。

解决方案 2 - 不需要在 deleteOrganization(( 方法中删除用户

如果我们不需要一直从组织中删除用户,我们可以从 deleteOrganization(( 中删除这部分代码。 在我们也需要删除用户的少数地方,我们可以像现在删除组织一样执行循环。 由于它使用公共方法,因此只有任何客户端都可以调用它们。 我们也可以将逻辑提取到Service类中。

public void DeleteOrganizationService(){
   private MyClass myClass;
   ...
   public void delete(Organization organization)
   {
       myClass.deleteOrganization(organization);
        /*Delete related users*/
        for (User user : organization.getUsers()) {
            myClass.deleteUser(user);
        }
    }
 }

这是更新的MyClass

public void deleteOrganization(Organization organization)
{
    /*Delete organization only does not modify users*/
}
public void deleteUser(User user)
{
    /*same as before*/
}

不要监视被测试的类,扩展它并覆盖 deleteUser 以执行一些良性的事情——或者至少在你的测试控制下做一些事情。

Self Use of only override-able method is not proper design pattern.
Solution to above problem
You can eliminate a class’s self-use of overridable methods mechanically, without changing its behavior. Move the body of each overridable method to a private “helper method” and have each overridable method invoke its private helper method.
public class MyClass
{
    public void deleteOrganization(Organization organization)
    {
        /*Delete organization*/
        /*Delete related users*/
        for (User user : organization.getUsers()) {
            deleteUserHelper(user);
        }
    }
    public void deleteUser(User user)
    {
        deleteUserHelper(user);
    }
   private void deleteUserHelper(User user){
       /*delete user*/
   }
}

现在这个解决方案克服了可覆盖方法的自我使用

相关内容

  • 没有找到相关文章

最新更新