我理解以下是一个主观问题,但您的指导方针对我追求干净,可测试的代码非常有帮助。
请考虑以下示例,我认为这违反了一系列设计原则。
public class OfferEligibilityCheckerServiceImpl implements OfferEligibilityCheckerService, Refreshable{
private Map<String, OfferCriteria> offerIdToOfferCriteriaMap;
private OffersAccessorService offersAccessorService
public OfferEligibilityCheckerServiceImpl (OffersAccessorService offersAccessorService ){
this.offersAccessorService = offersAccessorService;
initValidOfferIdSet();
}
protected void initOfferIdToOfferCriteriaMap(){
offerIdToOfferCriteriaMap = offersAccessorService.get..Criteria();
}
//REAL BUSINESS LOGIC, i.e. this is why the service is used by clients!!
@Override
public boolean isUserEligible(String offerId, UserInfo userInfo){
offerCriteria = offerIdToOfferCriteriaMap.get(offerId);
return offerCriteria.isEligible(userInfo); // let's not worry about NPE
}
// Gets invoked at regular intervals by some scheduler, say Spring.
@Override // from Refreshable
public void refresh(){ // ANOTHER responsibility
initOfferIdToOfferCriteriaMap();
}
}
我觉得上面的代码在很多层面都是错误的,但是我缺乏足够深入的知识来说服别人说它是平庸的/不可测试的。
根据我的有限知识,上述设计的问题在于它看起来是可测试的,因为某些部件可以替代,但它违反了所有“可测试设计”指南。
我和其他人之间的对话。
我:构造函数中的复杂逻辑。
其他:不,我正在从构造函数中调用一个受保护的方法,如果你需要一个双重测试,可以覆盖它。
我: demeter违规法 - 询问确切的事情,而不是中间人。
其他:查看“代码到接口”的强大功能。我正在传递一个serviceImpl,而构造函数需要一个Service。所以我总是可以在测试时替换,以便serviceImpl在单元测试期间不会真正与DAO /数据库通信。
我:违反SRP - 处理业务逻辑,处理让我在构建过程中得到自己的东西,处理让我自己刷新。
其他:没关系!我不想将这个类分成3个类,并且要经历调度它们/连接它们的开销。
我:混合业务逻辑和对象构建逻辑。
其他:我甚至没有得到你。
问题1) 我是对的吗?
我可能没有指出正确的问题或者没有以正确的方式表达它们。
如果你能列出我们将来可能面临的问题,那就太好了。如果你可以解决或验证我的积分1到4,那就更好了。
问题2: 您将如何重新设计它(包括接线部分)?
答案 0 :(得分:5)
这就是设计模式和原则的问题 - 过分关注它们会使我们远离编写代码/生成软件的真正目的......这就是解决业务问题。
首先,让我告诉你为什么这段代码没问题:
话虽如此,结论很明显 - 除了SRP违规外,没有太大的改进空间;令人耳目一新的部分确实可以在不同的组件中然而,那么你有两个你必须连接在一起的东西(你的同事也提到过 - 事实上,这类已经做过了)。有人可能认为这是要走的路,但是当课程/职责很小时,这种努力通常不值得获益,而且往往会给代码带来不必要的复杂性。
您的第1,2和4点不是重新设计的最佳参数。你是对的SRP。但是,你的同事的论点更强 - 将这样的小班打成3个最有可能不会有任何好处,并且肯定会让人们稍后提出问题。
总而言之,值得记住的是某人在某些时候必须阅读您的代码。您需要知道何时停止关注模式和完美设计以及何时开始专注于使您的代码尽可能简单,以便其他人尽可能地遵循。
答案 1 :(得分:2)
1 - 构造函数中的逻辑总是一个问题,如果你使用spring,例如当sprint创建服务时代码可能会失败,很难检测到那些类型的失败,在大型代码库中真的很痛苦。
2 - 将对象传递给构造函数并使用此对象提取您在业务逻辑中使用的真实对象(地图)时,有些东西闻起来很糟糕。
3 - IMMO这是代码中最难闻的气味,地图用作“缓存”,看起来对“offersAccessorService.get..Criteria()”的调用是一个昂贵的操作,转到数据库或一些其他的持久性机制和地图,它习惯于每次需要查询和提供时都不会调用这个代价高昂的操作,我是对的?如果最后一个是正确的,这是一个基础设施概念,而不是一个商业概念。此代码将基础架构职责与业务逻辑职责相结合这些东西属于同一类,但由于不同的原因而改变。例如,如果您在群集中部署此应用程序,则可能会转移到分布式缓存系统而不是内存中的简单映射,在此方案中,您需要更改此代码以用于基础架构而不是业务问题。
4 - 与1和3相关。
如果您有可变状态和多线程,那么另一个重要的问题是,如果方法刷新被后台“刷新”线程调用,同时用户正在调用“isUserEligible”? spring中的服务(通常你可以改变这种行为)实例化一次,服务中的可变状态在多线程应用程序中非常危险。
一个有效的重构,它将这个基础设施和业务问题分开:
public OfferEligibilityCheckerServiceImpl (OffersRepository offersRepository ){
this.offersRepository = offersRepository;
}
@Override
public boolean isUserEligible(String offerId, UserInfo theUser){
return offersRepository.getById(offerId)
.isEligibleFor(theUser);
}
offersRepository是它提供的地方,从bussinees的角度来看,我不在乎它的位置,我不关心这个提供是否缓存,这个责任归于具体的实现存储库(域驱动设计模式方面的存储库)。