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*/
}
}