Web App 架構的二三事

2020/11/06

Coding Guidelines

變量

MVC

總原則

  • 將任何異常(例如NullPointerException)、字段無效(例如email.invalid)、業務不合法(例如user.inactivated)的判斷,阻斷在外層,進入到下一層必定是正確的代碼。
  • 同上,發現任何異常、無效、不合法的判斷,及早拋出、中斷執行,讓外層回傳結果一定是正確的。
getUser() {
    check(user)
    check(user.id.valid) --> throw
}

HTTP Status

  1. 非系统错误不要乱传500,例如业务上逻辑的错误,不然容易造成判断困扰,不知道是哪一方出了问题
  2. 如果系统上没有问题,全部传200也可以,只要统一好就可以了

Return api design

  1. 成功:完全成功,不用做後續
  2. 錯誤
    1. 系統上的錯誤:例如TimeOut / IO –> 這個要拋出異常
    2. 檢驗上的錯誤:例如參數錯誤/字段不合法 –> 使用annotation捕獲,封裝bean,抛出
    3. 業務上的錯誤:例如查找一個不存在的數據/權限不足/尚未啟動無法操作/點數不足/密碼錯誤/重複登入 –> 封裝bean,错误码,或是其他业务流程处理

返回格式:

  • 無論正確/錯誤/異常都統一一種返回格式
  • code
  • message –> use enum
  • pagination
    • page: 当前第几页
    • size: 当前页数总共多少数据
    • total: 总数据量
{
  "code": 1,
  "message": "success",
  "data": {
      "pagination": {

      }
  }
}
{
  "code": 1,
  "message": "success",
  "data": [

  ]
}

page:

api response design:

constants:

Global

  • 系統設置

Controller

  • 處理任何的HttpRequest或是HttpResponse的邏輯,校驗此連線是否合法,有無異常,不處理業務邏輯
  • Controller层不会信任页面的校验,所以页面做过的校验都会再做一次,它要对service层负责
  • controller層接收的應該是完全沒加工的,或是已經加工完畢,可以直接回傳前端的資訊
  • 要有統一的返回接口,所有接口一致,不能一下String,一下Map(返回格式不统一),也不能返回void(没有考虑失败情况),全部回傳封裝過的Response Entity
if (balance == null ) {
  return HttpStatusCode(400);
}

Service

  • service层要对dao层负责,它就需要做业务逻辑的校验,比如添加的用户数据库中是否已存在,更新的数据数据库中有没有等等
  • 经过检验层的数据,则应该信任数据的正确性,只做业务处理(有争议)
assert balance > 0

Ref: 邏輯歸何處

Manager

  • 对第三方平台封装的层,预处理返回结果及转化异常信息
  • 对 Service 层通用能力的下沉,如缓存方案、中间件通用处理
  • 与 DAO 层交互,对多个 DAO 的组合复用。

Bean

  • 如果是一些基础验证,可以用Spring Hibernate Validation
  • 不要在gettersetter寫業務邏輯
  • 不應該開放default constructor,也就是應該把bean再次封裝,做成static factory method 給外面呼叫

如下:(錯誤的)

public class ArticleService {
   public Article create(String author, String content) {
      User user = userDao.findByName(author);
      checkPermissionForCreatingArticle(user);

      Article article = new Article();
      article.setAuthor(user);
      article.setContent(HtmlUtil.escape(content));
      article.setCreateTime(Instant.now());
      
      articleDao.save(article);
      return article;
   }
}

(正確的)

public class ArticleService {
   public Article create(String author, String content) {
      User user = userDao.findByName(author);
      checkPermissionForCreatingArticle(user);
      Article article = Article.create(user, content);
      return articleDao.insert(article);
   }
}

Dao

  • SQL寫在這

Ref: sql是不是可以写在service层?虽然service是业务层_y_dzaichirou的博客-CSDN博客

Reference

異常處理

原則

  • 方法的返回值可以为null,不强制返回空集合,或者空对象等,必须添加注释充分说明什么情况下会返回null值。(阿里巴巴)
  • 只拋出系統錯誤,不拋出業務錯誤
  • 只要有任何异常,全部抛出,不要各自返回return,因为在出事前没人会检查日志

不好的寫法:

        //TODO:验证玩家令牌
        Player player = playerDao.get(userId);
        if (player == null) {
            return Result.createErrorResult().setStatus(100).setMsg("玩家不存在");
        }
        if (!player.getToken().equals(token)) {
            return Result.createErrorResult().setStatus(101).setMsg("该玩家token不匹配");
        }
        
        //TODO:验证漫画
        Comic comic = dao.get(comicId);
        if (comic == null) {
            return Result.createErrorResult().setStatus(102).setMsg("漫画不存在");
        }
        
        //TODO:验证打赏类型
        RewardType rt = rewardTypeDao.get(rewardType);
        if (rt == null) {
            return Result.createErrorResult().setStatus(105).setMsg("打赏类型不合法");
        }
        Integer rewardNum = rt.getRewardNum();
  • 检查null的想法:如果出现 null 是正常现象,可以通过检查 null 来避免 NPE。如果不应该出现 null 却出现了 null,那就说明程序有问题,就要解决问题,而不是通过检查 null 来掩盖问题。
  • 異常三原則:
    • 具体明确:盡量縮小try catch範圍,定位清楚可能的異常位置
    • 提早抛出:不应该用异常控制流程
    • 延迟捕获:传递一个危险信号,需要让调用方知道
  • 不抛出异常情况:
    • 本方法没有能力处理的异常,调用方有能力处理,抛出是框架层面的选择
    • 在service层抛出异常后,必须在controller或更高层统一捕获处理,格式化后返回
  • 好处:
    • Java的函数参数就是输入,返回值就是输出
    • 业务回滚,这样可以使用spring的业务注解Transaction来控制业务
  • 坏处:
    • 异常不要用来做流程控制,条件控制
    • 异常设计的初衷是解决程序运行中的各种意外情况,且异常的处理效率比条件判断方式要低很多 (但其實不用扣細節,系統內開銷最大的是IO和線程,處理好這兩個比起異常的效率是立竿見影的)

Reference

異常處理:

Spring異常處理:

Spring



一位有故事的程序員

(Best Regard)

Show Disqus Comments

Post Directory

扫码关注公众号:吹雪
发送 290992
即可立即永久解锁本站全部文章