通过本系列文章,我试图通过一个简单的UC来构建我的架构世界观和方法论,其中核心的线索是SOC,使用的语言是
Scala
,编程范式为FP
第一篇 编译器的归编译器,运行时的归运行时
无论是2C 还是 2B,用户都是公司的命根子,当用户被千方百计吸引过来时,我们一定不希望用户被一个不稳定的用户注册服务拒之门外。
那如何构造一个稳定的用户注册服务呢?
这还不简单么
从业务角度,这确实没什么难的,我们可以简单的提炼出一个Case:
UC 1. 用户注册/Register
流程:
1. 校验用户提交的注册要素(手机号码,验证码,登陆名称,密码)
2. 保存用户注册信息,并返回成功注册的用户信息
2.1 如果用户注册要素校验不通过,则返回注册要素不合规范的错误信息
2.2 如果保存失败,则返回服务端暂时不可用的错误信息
前置条件:
1. 通知服务已经发送该手机号码的验证码,并提供验证码验证服务。
后置条件:
1. 注册成功后,用户可通过登录名和密码登录登录以获取服务。
基于业务,我们很容易建模:
/** 用户信息
* @param mobile: 手机号码
* @param otp: 验证码
* @param loginName: 登录名
* @param password: 密码
*/
case class User(mobile: String, otp: String, loginName: String, password: Vector[Char])
/** 用户服务
*
*/
trait UserService {
/** 注册用户,完成 UC1 的业务规则
*/
def register(user: User): Unit
}
我们可以把这些交给Coding小伙伴来实现了吧:
class UserServiceImpl extends UserService {
/** 注册用户,完成 UC1 的业务规则
*/
def register(user: User): Unit = {
// 校验手机号码格式
if (! validateMobile(user.mobile)) throw new Exception("手机号码格式错误")
// 调用验证码验证Restful服务
if (! validateOtp(user.otp)) throw new Exception("验证码错误")
// 校验用户登录名格式
if (! validateLoginName(user.loginName)) throw new Exception("用户名格式错误")
// 校验用户登录名是否冲突
if (! hasExisted(user.loginName)) throw new Exception("用户名冲突")
// 校验密码强度
if (! validatePassword(user.password)) throw new Exception("密码强度不符合要求")
// 持久化用户信息
persistUser(user)
}
private def validateMobile(mobile: String): Boolean = ???
private def validateOtp(otp: String): Boolean = ???
private def validateLoginName(loginName: String): Boolean = ???
private def hasExisted(loginName: String): Boolean = ???
private def validatePassword(password: Vector[Char]): Boolean = ???
private def persistUser(user: User): Unit = ???
}
So far, so good!
减少编码错误
上线后,这段代码正常运行了一段时间,没有出现啥问题,Good!
But,But,在一次上线后,突然发现,所有的用户无法注册了,What !!!
在比较代码变更时发现,构建User对象时,一个小伙伴无意中将otp参数赋给了loginName!编译、发布,一切正常,但在运行时,校验不通过,所以捅了大篓子!
当然,我们可以通过代码之外的手段来减少这种错误,比如测试。但一些常规更新中,很可能漏掉无关的一些功能的测试,从“反求诸己”的原则自我要求的话,我们必须检视,有没有针对这种错误的改进的空间?我们的代码是否有足够的自我防御能力,尽早发现这种错误呢?
一种可选的答案是Typeful 。也就是强类型且类型完全的,让编译器对类型进行检查,在编译期间就为我们排出这种低级错误。
让我们再次审视我们的代码,它是否真的反应了业务?业务用例说,用户注册要素包含手机号码
、验证码
等,再看看我们的User
类是怎样对这两个要素建模的。发现差异了吗?loginName
和 otp
两个参数都是String
类型, 我们用属性名称来建模,而没有使用类型来建模!然而编译器不会检查变量名,但会检查变量的类型(Scala是强类型语言)。
让我们充分利用强类型,让编译器替我们干最脏最累的活吧!Let's Do it!
/** 手机号码
*/
case class Mobile(value: String) extends AnyVal
/** 手机验证码
*/
case class OTP(value: String) extends AnyVal
/** 登录名
*/
case class LoginName(value: String) extends AnyVal
/** 密码
*/
case class Password(value: Vector[Char]) extends AnyVal
/** 用户
*/
case class User(mobile: Mobile, otp: OTP, loginName: LoginName, password: Password)
嗯,这样一来,我们的代码更加类型安全了!不会再出现错传参数的低级错误了。因此,我们要尽可能的Typeful
,让编译器检查低级错误。
更深一层考虑,我们在做一件事情:SOC(Separation Of Concerns
), 分离的是什么呢?我们分离的是编译和运行,充分利用编译
的类型检查职责,避免将类型的检查延迟到运行时!之前的代码很明显没有意识到这种分离。(除了使用变量名称来指称业务含义,我们经常犯的错还包括在运行时对对象进行类型检查,根据对象的类型决定业务的走向)
这种重构,在实际的开发过程中出现过,比如在开发加解密工具包时,一开始对所有的参数都是用
Array[Byte]
类型,导致外部调用方经常讲公钥与私钥参数顺序搞反了,自己的单元测试完全通过,但集成到其他模块时就出错,查这种错花了不少时间,可谓是教训深刻!