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

简介: 由阿里云云效主办的2021年第3届83行代码挑战赛已经收官。超2万人围观,近4000人参赛,85个团队组团来战。大赛采用游戏闯关玩儿法,融合元宇宙科幻和剧本杀元素,让一众开发者玩得不亦乐乎。本文作者: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 = 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 = 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 = 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<AbstractNextDayProcessor> 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<String, AbstractNextDayProcessor> 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元使用。

相关实践学习
基于函数计算一键部署掌上游戏机
本场景介绍如何使用阿里云计算服务命令快速搭建一个掌上游戏机。
相关文章
|
7月前
|
设计模式 算法 Java
设计模式第十五讲:重构 - 改善既有代码的设计(下)
设计模式第十五讲:重构 - 改善既有代码的设计
240 0
|
6月前
|
设计模式 算法
重构,避免重构误区
重构,避免重构误区
25 0
|
7月前
|
设计模式 Java 测试技术
设计模式第十五讲:重构 - 改善既有代码的设计(上)
设计模式第十五讲:重构 - 改善既有代码的设计
260 0
|
11月前
|
设计模式 安全 Java
【Java设计模式 规范与重构】 二 重构的保障:单元测试,以及如何提高代码可测试性
【Java设计模式 规范与重构】 二 重构的保障:单元测试,以及如何提高代码可测试性
108 0
|
11月前
|
设计模式 SQL 算法
【Java设计模式 规范与重构】 六 代码重构小结
【Java设计模式 规范与重构】 六 代码重构小结
145 0
|
11月前
|
设计模式 IDE Java
【Java设计模式 规范与重构】 四 小型重构的手段:规范的十五条军规
【Java设计模式 规范与重构】 四 小型重构的手段:规范的十五条军规
98 0
|
11月前
|
数据处理
《重构2》第六章-重构基础
《重构2》第六章-重构基础
281 0
|
12月前
|
设计模式 程序员 开发者
程序员在开发中必经之路:重构代码
众所周知,程序员在开发过程中接手前人代码,或者接手公司外购项目的代码等情况的时候,都有想要重构代码的冲动,与其这样说,不如说程序员只要是接手不是自己亲自写的代码都想重构!俗话说得好:一百个程序员脑中有一百个编程思维,不同程序员就算是开发相同功能的程序,一定会有不同的实现方式,而且代码格式和实现方式也肯定是不一样的,这样就给程序的代码重构留下了伏笔。
140 1
|
设计模式
重构·改善既有代码的设计.04之重构手法(下)完结
重构改善既有代码的设计完结篇,汇总了全部的重构手法。看看哪些手法对你的项目能有所帮助…
7357 2
重构·改善既有代码的设计.04之重构手法(下)完结
|
开发者
重构的核心-让代码保持整洁
很久之前团队师兄向我推荐了《重构:改善既有代码的设计》这本书,粗略翻阅看到很多重构的细节技巧,但当时还处于未接触过工程代码,只关注代码功能,不太考虑后期维护的阶段,读起来觉得枯燥无味,几乎没有共鸣,一直没有细细阅读。在工作一年后,终于在师兄的督促下,利用一个月左右的早起时光读完了这本书,收获很多,感谢师兄的督促,感谢这本书陪伴我找回了阅读习惯。把这本书推荐给已经接触了工程代码、工作一年左右的新同学,相信有了一定的经验积累,再结合日常项目实践中遇到的问题,对这本书的内容会有很多自己的思考感悟
40561 4
重构的核心-让代码保持整洁