Java 如何避免班级自用

Java 如何避免班级自用,java,unit-testing,design-patterns,mocking,mockito,Java,Unit Testing,Design Patterns,Mocking,Mockito,我有以下课程: public class MyClass { public void deleteOrganization(Organization organization) { /*Delete organization*/ /*Delete related users*/ for (User user : organization.getUsers()) { deleteUser(user);

我有以下课程:

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
方法,这是由类自身使用引起的。除了在大多数情况下的
之外,不推荐使用此解决方案,这不是您想要设计应用程序的方式
语句,这表明存在代码气味,确实需要重构来改进此代码


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

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

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

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

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

这将重新使用执行删除的实际代码,并避免子类更改
deleteUser()
以改变行为的危险

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

如果我们不需要一直从组织中删除用户,我们可以从deleteOrganization()中删除该部分代码。在少数几个我们也需要删除用户的地方,我们可以像deleteOrganization现在那样执行循环。因为它使用公共方法,所以只有任何客户端才能调用它们。我们还可以将逻辑拉入
服务

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来做一些良性的事情——或者至少是在测试的控制下做一些事情。

类的自我使用不一定是个问题:我还不相信“它应该只测试deleteOrganization方法”,原因除了个人或团队风格。尽管将
deleteUser
deleteOrganization
保持为独立和孤立的单元是有帮助的,但这并不总是可行的,特别是当这些方法相互调用或依赖于一个公共状态时。测试的重点是测试“最小的可测试单元”——它不要求方法是独立的,也不要求它们可以独立测试

您有一些选择,这取决于您的需求以及您希望代码库如何发展。其中两个来自你的问题,但我在下面用优势重申它们

  • 测试黑盒。

    如果将MyClass视为一个不透明的接口,您可能不会期望或要求
    deleteOrganization
    重复调用
    deleteUser
    ,您可以想象实现会发生变化,以至于它不会这样做。(例如,将来的升级可能会有一个数据库触发器负责级联删除,或者单个文件删除或API调用可能负责组织删除。)

    如果您将
    deleteOrganization
    deleteUser
    的调用视为私有方法调用,那么您就只能测试MyClass的契约,而不是它的实现:创建一个有一些用户的组织,调用该方法,并检查该组织是否消失,用户是否也消失。它可能很冗长,但却是最正确、最灵活的测试

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

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

    正如Mockito文档所暗示的那样,为了使类更“SRPy”(即更好地符合),您将看到
    deleteUser
    deleteOrganization
    是独立的。通过将它们分为两个不同的类,您可以拥有组织
    public void deleteOrganization(Organization organization)
    {
        /*Delete organization only does not modify users*/
    }
    
    public void deleteUser(User user)
    {
        /*same as before*/
    }
    
    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*/
       }
    }