公司的彩票项目希望上线重庆时时彩,这是一种高频彩,此彩种在每天的00:04至01:54期间每4分钟开出一个 彩期, 而在09:59至23:59期间每10分钟开出一个彩期,这样每天能够产生120个彩期。由于时时彩的玩家倾向 于使用彩票的追号功能进行投注(后面将解释什么是追号),我们估计重庆时时彩上线后会产生大量的追号订单,而 现有的追号系统一次请求只能处理一笔订单,这可能导致很多订单无法在可售的彩期内完成投注,在权衡了各方面 的考虑后,我们决定用Ruby重新写一套追号程序,新追号程序的关键特性就是能够批量投注。

什么是追号?

一些老彩民喜欢使用同一个号码连续投注多期,如果每一期都让他们重复相同的步骤投注,估计会把他们惹恼, 所以我们可以让这些彩民在购买彩票的时候选择追号,这样他们就可以使用相同的号码自动投注。 举一个简单的例子,某彩民投注双色球,追10期,那么在后面的10期里,每一期我们的追号系统都会使用她第一次投注的号码自 动投注双色球。

老追号流程

老追号程序是其他同事用Jave开发的,已经在生产环境运行大半年了,没有出过什么大问 题,如果它支持批量投注的话那么其实我们可以继续使用它。老追号的基本流程如下,

  1. 检索出可追号订单
  2. 遍历订单然后投注
  3. 若投注成功,订单标识为成功
  4. 若投注失败,订单标识为失败
  5. 根据投注结果,修改追号订单的数据,比如已追期数等等

新追号的第一版设计

我们的彩票项目后面接了多个彩票供应商,我们称为渠道,这些渠道是我们的供货商,我们卖的所有彩票都是这 些渠道提供的。我为每个渠道都建了一个类用来封装所有和此渠道相关的业务,比如批量投注,新期查询等。

class HuaCaiChannel
  # 批量订单接口
  def api_batch_bets
	...
  end

  # 新期查询接口
  def api_query_term
	...
  end
end

我还设计了一个叫做LotFarm的类用来封装与彩票数据库交互的业务,比如检索出可追号订单,生成投注数据,投注结果入库等。

class LotFarm
  # 检索出可追号订单
  def batch_get_prebets
  ...
  end

  # 批量投注
  def batch_bets
  ...
  end

  # 投注结果入库
  def depote_bets
  ...
  end

end

新追号的第一版部署上去后,没有出错,大约过了两周,运营的同事说咱们再上一个渠道,并且还要加上许多的新功能 比如服务器从宕机状态恢复后能够检查漏追,能够选择价格最便宜的渠道,能够开关彩种等等。于是问题出现了,我 发现第一版的代码中类与类之间的耦合性太强了,并且这些类各个都是巨无霸,改起来要非常小心,还有这些大类 对测试的支持不是很友好。我觉得第一版写地太着急了,现在应该好好思考下这个追号程序的设计。

为什么要给渠道建立一个XXXChannel的类呢? 为什么要建一个LotFarm的类呢? 为什么还要写其他许多大大小小的类呢? XXXChannel将近400百多行,里面有渠道的各种api和配置数据。LotFarm类将近600多行,里面包含了各种相关或者不 相关的业务。

为什么这些类一眼看过去并不知道它们是干什么的? 为什么这些类测试起来很不方便?

针对上面的疑问,我决定给自己写的类立两条规矩,

  1. 名副其实
  2. 单一职责

名副其实就是说我们一眼看到类的名字就应该知道这个类是用来做什么的,以HuaCaiChannel来说,这个类名差强人意, 我们只能知道它是对花彩渠道的一个封装,但是对其具体能做什么不清楚,必须看了它里面的代码才能够了解。 单一职责就是说你设计的类应该只做一件事情,并且要把这件事情做得漂漂亮亮的,显然HuaCaiChannel这个类不具备 这个特征,因为它里面封装了很多功能。

于是我觉得HuaCaiChannel#api_batch_bets不应该是一个方法,它应该是一个类,并且这个类只做一件事情,

class BatchingBetsHuaCai
  def call
	...
  end
end

BatchingBetsHuaCai类只有一个公共的实例方法,它只做一件事情那就是向花彩渠道批量投注,因为它只做一件 事情,并且只有一个对外的公共方法,我对它进行测试和调试方便多了,并且我觉得如果其他同事看到了这个类应 该不用过多地思考就知道它是做什么的。我决定按照这个思路走下去对第一版的追号进行重构。

怎么制作电路板

我有过在PCB(电路板)工厂的流水线上工作的经历,我当时的职位是样品工程师。一般来说样品工期很紧张,质量 要求也很高,因为客户会根据样品的生产情况来决定是否签定批量订单合同。我的职责就是跟踪和监督PCB样品的 生产,确保它们不会被遗忘在某条流水线上或者被粗鲁地对待,有时也帮助工人们解决一些制作工艺上的问题。

我们平时见到的内存条除掉上面的内存颗粒剩下的就是一条电路板。这种电路板是单层的,带了金手指。我就说说 这种电路板是怎么做的。

