如何面向对象做好重构?|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元使用。

相关文章
|
3月前
|
数据可视化
Tushare接口文档:财务指标数据(fina_indicator)
本文旨在对Tushare的财务指标数据`fina_indicator`数据接口进行介绍,提供更多参考示例和使用说明。 财务指标数据包括盈利能力(如“净资产收益率”、“销售净利率”)、成长能力(如“营业收入同比增长率”)、偿债能力(如“资产负债率”、“流动比率”)、运营能力和现金流量等指标。本文除了介绍如何获取某支股票的财务数据外还介绍了通过VIP版本(`fina_indicator_vip`)一次性获取某个报告期内的所有股票财务指标。要注意传递`fields`以便能够获取想要的字段,还要注意`update_flag`对修正数据的标识。
578 2
|
4月前
|
缓存 人工智能 安全
别被代码吓跑!产品经理的淘宝API接入避坑指南
本文从产品视角拆解淘宝API对接四步法:①资质认证(执照/通行证);②精准定义数据需求;③防范限流与异常;④严守数据安全底线。帮产品经理摆脱“技术恐惧”,掌握业务边界与规则,高效推动项目落地。
370 0
|
4月前
|
人工智能 JSON API
嘴炮Hermes:我干完了!实际啥也没做,咋整?
本文揭秘AI Agent“嘴炮”现象(执行幻觉):模型不真实调用工具,却编造看似完美的执行报告。以Hermes生成壁纸任务为例,剖析其成因——LLM本质是概率文本生成器。提出两大缓解方案:开启`tool_use_enforcement: true`强制校验 + 在SOUL.md中植入“执行铁律”,显著降低幻觉率。
385 0
|
4月前
|
安全 测试技术 API
筑牢云上办公防线:禁用USB网络共享策略的统一部署与技术实现
USB网络共享(如手机USB热点)正成企业安全“暗道”:绕过防火墙、DLP与审计,引发数据泄露与内网入侵。本文剖析其四大风险,指出“禁用所有USB”不可行,提出基于阿里云无影API的精准管控方案——按VID/PID识别并阻断RNDIS/CDC-NCM类设备,保留UKey、打印机等合法外设,实现安全与效率平衡。(239字)
|
5月前
|
弹性计算 NoSQL 关系型数据库
基于阿里云ECS部署Candfly API分发系统:从0到1搭建商业化API分发平台
本文详解如何基于阿里云ECS私有化部署Candfly API分发系统,实现从0到1搭建商业化API平台。涵盖多租户管理、计费、限流、监控等全功能,支持对外变现与对内治理,数据自主可控,部署仅需10分钟,成本低至300元/月。(239字)
|
自然语言处理 数据安全/隐私保护 UED
如何选择适合的域名:关键步骤与策略
在数字化时代,域名不仅是网站的身份标识,更是品牌在线形象的重要组成部分。本文将指导您如何选择一个恰当、易于记忆且富有意义的域名,从明确目标与定位、选择合适的域名类型与后缀、考虑长度与可读性、检查可用性与合法性、评估SEO价值、考虑未来扩展、购买与注册到监测与维护,全方位帮助您打造优质域名。
1216 4
|
存储 JSON JavaScript
Python处理JSON文件数据各类操作一文详解+实例代码
Python处理JSON文件数据各类操作一文详解+实例代码
1796 0
Python处理JSON文件数据各类操作一文详解+实例代码
|
Python
Pycharm开发环境下创建python运行的虚拟环境(自动执行安装依赖包)
Pycharm开发环境下创建python运行的虚拟环境(自动执行安装依赖包)
1998 0
Pycharm开发环境下创建python运行的虚拟环境(自动执行安装依赖包)
|
Ubuntu Linux
Ubuntu 20.04 右键扩展在vscode中打开
Ubuntu 20.04 右键扩展在vscode中打开
1852 0
Ubuntu 20.04 右键扩展在vscode中打开
|
数据采集 弹性计算 人工智能
玩转ECS第3讲 | ECS自助服务之智能诊断和自动化修复
自助服务水平的高低是云厂商的核心竞争力,阿里云经过过去几年的积累,已经有了非常高效的自助服务能力。今天就将这些能力透露给最终用户。本次分享由阿里云高级技术专家滕圣波(云普)为大家介绍ECS自助服务之智能诊断与智能修复,解密背后的AI与实现细节,剧透ECS自助服务的未来。
玩转ECS第3讲 | ECS自助服务之智能诊断和自动化修复