如何避免类自用 [英] How to avoid class self-use

查看:53
本文介绍了如何避免类自用的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我有以下课程:

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 方法.

This class represents a self-use in that its public method deleteOrganization uses its other public method deleteUser. In my case this class is a legacy code against which I started adding unit tests. So I started by adding a unit test against the first method deleteOrganization and ended up figuring out that this test has been extended to also test the deleteUser method.

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

The problem is that this test is not isolated anymore (it should only test the deleteOrganization method). I was obliged to treat the different conditions related to deleteUser method in order to pass it so to pass the test, which has hugely increased the complexity of the test.

解决方案是监视被测类并存根 deleteUser 方法:

The solution was to spy the class under test and stub deleteUser method:

@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声明:

New Problem

Though the previous solution solves the problem, it's not recommended as the javadoc of the spy method states:

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

As usual you are going to read the partial mock warning: Object oriented programming is more less tackling complexity by dividing the complexity into separate, specific, SRPy objects. How does partial mock fit into this paradigm? Well, it just doesn't... Partial mock usually means that the complexity has been moved to a different method on the same object. In most cases, this is not the way you want to design your application.

deleteOrganization 方法的复杂性已移至 deleteUser 方法,这是由类自用引起的.不推荐这种解决方案的事实,除了在大多数情况下,这不是您设计应用程序的方式语句,表明存在代码异味,确实需要重构改进此代码.

The complexity of the deleteOrganization method has been moved to deleteUser method and this is caused by the class self-use. The fact that this solution is not recommended, in addition to the In most cases, this is not the way you want to design your application statement, suggests that there is a code smell, and refactoring is indeed needed to improve this code.

如何去掉这个自用?是否有可以应用的设计模式或重构技术?

How to remove this self-use? Is there a design pattern or a refactoring technique that can be applied?

推荐答案

类自用不一定是问题: 我还不确定它应该只测试 deleteOrganization 方法出于个人或团队风格以外的任何原因.尽管将 deleteUserdeleteOrganization 保持在独立和隔离的单元中是有帮助的,但这并不总是可行或实用的——尤其是如果这些方法相互调用或依赖于一个共同状态.测试的重点是测试最小的可测试单元"——它不要求方法是独立的,也不需要它们可以独立测试.

Class self-use isn't necessarily a problem: I'm not yet convinced that "it should only test the deleteOrganization method" for any reason other than personal or team style. Though it's helpful to keep deleteUser and deleteOrganization into independent and isolated units, that isn't always possible or practical—especially if the methods call one another or rely on a piece of common state. The point of testing is to test the "smallest testable unit"—which does not require that methods are independent or that they can be tested independently.

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

You have a few choices, depending on your needs and how you expect your codebase to evolve. Two of these are from your question, but I'm restating them below with advantages.

  • 测试黑盒.

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

If you were to treat MyClass as an opaque interface, you likely wouldn't expect or require deleteOrganization to repeatedly call deleteUser, and you can imagine the implementation changing such that it doesn't do so. (Maybe a future upgrade would have a database trigger take care of a cascading delete, for instance, or a single file deletion or API call might take care of your organizational delete.)

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

If you treat deleteOrganization's call to deleteUser as a private method call, then you're left testing MyClass's contract and not its implementation: Create an organization with some users, call the method, and check that the organization is gone and that the users are too. It may be verbose, but it is the most correct and flexible test.

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

This may be an attractive option if you expect your MyClass to change drastically, or to get a whole new replacement implementation.

将类水平拆分为 OrganizationDeleter 和 UserDeleter.

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

To make the class more "SRPy" (i.e. conforming better to the Single Responsibility Principle), as the Mockito documentation alludes, you'd see that deleteUser and deleteOrganization are independent. By separating those out into two different classes, you could have the OrganizationDeleter accept a mock UserDeleter, eliminating your need for a partial mock.

如果您希望删除用户的业务逻辑会有所不同,或者您希望编写另一个 UserDeleter 实现,这可能是一个有吸引力的选择.

This may be an attractive option if you expect the user-deleting business logic to vary, or you want to expect to write another UserDeleter implementation.

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

如果底层基础设施和高层调用之间存在足够的差异,您可能需要将它们分开.MyClass 将保留 deleteUserdeleteOrganization,执行任何需要的准备或验证步骤,然后对 MyClassService 中的原语进行几次调用.deleteUser 可能是一个简单的委托,而 deleteOrganization 可以在不调用其邻居的情况下调用服务.

If there's enough difference between the low-level infrastructure and high-level calls, you may want to separate them out. MyClass would keep deleteUser and deleteOrganization, perform whatever preparation or validation steps are needed, and then make a few calls to the primitives in MyClassService. deleteUser would probably be a straightforward delegate, whereas deleteOrganization could call the service without invoking its neighbor.

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

If you have enough low-level calls to warrant this extra separation, or if MyClass deals with both high-level and low-level concerns, this may be an attractive option—especially if it's a refactor that's occurred to you before. Be careful to avoid the pattern of baklava code, though (too many thin, permeable layers than you could ever keep track of).

继续使用部分模拟.

虽然这确实泄漏了内部调用的实现细节,并且这确实违反了 Mockito 警告,但总的来说,部分模拟可以提供最佳的实用选择.如果您有一个方法多次调用其兄弟方法,则可能无法明确定义内部方法是辅助方法还是对等方法,部分模拟将使您不必制作该标签.

Though this does leak the implementation detail of internal calls, and this does go against the Mockito warning, on balance partial mocks could provide the best pragmatic choice. If you have one method calling its sibling multiple times, it may not be well-defined whether the inner method is a helper or a peer, and a partial mock would save you from having to make that label.

如果您的代码有有效期,或者是实验性的,不足以保证完整的设计,这可能是一个有吸引力的选择.

This may be an attractive option if your code has an expiration date, or is experimental enough not to warrant a full design.

(旁注:确实,除非 MyClass 是最终的,否则它对子类化是开放的,这是使部分模拟成为可能的一部分.如果您要记录 deleteOrganization 的一般合同涉及多个调用 deleteUser,那么创建子类或部分模拟将是完全公平的游戏.如果它没有记录,那么它是一个实现细节,应该这样对待.)

(Side note: It's true that unless MyClass is final it is open to subclassing, which is part of what makes partial mocks possible. If you were to document that deleteOrganization's general contract involved multiple calls to deleteUser, then it would be entirely fair game to create a subclass or partial mock. If it's not documented, then it's an implementation detail, and should be treated as such.)

这篇关于如何避免类自用的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