美文网首页开源软件实验室
用户注册服务进阶(一)

用户注册服务进阶(一)

作者: 大刀 | 来源:发表于2018-12-12 17:29 被阅读70次
    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]类型,导致外部调用方经常讲公钥与私钥参数顺序搞反了,自己的单元测试完全通过,但集成到其他模块时就出错,查这种错花了不少时间,可谓是教训深刻!

    相关文章

      网友评论

        本文标题:用户注册服务进阶(一)

        本文链接:https://www.haomeiwen.com/subject/xfxphqtx.html