关于阿里云:如何面向对象做好重构|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元应用。

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理