如何面向对象做好重构(|83行代码)

简介: 由阿里云云效主办的2021年第3届83行代码挑战赛已经收官。超2万人围观,近4000人参赛,85个团队组团来战。大赛采用游戏闯关玩儿法,融合元宇宙科幻和剧本杀元素,让一众开发者玩得不亦乐乎。本文作者:83行代码优秀参赛选手。
题目介绍
【如何面向对象做好重构(|83行代码)】我们的系统:

  • 商品(Item)都有一个销售剩余天数(SellIn),表示该商品必须在该值所代表的天数内销售出去。
  • 所有商品都有一个Value值,代表商品的价值。
  • 每过一天,所有商品的SellIn值和Value值都减1。
  • 一旦过了销售剩余天数,价值就以双倍的速度下滑。
  • 陈年老酒(Aged Wine)是一种特殊的商品,放得越久,价值反而越高。而且过了销售剩余天数后价值会双倍上涨。
  • 商品的价值永远不会小于0,也永远不会超过50。
  • 魔法锤(Sulfuras)是一种传奇商品,其销售剩余天数和品质值都不会变化。
  • 演出票(Show Ticket)越接近演出日,价值反而上升。在演出前10天,价值每天上升2点;演出前5天,价值每天上升3点。但一旦过了演出日,价值就马上变成0。
  • 最近因为灾害,我们采购了特效药(Cure), 特效药的贬值速度是普通物品的两倍,这更加需要尽快升级我们的系统。
解题思路:
这道题是一道非常典型的面向对象的问题,从题目描述来看是实现计算商品价值随着时间流逝变化的功能。首先可以先把题目预设的updateValue方法删的只剩签名... 满眼的if else和数不清的缩进基本上不具备可读性,写这一堆也难为出题的大佬了...
实现方式:
package store; // Please don't modify the class name. public class Store { Item[] items; // Please don't modify the signature of this method. public Store(Item[] items) { this.items = items; }// Please don't modify the signature of this method. public void updateValue() {}

