关于阿里云:如何面向对象做好重构|83行代码

46次阅读

共计 7763 个字符,预计需要花费 20 分钟才能阅读完成。

简介:由阿里云云效主办的 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 元应用。

正文完
 0