第一章-宝箱抽奖模块与代码设计(一)

简要 信息
作者 卡卡
博客 http://blog.csdn.net/kakashi8841
邮箱 john.cha@qq.com
本文所属专栏 http://blog.csdn.net/column/details/12687.html

无聊的开场白

每篇文章的背后都有个”高大上”的故事

大家好,我是卡卡(取自火影忍者中卡卡西)。其实这篇文章是快到截止日期才写的,因为自己创业中,然后最近刚好有个同学创业,很多技术上的东西需要我帮忙,因此本来和CSDN约好的这篇文章也是一拖再拖。然后在快到文章截止日期的某个晚上,有个朋友刚好和我聊起怎样提升代码质量的问题。于是就有了这篇文章。

怎样的代码才算是好的代码

Linux大神大概是这么说的,两个程序员写出的代码不同在于他们的对程序的品味不同。确实,有的人对代码比较敏感,一眼就能看出,这里写的不好,这里不灵活,这里可能有问题。
那么很多积极上进的少年肯定还是希望能提高自己代码设计的质量。本文将通过一个实例来说明怎样逐步优化代码。

简单的需求

很多游戏中都有开宝箱的功能。比如现在你收到一个任务。让你去做一个开宝箱的模块。需求如下:
1. 玩家可以看到三种白银、黄金、钻石三种类型的宝箱,分别消耗游戏中的白银、黄金和钻石。
2. 这三种宝箱玩家可以请求开1次和开10次。

简单的代码(Java实现)

一些基本的类

玩家类

存放玩家资源和其他信息的类,这里为了演示,去掉无关的属性,只有资源属性。
这个类目前的写法是比较常见的写法,但是这个类有很多优化的地方。文章后面会说。

package com.kakashi01.player.domain;public class Player {private static final int    MAX_SLIVER   = Integer.MAX_VALUE;private static final int    MAX_GOLD       = Integer.MAX_VALUE;private static final int    MAX_DIAMOND = Integer.MAX_VALUE;private int                 id;private int                 sliver;  // 白银private int                 gold;    // 黄金private int                 diamond; // 钻石public int getId() {return id;}public void setId(int id) {this.id = id;}public int getSliver() {return sliver;}public void setSliver(int sliver) {this.sliver = sliver;}public int getGold() {return gold;}public void setGold(int gold) {this.gold = gold;}public int getDiamond() {return diamond;}public void setDiamond(int diamond) {this.diamond = diamond;}/*** 修改白银** @param alter*            修改值。正数表示增加,负数表示减少* @return*/public boolean alterSliver(int alter) {int old = sliver;sliver = alter(sliver, alter, 0, MAX_SLIVER);return old != sliver;}/*** 修改钻石** @param alter*            修改值。正数表示增加,负数表示减少* @return*/public boolean alterDiamond(int alter) {int old = diamond;diamond = alter(diamond, alter, 0, MAX_DIAMOND);return old != diamond;}/*** 修改黄金** @param alter*            修改值。正数表示增加,负数表示减少* @return*/public boolean alterGold(int alter) {int old = gold;gold = alter(gold, alter, 0, MAX_GOLD);return old != gold;}/*** 修改传入的current,修改量为alter。修改后应该在[min, max]范围内。** @param current*            修改前的值* @param alter*            修改量* @param min*            下限(包含)* @param max*            上限(包含)* @return 返回修改后的值*/private int alter(int current, int alter, int min, int max) {if (alter > 0) {current += alter;if (current < min || current > max) {current = max;}} else if (alter < 0) {if (current >= -alter) {current += alter;}if (current < min) {current = min;}}return current;}
}

抽奖服务类

这个类暂时只有一个空方法。这也是我们下面将实现的内容。

package com.kakashi01.lottery;public class LotteryService {/*** 抽奖方法* @param lotteryType 宝箱类型* @param timesType    次数类型*/public void lottery(int lotteryType, int timesType){}
}

我们需要定义一个用于表示抽奖信息的类ConfigLottery。根据宝箱类型和次数类型,就可以取到抽奖信息。然后进行抽奖。
因此在LotteryService中增加一个lotteryMap(Map类型),以及getConfigLottery方法用于从lotteryType和timesType映射到ConfigLottery。

private final Map<Integer, Map<Integer, ConfigLottery>> lotteryMap = new HashMap<>();public ConfigLottery getConfigLottery(int lotteryType, int timesType) {Map<Integer, ConfigLottery> map = lotteryMap.get(lotteryType);if (map == null) {return null;}return map.get(timesType);
}