从Store类不可修改的签名可以看出,items是由测试方进行构建, 并调用任意次updateValue触发修改,所以Item类应该没有修改抽象的必要,我们只需要根据它的name、sellIn、value进行处理即可。
既然Item没有必要动,那我们自然需要一个业务处理逻辑来对Item的每天变化进行更改,并且对于特殊商品,它的处理逻辑会有针对性的做调整。
所以这道题的主要实现就是实现一个统一的抽象逻辑和几个特别商品的业务重载。
1. 确认抽象逻辑
首先我们抛开不同商品的差异性描述, 先确认所有商品的共同性, 得到以下特征
? sellIn和value, 分别代表剩余天数和价值, 无论如何变动或者不变动, 这是所有商品的共通属性, 也已经被定义在了Item里面(虽然没用getter和setter显得不那么OO)
? 每过一天, sellIn和value都会变化
? 一旦超过销售天数(sellIn < 0), value会以另一种方式变化(默认是双倍下滑
? 商品的价值永远不会小于0,也永远不会超过50。
依据上面的四点描述,我们已经可以简单的完成一个普通商品的变化逻辑。这里我们先定义这个抽象类,叫做AbstractNextDayProcessor,并且定义一个方法process(Item item),表示执行下一天的操作,对应Store::updateValue
package store; public abstract class AbstractNextDayProcessor { public void process(Item item) { //sellIn减少1 item.sellIn--; if (item.sellIn >= 0) { //没过期 modifyValue(item, -1); } else { //过期了 modifyValue(item, -2); } }//保证不超过50, 不小于0 private void modifyValue(Item item, int deltaValue) { item.value = https://www.it610.com/article/Math.min(Math.max(item.value + deltaValue, 0), 50); }

至此,我们实现了一个普通的商品的变化逻辑。
2. 确认可抽象业务
下面我们来观察特殊商品的特性来决定上述逻辑中需要被抽象的部分:
? 陈年老酒(Aged Wine)sellIn与普通商品一致, value变化相反;
? 魔法锤(Sulfuras)sellIn不变化, value不变化;
? 演出票(Show Ticket)sellIn与普通商品一致, value有比较复杂的独特变化方式;
? 特效药(Cure)sellIn与普通商品一致, value变化是普通两倍;
由上面的黑体部分可见,对于不同的特殊商品,无论是sellIn变化,还是value变化,都会有特殊情况。针对这种情况,需要把相应的逻辑提取到一个方法中,为了让各特殊的实现类进行重载,而sellIn除了魔法锤之外,其他都遵循统一的逻辑,所以sellIn变化可以在基类中进行默认实现,而value变化可以看到各不相同,可以作为抽象方法处理。
改动后的抽象类如下:
package store; public abstract class AbstractNextDayProcessor { public void process(Item item) { item.sellIn += getSellInIncrement(); if (item.sellIn >= 0) { //没过期 modifyValue(item, getValueIncrementInDate()); } else { //过期了 modifyValue(item, getValueIncrementOutOfDate()); } }//没过期的value变化量 protected abstract int getValueIncrementInDate(); //过期时value的变化量 protected abstract int getValueIncrementOutOfDate(); //每天sellIn的变化量, 绝大多数商品都是-1 protected int getSellInIncrement() { return -1; }//保证不超过50, 不小于0 private void modifyValue(Item item, int deltaValue) { item.value = https://www.it610.com/article/Math.min(Math.max(item.value + deltaValue, 0), 50);

这时我们可以开始实现各个特殊商品的处理逻辑,需要注意的是演出票有些特殊, 它value在没过期时的计算是依赖item、sellIn的,而过期后的变化也不是一个固定值而是清零,所谓清零,就是它的变化量是 - item.value,所以这里两个value变化的抽象方法需要额外提供参数Item,调整如下:
protected abstract int getValueIncrementInDate(Item item); protected abstract int getValueIncrementOutOfDate(Item item);

基于抽象类,实现各特殊商品的实现类,还有一个普通商品的实现类。
//老酒 public class AgedWineNextDayProcessor extends AbstractNextDayProcessor { //每过一天, 价值+1 @Override protected int getValueIncrementInDate(Item item) { return 1; }//过期的话, 价值+2 @Override protected int getValueIncrementOutOfDate(Item item) { return 2; } }//特效药 public class CureNextDayProcessor extends AbstractNextDayProcessor { //每过一天, 价值-2 @Override protected int getValueIncrementInDate(Item item) { return -2; }//过期的话, 价值-4 @Override protected int getValueIncrementOutOfDate(Item item) { return -4; } }//演出票 public class ShowTicketNextDayProcessor extends AbstractNextDayProcessor { @Override protected int getValueIncrementInDate(Item item) { //注意, 因为抽象类是先做了sellIn--, 所以这里的几个判断范围是 [0,4], [5-9], [10-], 如果先计算价值再--的话, <需要改成<= if (item.sellIn < 5) { return 3; }if (item.sellIn < 10) { return 2; }return 1; }@Override protected int getValueIncrementOutOfDate(Item item) { return -item.value; } }//魔法锤 public class SulfurasNextDayProcessor extends AbstractNextDayProcessor { //价值不变动 @Override protected int getValueIncrementInDate(Item item) { return 0; }//价值不变动 @Override protected int getValueIncrementOutOfDate(Item item) { return 0; }//sellIn不变动 @Override protected int getSellInIncrement() { return 0; } }//普通商品 public class OtherNextDayProcessor extends AbstractNextDayProcessor { @Override protected int getValueIncrementInDate(Item item) { return -1; }@Override protected int getValueIncrementOutOfDate(Item item) { return -2; } }

至此,核心的业务逻辑全部完成。
3. 构建判断流程
所有的工具已经就位,下一步就是考虑如何让不同的处理器可以正确处理到它对应的商品。从Item定义和题目可以看出,商品是通过不同的名字来区分的,这里有几种实现方式,我个人比较倾向让各业务逻辑自己进行判断,类似Spring MVC的filter chain,那就是构造一个处理器链,从第一个处理器开始若能处理则处理,不能处理再交给下一个,直到交到最后一个托底(普通商品),那process方法自然需要加上一个步骤,就是判断这个item的名字是否属于自己处理的范畴,AbstractNextDayProcessor类调整如下:
package store; public abstract class AbstractNextDayProcessor { private final String name; public AbstractNextDayProcessor(String name) { this.name = name; }public boolean process(Item item) { //name不为null说明是特殊处理器,与item不同代表不应处理此item if (name != null && !name.equals(item.name)) { return false; }item.sellIn += getSellInIncrement(); if (item.sellIn >= 0) { //没过期 modifyValue(item, getValueIncrementInDate(item)); } else { //过期了 modifyValue(item, getValueIncrementOutOfDate(item)); }return true; }protected abstract int getValueIncrementInDate(Item item); protected abstract int getValueIncrementOutOfDate(Item item); protected int getSellInIncrement() { return -1; }//保证不超过50, 不小于0 private void modifyValue(Item item, int deltaValue) { item.value = https://www.it610.com/article/Math.min(Math.max(item.value + deltaValue, 0), 50); } }

增加了私有属性name, 标识自己可识别的item name, 如果name为空, 代表可处理一切, 如果不为空, item name必须与自己相匹配才执行. 返回值true代表处理成功. 这里自然就要求其他实现类也要默认实现对应的唯一构造函数, 例如:
package store; public class SulfurasNextDayProcessor extends AbstractNextDayProcessor { public SulfurasNextDayProcessor() { super("Sulfuras"); }//价值不变动 @Override protected int getValueIncrementInDate(Item item) { return 0; }//价值不变动 @Override protected int getValueIncrementOutOfDate(Item item) { return 0; }//sellIn不变动 @Override protected int getSellInIncrement() { return 0; } }

最后, 我们需要来实现Store中的执行入口updateValue.
首先要构建一个处理器链, 并且一定要保证OtherNextDayProcessor放在最后, 因为它会对所有item进行处理并返回true.
然后循环每一个item, 然后循环处理器链, 当第一个返回true时, 即结束当前item的处理, 代码如下
package store; import java.util.Arrays; import java.util.List; // Please don't modify the class name. public class Store { Item[] items; private final List processors; // Please don't modify the signature of this method. public Store(Item[] items) { this.items = items; processors = Arrays.asList( new AgedWineNextDayProcessor(), new CureNextDayProcessor(), new ShowTicketNextDayProcessor(), new SulfurasNextDayProcessor(), new OtherNextDayProcessor()); }// Please don't modify the signature of this method. public void updateValue() { for (Item item : items) { for (AbstractNextDayProcessor processor : processors) { if (processor.process(item)) { //如果返回true, 代表已找到正确的处理器进行处理, 跳出内层循环. break; } } } } }

这时可以抓紧提交了,可以看到正确性验证是100分,其他代码规约和复杂度有扣分,使用插件检查调整即可。
方案2:
按照以上方式是个人认为面向对象思想最好的实现,但是复杂度评分一直到不了满分,最后判断是代码量偏大的缘故。
如果想要冲击100分,可以不在Processor里添加name字段,转而在Store里用map来维护处理器,代码如下:
package store; import java.util.HashMap; import java.util.Map; // Please don't modify the class name. public class Store { private final Map processors; private final AbstractNextDayProcessor defaultProcessor; Item[] items; // Please don't modify the signature of this method. public Store(Item[] items) { this.items = items; processors = new HashMap<>(); processors.put("Age Wine", new AgedWineNextDayProcessor()); processors.put("Cure", new CureNextDayProcessor()); processors.put("Show Ticket", new ShowTicketNextDayProcessor()); processors.put("Sulfuras", new SulfurasNextDayProcessor()); defaultProcessor = new OtherNextDayProcessor(); }// Please don't modify the signature of this method. public void updateValue() { for (Item item : items) { processors.getOrDefault(item.name, defaultProcessor).process(item); } } }

个人认为这做法不是太好,因为会导致处理器判断逻辑与Store有耦合(虽然方案1在目前实现也有耦合,但实际业务中,方案1的list可以通过依赖注入的方式构建)。
最后
几个应试小技巧,一定不要吝惜提交次数,及时提交可以借助测试用例进行快速检查。另外,比赛前通过前几关预赛摸清楚比赛规则,今年对提交次数有限制,提前确认好增加提交次数的手段备用。
大赛目前全部关卡开放体验,域名地址:https://code83.ide.aliyun.com/,欢迎你来。
推荐阅读
1、用代码玩剧本杀?第3届83行代码大赛剧情官方解析
2、无算法不Java,这道算法题很难?
欢迎大家使用云效,云原生时代新DevOps平台,通过云原生新技术和研发新模式,大幅提升研发效率。现云效公共云基础版不限人数0元使用。

    推荐阅读