内存条的电路板最开始的时候是一块大的聚酯板,两面镀了铜,上面没有任何孔和线路,叫做光板。这块光板经过 钻孔,一铜,蚀刻,二铜,印刷,切割等工序后就会变成一块块的电路板,最后会经过品检部门的检查来决定电路 板是否可以出货,不合格的电路板会根据品质的等级情况进行返工或者直接报废。其实每一道工序完成后都会进行 相关的质检,以决定样品是否可以流入下道工序。每一道工序看起来相互依赖,其实相互独立,因为各工序的工人 只关心流入到她这儿的材料是否符合她所操作工序的规范,而不在意材料的上道或下道工序是什么。比如钻孔部门 的工人往往只关心板子的大小,厚度以及孔的数量和直径等等和钻孔相关的信息,而到了一铜,二铜部门工人们只 关心镀铜的厚度,镀铜的面积,以及一些特殊孔径里铜的厚度,到了蚀刻工序后,工人们关心的是线路和文字的排 布,油漆颜色等信息。当然如果一块已经刷了油漆的板子流入到了钻孔部,工人应该会拒绝给它钻孔,因为这时候 工人们还是会关心一下这块板子的来历。

新追号的第二版设计

数据经过追号的流程最终完成追号,就如同光板经过各道工序最终变成电路板。我欣赏电路板制作时各工序间的紧 密配合和恰到好处的独立性,我希望新追号程序的第二版能够像一个成熟的工艺流程一样优雅地完成追号。

新的追号加入了一些新的特性,比如检查漏追,多渠道追号等,经过几天的分析,我们最终确定了新追号的工序 (活动),这里我用工序代替流程是觉得工序比流程更加细致,因为一个流程里面会包含多种工序,在软件的世界里 我给工序一个新的单词Activity(活动),新追号程序包含下面的活动(Activity),

  • 修复空彩期
  • 同步彩期
  • 创建预期彩期
  • 检查漏追的彩期
  • 入库漏追的彩期
  • 入库过期的彩期
  • 投注渠道排序
  • 构建投注记录
  • 分组投注记录
  • 合并投注记录组
  • 海盅渠道批量投注
  • 海盅渠道处理投注返回
  • 花彩渠道批量投注
  • 花彩渠道处理投注返回
  • 京跎渠道批量投注
  • 京跎渠道处理投注返回

正如标题所说,我们需要直面业务和流程,我们不需要去一个类似于ActiveRecord::Base这么大的类 去委婉的实现我们的功能,我们需要一道一道地,直接地去实现我们面前的这些”小工序”,然后将这些”小工序”优雅地 组装起来,最终实现一个复杂的,健壮的系统。

上面这些活动都是相互独立的,但是如果传递给它们正确的数据,并且让它们以合适的顺序运转起来,就能够正确高效 地完成追号任务。我为每个活动都创建了一个类,这些类直接或间接继承于Activity类,

class Activity
  # 实例属性:
  # result     - 执行结果
  # note       - 活动说明
  # result_key - 结果key
  attr_reader   :note, :result_key, :data
  attr_accessor :result

  class << self

	# 类属性:
	# note       - 活动说明
	# result_key - 结果key
	attr_accessor :note, :result_key

	# 活动说明
	def note
	  @note.nil? ? self.name : @note
	end

	# 结果key
	def result_key
	  @result_key.nil? ? self.name.to_s.to_sym : @result_key.to_sym
	end

  end

  def initialize(data)
	@data = data
	@note = self.class.note
	@result_key = self.class.result_key
	@result = nil
  end

  def call
	raise '请在子类实现call方法'
  end

  private

  def return_data(h = {})
	data.merge(result_key => result).merge(h)
  end

end

这个Activty类是我从项目代码中抽取出来的,去掉了一些日志方法。活动在初始化时可以接收数据,数据是一个 hash, 这里我用数据代替参数这一说法,活动通过调用call方法运行,从活动里出来的也是数据。

data-activity-data

现在第二版新的追号程序已经在线上运行了一个多月,绝大部分时间运行地非常好,即使出现了bug也很快就修复了, 因为我能够很快定位bug出现在哪个活动里,就像我原先在工厂里能够很快地定位产品是在哪道工序上出错一样。如果需要加入新的 投注渠道,我们也能够比较快的加入,因为我们只需要实现与渠道对应的投注活动和处理返回活动,事实也确是实 这样的,我曾经用一个上午的时间就搞定了一个新渠道的接入,运营的同事都觉得我们的开发速度够赞的。

活动的原则

  1. 独立自主,自己的事情自己完成,不在活动里运行其它活动
  2. 单一职责,每个活动只做一件事情,并且要干好
  3. 合适大小,活动既不能太大也不能太小
  4. 宽进严出,对于流进的数据,可忽略自己不需要的,流出的数据要确保能够被其他活动使用
  5. 严肃测试,确保活动干好了本职工作,就像电路板的生产一样,需要对每道工序的生产工艺进行检验测试