先大致确定抽奖方法lottery的逻辑,确定后的LotteryService代码如下:

package com.kakashi01.lottery;import java.util.HashMap;
import java.util.Map;import com.kakashi01.lottery.domain.ConfigLottery;
import com.kakashi01.player.domain.Player;public class LotteryService {private final Map<Integer, Map<Integer, ConfigLottery>> lotteryMap = new HashMap<>();public ConfigLottery getConfigLottery(int lotteryType, int timesType) {Map<Integer, ConfigLottery> map = lotteryMap.get(lotteryType);if (map == null) {return null;}return map.get(timesType);}/*** @param player*            进行抽奖的玩家* @param lotteryType*            宝箱类型* @param timesType*            次数类型*/public void lottery(Player player, int lotteryType, int timesType) {ConfigLottery configLottery = getConfigLottery(lotteryType, timesType);if (configLottery == null) {System.err.println("Can not found such ConfigLottery : " + lotteryType + ", " + timesType);return;}if (tryCostResource(player, configLottery)) {dropItem(configLottery);} else {System.err.println("Not enough resource for lottery : " + lotteryType + ", " + timesType);return;}}private void dropItem(ConfigLottery configLottery) {// TODO 掉落物品}private boolean tryCostResource(Player player, ConfigLottery configLottery) {// TODO 扣除资源return false;}
}

可以看到LotteryService中lottery的方法签名变了,增加了Player对象,而且还增加了存根方法dropItem和tryCostResource。这两个方法都被lottery调用。
有了上面大致的逻辑流程。我们就可以一步步来实现功能了。

扣除资源方法tryCostResource,应该怎么实现?

大家应该很容易就想到了,既然已经取到了抽奖的配置ConfigLottery,那么只要把这个抽奖需要多少资源保存在ConfigLottery中的一个字段就可以了。比如ConfigLottery中增加一个cost字段代表消耗多少资源,当lotteryTyoe为1、2、3时分别代表白银宝箱、黄金宝箱、钻石宝箱,那么自然可以根据lotteryType的值判断需要消耗什么资源。
那么,此时tryCostResource的实现代码为:

private boolean tryCostResource(Player player, ConfigLottery configLottery) {switch (configLottery.getLotteryType()) {case ConfigLottery.SLIVER:return player.alterSliver(-configLottery.getCost());case ConfigLottery.GOLD:return player.alterGold(-configLottery.getCost());case ConfigLottery.DIAMOND:return player.alterDiamond(-configLottery.getCost());}return false;
}

很直观,根据不同的宝箱类型。然后扣除不同的资源。并返回是否扣除成功。
对了忘记说,ConfigLottery的代码已经被改为:

package com.kakashi01.lottery.domain;public class ConfigLottery {public static final int SLIVER  = 1;public static final int GOLD    = 2;public static final int DIAMOND = 3;private int             lotteryType;private int             timesType;private int             cost;       // 抽奖需要消耗的资源public int getLotteryType() {return lotteryType;}public void setLotteryType(int lotteryType) {this.lotteryType = lotteryType;}public int getTimesType() {return timesType;}public void setTimesType(int timesType) {this.timesType = timesType;}public int getCost() {return cost;}public void setCost(int cost) {this.cost = cost;}
}

简单,粗暴,有效。然而这么简单的代码,也是存在一些可以优化的地方。我们文章后面再说。现在还是先赶紧实现需求。只剩下dropItem方法写完就可以下班了~

dropItem与大转盘

抽奖,开宝箱其实很像平常见到的转盘,转盘上画满各种奖品,然后旋转转盘,等待转盘停下时,指针指向的物品就是奖品。那么,应该怎样用代码描述这样的一个行为。其实,根据数学的一些基础知识,我们知道,转盘为360度。如果某个物品占据了一个80度的扇形,那么意味着这个物品被抽中的概率为80/360。而转盘上所有扇形加起来的角度之和为360度,即为概率的基数。同样的道理,我们只要有以下数据,就可以模拟转盘进行随机抽取。
我们需要该宝箱会掉落什么物品,多少数量,以及每个物品所占的比重。那么修改ConfigLottery为如下代码:

package com.kakashi01.lottery.domain;import java.util.List;public class ConfigLottery {public static final int         SLIVER  = 1;public static final int         GOLD    = 2;public static final int         DIAMOND = 3;private int                     lotteryType;private int                     timesType;private int                     cost;           // 抽奖需要消耗的资源private List<ConfigLotteryItem> items;          // 掉落的物品public int getLotteryType() {return lotteryType;}public void setLotteryType(int lotteryType) {this.lotteryType = lotteryType;}public int getTimesType() {return timesType;}public void setTimesType(int timesType) {this.timesType = timesType;}public int getCost() {return cost;}public void setCost(int cost) {this.cost = cost;}public List<ConfigLotteryItem> getItems() {return items;}public void setItems(List<ConfigLotteryItem> items) {this.items = items;}
}

可以看到相对之前只是增加了items字段。ConfigLotteryItem的代码如下:

package com.kakashi01.lottery.domain;public class ConfigLotteryItem {private int modelID;    // 物品模型IDprivate int num;        // 物品数量private int weight;     // 权重public int getModelID() {return modelID;}public void setModelID(int modelID) {this.modelID = modelID;}public int getNum() {return num;}public void setNum(int num) {this.num = num;}public int getWeight() {return weight;}public void setWeight(int weight) {this.weight = weight;}}

可以看到ConfigLotteryItem的代码很简单,它只记录了掉落的物品模型ID,掉落的物品数量,该物品的权重。

那么,这时候就可以使用该信息来掉落物品了。修改LotteryService中dropItem的代码如下:

private void dropItem(ConfigLottery configLottery) {Random rand = new Random();for (int i = 0; i < configLottery.getTimesType(); i++) {List<ConfigLotteryItem> items = configLottery.getItems();int totalWeight = 0;for (ConfigLotteryItem item : items) {totalWeight += item.getWeight();}int randNum = rand.nextInt(totalWeight);for (ConfigLotteryItem item : items) {if (randNum < item.getWeight()) {System.out.println("Drop item " + item.getModelID() + ", " + item.getNum());break;}randNum -= item.getWeight();}}
}

这里面先计算这个抽奖信息的总权重totalWeight。策划的配置不一定使得所有物品的权重之和为100,更不一定为360。权重更多的表示的是该物品在整个转盘中所占的比例,是相对其他物品而言。因此,需要把所有物品的权重进行求和。
然后,在[0, totalWeight)区间产生随机数。你可以理解为在圆盘中的0~360之间选择一个角度。然后第二层循环则是判断该随机数落在哪个区间。落在这个区间,则表示掉落这个物品。

接下来编写测试代码观察运行结果:

package com.kakashi01.lottery;import java.util.LinkedList;
import java.util.List;import com.kakashi01.lottery.domain.ConfigLottery;
import com.kakashi01.lottery.domain.ConfigLotteryItem;
import com.kakashi01.player.domain.Player;public class LotteryDemo {private static final int[]          lotteryTypes    = new int[] { ConfigLottery.SLIVER, ConfigLottery.GOLD,ConfigLottery.DIAMOND };private static final int[]          timesTpyes      = new int[] { 1, 10 };                                                                                                                                                                                                                                                                                                  // 只能抽1次或10连抽private static final LotteryService loggterService  = new LotteryService();static {// 这个static代码块中的代码实际上正式开发应该读取策划的配置表。为了演示方便在这里通过程序生成数据for (int lotteryType : lotteryTypes) {for (int timesType : timesTpyes) {ConfigLottery configLottery = new ConfigLottery();configLottery.setLotteryType(lotteryType);configLottery.setTimesType(timesType);configLottery.setCost(10000 * timesType);List<ConfigLotteryItem> items = new LinkedList<>();items.add(new ConfigLotteryItem(1000 * lotteryType, 1, 10));items.add(new ConfigLotteryItem(1001 * lotteryType, 2, 20));items.add(new ConfigLotteryItem(1002 * lotteryType, 3, 30));configLottery.setItems(items);loggterService.addConfigLottery(configLottery);}}}public static void main(String[] args) {Player player = null;for (int lotteryType : lotteryTypes) {for (int timesType : timesTpyes) {System.out.println("Lottery#" + lotteryType + "#" + timesType);for (int i = 0; i < 10; i++) {player = newPlayer();System.out.println("-- " + i);loggterService.lottery(player, lotteryType, timesType);}}}}private static Player newPlayer() {Player player = new Player();player.setSliver(100000);player.setGold(100000);player.setDiamond(100000);return player;}
}

以上,一个简单的测试代码就算完成了。

优化资源处理

终于做完需求了。但是,咱作为优秀的忍者,哦不,作为优秀的程序员。还记得之前提过的需要优化的地方吗。
回到最开始的Player代码。有木有发现,alterSliver、alterGold、alterDiamond这几个方法的实现和类似。虽然我们已经通过抽象逻辑,使用alter方法同时处理3种资源的修改逻辑。但是,还是不那么完美。比如,加入你游戏里面突然加入了一种新资源Power(体力)。你就得在Player中增加如下代码:

    private static final int    MAX_POWER   = Integer.MAX_VALUE;private int                 power;public int getPower() {return power;}public void setPower(int power) {this.power = power;}public boolean alterPower(int alter) {int old = power;power = alter(power, alter, 0, MAX_POWER);return old != power;}

相信对代码充满的追求的同学已经开始思考怎样避免这种行为了。这里你可以先想一下,再继续看下面对Player的修改:
请先思考
请先思考
请先思考
下面是修改后的Player

package com.kakashi01.player.domain;import java.util.HashMap;
import java.util.Map;public class Player {public static final int                     RESOURCE_SLIVER     = 1;public static final int                     RESOURCE_GOLD       = 2;public static final int                     RESOURCE_DIAMOND    = 3;public static final int                     RESOURCE_POWER      = 4;public static final int[]                   ALL_RESOURCES       = {RESOURCE_SLIVER,RESOURCE_GOLD,RESOURCE_DIAMOND,RESOURCE_POWER };private static final Map<Integer, Integer>  RESOURCE_MAX        = new HashMap<>();static {for (int resource : ALL_RESOURCES) {RESOURCE_MAX.put(resource, Integer.MAX_VALUE);}}private int                     id;private Map<Integer, Integer>   resources   = new HashMap<>();public int getId() {return id;}public void setId(int id) {this.id = id;}public int getResource(int resource) {Integer r = resources.get(resource);if (r == null) {return 0;}return r.intValue();}public void setResource(int resource, int value) {resources.put(resource, value);}public boolean alterResource(int resource, int alter) {int current = getResource(resource);int old = current;int min = 0;int max = getMaxResource(resource);if (alter > 0) {current += alter;if (current < min || current > max) {current = max;}} else if (alter < 0) {if (current >= -alter) {current += alter;}if (current < min) {current = min;}}setResource(resource, current);return old != current;}public static int getMaxResource(int resource) {Integer r = RESOURCE_MAX.get(resource);if (r == null) {return 0;}return r.intValue();}
}

可以看到修改后的Player代码对资源的处理更加统一,甚至说,忍者,哦不,开发者,可以对资源的类型不那么敏感,如果策划想增加另一种资源,其实程序只是定义多一个资源类型而已。不需要增加大段的代码。有的人说,快速完成功能才是王道,代码不需要好的设计。难道这个设计不是能让你更快速完成功能吗?因此,很多东西不是非此即彼。不是你代码写的差,你开发效率就高的。(偷笑,别砸我)

由于Player对资源处理进行了修改,那么相应的,修改LotteryService中的tryCostResource方法。修改后代码如下:

private boolean tryCostResource(Player player, ConfigLottery configLottery) {return player.alterResource(configLottery.getLotteryType(), -configLottery.getCost());
}

看,又是一个好的设计减少了代码量的例子。LotteryDemo中的那几个setXXX方法,也要自己修改为setResource方法。

下班前的悬念

优化完了,也快到时间下班了。此时策划向你走了过来。拍了你的肩膀,向你投来崇拜的眼神,你已经完成了抽奖了呀。不错呀。不过。。。可能有几个需求还要改一下。策划向你留下了几个需求:
1. 每种宝箱有可能不是消耗对应的资源。比如,白银宝箱可能也可以消耗黄金来开启。
2. 10连抽现在只是简单的循环抽了10次,需要改成10连抽里面9次是正常抽的,而有1次会掉落更高级的东西。

此时如果是你,你会在上面的代码进行怎样的调整,使得适应新的需求。由于时间和篇幅的关系,下一篇文章会继续宝箱抽奖模块的制作。程序员和策划之间的斗智斗勇还在继续。

本文项目可以在https://github.com/johncha/CodeDesign-1找到。请先阅读git中的README.md查看项目的使用说明。

如果你对本文有什么建议或意见,可以发邮件到john.cha@qq.com或到blog.csdn.net/kakashi8841中留言。

第一章-宝箱抽奖模块与代码设计(一)相关推荐

  1. 第三章-宝箱抽奖模块与代码设计(三)

    第三章-宝箱抽奖模块与代码设计(三) 简要 信息 作者 卡卡 博客 http://blog.csdn.net/kakashi8841 邮箱 john.cha@qq.com 本文所属专栏 http:// ...

  2. 用Groovy思考 第一章 用Groovy简化Java代码

    用Groovy思考  第一章 用Groovy简化Java代码 作者:chszs,转载需注明.博客主页:http://blog.csdn.net/chszs 1. Groovy的安装 目前Groovy的 ...

  3. c语言程序第一章编程,c语言程序的设计第一章 C语言编程入门.ppt

    c语言程序的设计第一章 C语言编程入门 第1章 C语言编程入门 本章是本书的入门篇,专为初学者熟悉编程过程.掌握程序结构而准备的. 本章学习目标 ? 1)? 能够通过模仿与改变来构造带有测试函数的C语 ...

  4. 开宝箱抽奖CSS3动画代码

    下载地址 使用Zepto.js插件实现的开宝箱抽奖,动画效果配合CSS3代码,很不错的特效代码,特效基于Zepto.CSS3,其中有好几种CSS3动画效果,背景光环滚动,宝箱摇摆开启,弹出提示层,还是 ...

  5. 《第一行代码》 第一章:第一行Android代码

    1,Android系统架构 2,开发的四大组件 3,丰富的系统控件 Android 系统为开发者提供了丰富的系统控件,使得我们可以很轻松地编写出漂亮的界面.当然如果你品位比较高,不满足于系统自带的控件 ...

  6. Python入门-第一章Python基础(1.代码格式)

     hello,程序猿们,我是挪威森林的水手,从此篇文章开始,我将和大家一起从0基础开始学习Python,当然对于Python我也是初学者,大家可以相互多多交流,有好的意见大家可以在下面评论区多多交流, ...

  7. 算法竞赛入门经典 第一章 上机练习(C++代码)

    //平均数(average) //输入3个整数,输出它们的平均值,保留3位小数. #include<iostream> #include<iomanip> using name ...

  8. c++ primer5 第一章书籍上的一些代码

    #include <iostream>using namespace std;int main(){int sum = 0,value = 0;while(cin>>value ...

  9. 【hadoop生态之ZooKeeper】第一章Zookeeper概述【笔记+代码】

    一.Zookeeper概述 1.1 概述 Zookeeper是一个开源的分布式的,为分布式应用提供协调服务的Apache项目.Hadoop和Hbase的重要组件.它是一个为分布式应用提供一致性服务的软 ...

最新文章

  1. 对数据库表中的某一字段去重分组排序
  2. Leangoo看板协作工具与Trello还真的不一样
  3. 《云计算》学习笔记4——Google的云计算原理与应用(分布式结构化数据表BigTable)
  4. CodeForces - 1360H Binary Median(二分)
  5. java 拼sql最大长度,java.sql.SQLNonTransientConnectionException: 用户 ID 长度 (0) 超出 1 到 255 的范围...
  6. SAP CRM系统UI checkbox的设计与实现
  7. Java / Web项目中的NPM模块Browser-Sync
  8. ztree java 增删改_Ztree实现增删改查
  9. 百度之星初赛(A)——T5
  10. 分布式:分布式系统的设计
  11. GreaseMonkey批量删除微博代码
  12. 【测试开发】Pytest—Html测试报告定制及封装
  13. python获取期货数据_【python量化】期货ML策略(一)数据获取
  14. 局域网内windows远程mac(使用TeamView)
  15. 谏太宗十思疏 魏征(原文/译文)
  16. 如何让电脑同时连接内外网?
  17. Java 8 stream学习
  18. 使用卡尔曼滤波和扩展卡尔曼滤波进行毫米波雷达和激光雷达数据融合示例
  19. ERP系统物料清单管理:自由选配,随需应变!
  20. AcWing——杨辉三角

热门文章

  1. 想要知道孩子的长相吗
  2. linux 三维机械软件有哪些,机械行业主要用到哪些三维设计软件?
  3. 2021.8.8 ~ 2021.8.14 在SSL集训总结(Week 1 已更完)
  4. 戏曲《未央宫》郭德纲
  5. 打开智慧的魔盒—思维导图、概念图应用宝典
  6. js中的indexOf方法和lastIndexOf方法
  7. 全网最全的 postman 工具使用教程
  8. 2022年最后一天啦,那就好好告个别吧
  9. 第四天 魔小灯(蓝牙智能灯)1
  10. 【ACWing】129. 火车进栈