项目从快速原型发展到堆积完基础功能、有一定用户基础。开始还技术的债。重构的价值在这个阶段开始体现。
服务拆分重构
这个项目会是微服务架构模式,但是一开始就拆分出很多一堆服务,并不是很好的实践:
- 项目启动初期,业务方向、业务形态变化大,产品需要的快速原型、快速试错。
- 3个人拆20个微服务,没有实际的价值,只会增加协议变更成本、部署成本,不利于业务的快速试错。
因此,项目初期,只拆分出2个大粒度的核心服务:房间服务和推荐服务。其中房间服务包含个人练歌、唱歌pk游戏房间、Channel鉴权服务。 随着几个月的业务迭代,原来个人练歌功能业务地位提升,代码也越来越复杂。唱歌pk房间的玩法也从单一的小房间轮唱模式,发展为抢唱、轮唱、小房间、大房间。这时候做服务拆分重构:
- 个人练歌服务
- 小房间服务
- 大房间服务
- Channel鉴权服务
这里有个技巧。个人服务和鉴权服务,一开始就被识别为有可能晋升为单独服务部署和管理,因此作为单独一个domain处理,相关的代码在同一个package。到了要做服务拆分就直接整个package拷贝出来再修改。类似:
com.xxx.sd.solo.service
com.xxx.sd.solo.controller
com.xxx.sd.solo.repository
com.xxx.sd.solo.domain
com.xxx.sd.solo.util
领域模型优化
领域模型是随需求迭代而不断进化的。 最大变化是歌曲片段。最初模型参照竞品修改得来,考虑到schema可能频繁变更,底层采用ElasticSearch存储。业务迭代几个月过后,一堆透传字段、无效字段、非必要字段,导致结构体迅速膨胀,配置和解析踩过几次坑导致服务不可用。于是进行梳理:
- 过时无效字段,删除
- 命名不规范、有歧义的字段,先增加新的规范字段,原有服务切换到新字段并且通过验证后,再删除旧字段。
- 调整节点结构,重新抽象公共属性。
代码重构
剩下就是体力活时间,举几个例子。
工厂模式
最初只有小房间模式、轮唱玩法(对应game_type字段),房间状态机服务直接根据上下文闭包构造引擎。 后来分别增加抢唱玩法、大房间模式,原来的调用方自己判断要game_type字端选择状态机引擎,显然不合适。
@Service
public class RoomStateEngineFactory implements IStateMachine{
@Autowired
private InTurnRoomStateEngine inTurnRoomStateEngine;
@Autowired
private RaceRoomStateEngine raceRoomStateEngine;
public IStateMachine newInstance(GameTypeEnum gameType, RoomContext roomContext){
if(gameType == GameType.IN_TURN){
return inTurnRoomStateEngine.newInstance(roomContext);
}
if(gameType == GameType.RACE){
return raceRoomStateEngine.newInstance(roomContext);
}
throw new IllegalArgumentException("unknown gameType="+gameType);
}
// 其他方法
}
不同game_type的状态机实现逻辑,由不同的IStateMachine接口实例提供。RoomStateEngineFactory作为工厂类,封装选择IStateMachine逻辑,调用方无需理解细节。
后续增加新的玩法(game_type),已有的调用方不需要改动。
策略模式和if…else…
延长播放时间有多种策略可以选择,随机增加、等值增加、区间增加等,有配置项控制。
if("random".equals(type)){
//
}else if("interval".equals(type)){
//
}else if("range".equals(type)){
//
}else {
// unknown config, throws error
}
未来还想增加,于是重构为
public interface IExtraTimeStrategy{
int calculate(int like);
}
public class ExtraTimeStrategyProcessor implements IExtraTimeStrategy, ApplicationContextAware {
@Value("extraTime.strategy")
private String strategy;
// Spring应用上下文环境
private ApplicationContext applicationContext;
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
public int calculate(int like){
IExtraTimeStrategy s = applicationContext.getBean("strategy", IExtraTimeStrategy.class);
if(s==null){
throw new IllegalArgumentException("unkown strategy="+strategy);
}
return s.calculate(like);
}
}
// 具体的延长时间策略实现类
@Service("byRandom")
public class RandomExtraTimeStrategy implements IExtraTimeStrategy{
// more code
}
@Service("byInterval")
public class IntervalExtraTimeStrategy implements IExtraTimeStrategy{
// more code
}
@Service("byRange")
public class RangeExtraTimeStrategy implements IExtraTimeStrategy{
// more code
}
为了新增策略不修改代码,直接向ExtraTimeStrategyProcessor注入ApplicationContext,根据配置项获取对应策略的bean。
方法太长
善用idea的Extract Method功能。
修改缓存key
有的缓存已经不再使用,或者需要重命名。
应用使用的缓存key名,都被封装到RedisKeyHelper,并且对外提供getXXXKey([param])。缓存key重构就只需要更改单一入口。
以前踩过的坑是,同一个缓存key没有集中管理的地方,调用方自己拼接,修改的时候出故障。因此项目一开始就强制规定RedisKeyHelper管理所有缓存key。
api接口字段变更
api中旧的字段在新版本废弃。对应字段加上@Deprecated注解,等待3到4星期客户端流量下降到足够低之后,再从代码中删除。