用户注册服务进阶(一)

Hello, Users

通过本系列文章,我试图通过一个简单的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类是怎样对这两个要素建模的。发现差异了吗?loginNameotp 两个参数都是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,让编译器检查低级错误。

更深一层考虑,我们在做一件事情:SOCSeparation Of Concerns), 分离的是什么呢?我们分离的是编译和运行,充分利用编译的类型检查职责,避免将类型的检查延迟到运行时!之前的代码很明显没有意识到这种分离。(除了使用变量名称来指称业务含义,我们经常犯的错还包括在运行时对对象进行类型检查,根据对象的类型决定业务的走向)

这种重构,在实际的开发过程中出现过,比如在开发加解密工具包时,一开始对所有的参数都是用Array[Byte]类型,导致外部调用方经常讲公钥与私钥参数顺序搞反了,自己的单元测试完全通过,但集成到其他模块时就出错,查这种错花了不少时间,可谓是教训深刻!

你可能感兴趣的:(用户注册服务进阶(一))