设计模式之美笔记2 布满荆棘的人生 2023-03-01 13:52 18阅读 0赞 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 ### 文章目录 ### * * 实战案例:虚拟钱包 * * 1. 业务背景 * * 1. 充值 * 2. 支付 * 3. 提现 * 4. 查询余额 * 5. 查询交易流水 * 2. 钱包系统的设计思路 * * 交易流水该如何记录和查询? * 是否应该在虚拟钱包系统的交易流水中记录充值、提现、支付这三种类型 * 那用户查流水时,如何展示每条交易流水的交易类型 * 3. 贫血模型MVC实现 * 4. 充血模型的DDD开发模式实现 * 5. 两个问题 * * 问题1 * 问题2 * 实战2:如何对接口鉴权这样一个功能开发做面向对象分析 * * 1. 案例介绍和难点分析 * 2. 对案例需求分析 * * 1. 第一轮基础分析 * 2. 第二轮优化 * 3. 第三轮优化 * 4. 第四轮优化 * 5. 最终确定需求 * 3. 如何进行面向对象设计 * * 1. 划分职责进而识别有哪些类 * 2. 定义类及其属性和方法 * 3. 定义类与类之间的交互关系 * 4. 将类组装起来并提供执行入口 * 4. 如何面向对象编程 * 5. 辩证思考和灵活运用 ## 实战案例:虚拟钱包 ## ### 1. 业务背景 ### 很多有支付、购买功能的应用都支持钱包的功能,应用为每个用户开设一个系统内的虚拟钱包账户,支持用户充值、提现、支付、冻结、透支、转赠、查询账户余额、查询交易流水等操作。 一般,每个虚拟钱包账户都会对应用户的一个真实的支付账户,可能是银行卡账户,或者支付宝、微信钱包等三方支付账户。暂时限定只支持充值、提现、支付、查询账户余额、查询交易流水五个核心功能。 具体业务流程 #### 1. 充值 #### 用户通过三方支付渠道,把自己银行卡账户的钱,充值到虚拟钱包账号中。可分解为三个主要的操作流程: 1. 从用户的银行卡账户转账到应用的公共银行卡账户 2. 将用户的充值金额加到虚拟钱包余额中 3. 记录刚刚的这笔交易流水 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy_size_16_color_FFFFFF_t_70] #### 2. 支付 #### 用户用钱包内的余额,支付购买应用内的商品。实际上,就是个转账的过程,从用户的虚拟钱包账户划钱到商家的虚拟钱包账户,然后出发真正的银行账户转账操作,从**应用的公共银行账户**转钱到商家的银行账户。此外,也要记录这笔支付的交易流水信息。 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy_size_16_color_FFFFFF_t_70 1] #### 3. 提现 #### 除了充值、支付外,用户可将虚拟钱包的余额,提现到自己的银行卡。实际就是扣减用户虚拟钱包中的余额,并触发真正的银行转账操作,从应用的公共银行账户转账到用户的银行账户。同样也要记录这笔提现的交易流水信息。 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy_size_16_color_FFFFFF_t_70 2] #### 4. 查询余额 #### 查询余额比较简单,看下虚拟钱包中的余额数字即可 #### 5. 查询交易流水 #### 只支持三种类型的交易流水:充值、支付、提现。会记录相应的交易信息。在需要查询时,将之前记录的交易流水,按照时间、类型等条件过滤后,展示即可。 ### 2. 钱包系统的设计思路 ### 根据上述业务流程和数据流转图,将业务分为两部分,一部分单纯跟应用的虚拟钱包账户打交道,一部分跟银行账户打交道。将钱包系统拆分为两个子系统:虚拟钱包系统和三方支付系统。 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy_size_16_color_FFFFFF_t_70 3] 接下来只关注虚拟钱包系统的设计和实现 要支持钱包的五个核心功能,虚拟钱包系统需要对应实现哪些操作。交易流水的记录和查询较特殊,之后再看。 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy_size_16_color_FFFFFF_t_70 4] 操作很简单,就是余额的加加减减。充值、提现、查询余额功能,只涉及一个账户余额的加减,而支付涉及两个账户的余额加减:一个账户减余额,一个账户加余额。 #### 交易流水该如何记录和查询? #### 交易流水需要包含的信息: * 交易流水ID * 交易时间 * 交易金额 * 交易类型:充值、提现、支付 * 入账钱包账号 * 出账钱包账号 为什么两个账号?为了兼容支付场景。为了保证数据一致性。对于业务来说,只需要保证最终一致性即可。 对于支付这种类似转账的操作,操作两个钱包的账户余额之前,先记录交易流水,并标记为“待执行”,当两个钱包的加减金额都完成后,再将交易流水标记为“成功”。否则,只要任意一个失败,都将状态标记为“失败”。通过后台task,拉取状态为“失败”或长时间处于“待执行”的交易记录,重新执行或人工处理。 #### 是否应该在虚拟钱包系统的交易流水中记录充值、提现、支付这三种类型 #### 虚拟钱包只需支持余额的加加减减,不涉及复杂业务概念,职责单一、功能通用。如果耦合太多业务概念,影响通用性,导致系统越做越复杂。 #### 那用户查流水时,如何展示每条交易流水的交易类型 #### 系统设计角度,不应该在虚拟钱包系统的交易流水中记录交易类型;产品需求角度,必须记录交易类型,如何解决该矛盾? 通过记录两条交易流水信息的方式解决。整个钱包系统分为两个子系统,对于上层钱包系统,可感知充值、支付、提现等业务概念,所以在钱包系统上层额外记录一条包含交易类型的交易流水信息。底层虚拟钱包系统不记录交易类型。 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy_size_16_color_FFFFFF_t_70 5] ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy_size_16_color_FFFFFF_t_70 6] 如上图,通过查询上层钱包系统的交易流水信息,来满足用户查询交易流水的功能需求,而虚拟钱包中的交易流水只用来解决数据一致性问题。 ### 3. 贫血模型MVC实现 ### controller层 public class VirtualWalletController{ //通过构造函数或者IOC框架注入 private VirtualWalletService virtualWalletService; public BigDecimal getBalance(Long walletId){ ...}//查询余额 public void debit(Long walletId,BigDecimal amount){ ...}//出账 public void credit(Long walletId,BigDecimal amount){ ...}//入账 public void transfer(Long fromWalletId,Long toWalletId, BigDecimal amount){ ...}//转账 } service层代码如下,省略了一些不重要的校验代码,如对amount是否小于0、钱包是否存在的校验等。 public class VirtualWalletBo{ // 省略getter、setter和constructor private Long id; private Long createTime; private BigDecimal balance; } public class VirtualWalletService{ //通过构造函数或IOC框架注入 private VirtualWalletDao walletDao; private VirtualWalletTransactionDao transactionDao; public VirtualWalletBo getVirtualWallet(Long walletId){ VirtualWalletEntity walletEntity = walletDao.getWalletEntity(walletId); VirtualWalletBo walletBo = convert(walletEntity); return walletBo; } public BigDecimal getBalance(Long walletId){ return virtualWalletDao.getBalance(walletId); } public void debit(Long walletId,BigDecimal amount){ VirtualWalletEntity walletEntity = walletDao.getWalletEntity(walletId); BigDecimal balance = walletEntity.getBalance(); if(balance.compareTo(amount)<0){ throw new NoSufficientBalanceException(...); } walletDao.updateBalance(walletId,balance.substract(amount)); } public void credit(Long walletId,BigDecimal amount){ VirtualWalletEntity walletEntity = walletDao.getWalletEntity(walletId); BigDecimal balance = walletEntity.getBalance(); walletDao.updateBalance(walletId,balance.add(amount)); } public void transfer(Long fromWalletId,Long toWalletId,BigDecimal amount){ VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactionEntity(); transactionEntity.setAmount(amount); transactionEntity.setCreateTime(System.currentTimeMills); transactionEntity.setFromWalletId(fromWalletId); transactionEntity.setToWalletId(toWalletId); transactionEntity.setStatus(Status.TO_BE_EXECUTED); Long transactionId = transactionDao.saveTransaction(transactionEntity); try{ debit(fromWalletId,amount); credit(toWalletId,amount); }catch(InsufficientBalanceException e){ transactionDao.updateStatus(transactionId,Status.CLOSED); ...throw exception e... }catch(Exception e){ transactionDao.updateStatus(transactionId,Status.FAILED); ...throw exception e... } transactionDao.updateStatus(transactionId,Status.EXECUTED); } } ### 4. 充血模型的DDD开发模式实现 ### 只有service层不同,把虚拟钱包VirtualWallet类设计成一个充血的Domain领域模型,并将原来在service类的部分业务逻辑移到VirtualWallet类中,让service类的实现依赖VirtualWallet类。 public class VirtualWallet{ //Domain 领域模型 private Long id; private Long createTime = System.currentTimeMills; private BigDecimal balance = BigDecimal.ZERO; public VirtualWallet(Long preAllocatedId){ this.id = preAllocatedId; } public BigDecimal balance(){ return this.balance; } public void debit(BigDecimal amount){ if(this.balance.compareTo(amount)<0){ throw new NoSufficientBalanceException(...); } this.balance.substract(amount); } public void credit(BigDecimal amount){ if(amount.compareTo(BigDecimal.ZERO)<0){ throw new InvalidAmountException(...); } this.balance.add(amount); } } public class VirtualWalletService{ //通过构造函数或IOC框架注入 private VirtualWalletDao walletDao; private VirtualWalletTransactionDao transactionDao; public VirtualWalletBo getVirtualWallet(Long walletId){ VirtualWalletEntity walletEntity = walletDao.getWalletEntity(walletId); VirtualWallet wallet = convert(walletEntity); return wallet; } public BigDecimal getBalance(Long walletId){ return virtualWalletDao.getBalance(walletId); } public void debit(Long walletId,BigDecimal amount){ VirtualWalletEntity walletEntity = walletDao.getWalletEntity(walletId); VirtualWallet wallet = convert(walletEntity); wallet.debit(amount); walletDao.updateBalance(walletId,wallet.balance()); } public void credit(Long walletId,BigDecimal amount){ VirtualWalletEntity walletEntity = walletDao.getWalletEntity(walletId); VirtualWallet wallet = convert(walletEntity); wallet.credit(amount); walletDao.updateBalance(walletId,wallet.balance()); } public void transfer(Long fromWalletId,Long toWalletId,BigDecimal amount){ VirtualWalletTransactionEntity transactionEntity = new VirtualWalletTransactionEntity(); transactionEntity.setAmount(amount); transactionEntity.setCreateTime(System.currentTimeMills); transactionEntity.setFromWalletId(fromWalletId); transactionEntity.setToWalletId(toWalletId); transactionEntity.setStatus(Status.TO_BE_EXECUTED); Long transactionId = transactionDao.saveTransaction(transactionEntity); try{ debit(fromWalletId,amount); credit(toWalletId,amount); }catch(InsufficientBalanceException e){ transactionDao.updateStatus(transactionId,Status.CLOSED); ...throw exception e... }catch(Exception e){ transactionDao.updateStatus(transactionId,Status.FAILED); ...throw exception e... } transactionDao.updateStatus(transactionId,Status.EXECUTED); } } 如果业务逻辑更加复杂,充血模型的优势就显示出来了。如要支持透支一定额度和冻结部分余额的功能。 public class VirtualWallet{ //Domain 领域模型 private Long id; private Long createTime = System.currentTimeMills; private BigDecimal balance = BigDecimal.ZERO; private boolean isAllowedOverdraft = true;//透支 private BigDecimal overdraftAmount = BigDecimal.ZERO; private BigDecimal frozenAmount = BigDecimal.ZERO; public VirtualWallet(Long preAllocatedId){ this.id = preAllocatedId; } public void freeze(BigDecimal amount){ ...} public void unfreeze(BigDecimal amount){ ...} public void increaseOverdraftAmount(BigDecimal amount){ ...} public void decreaseOverdraftAmount(BigDecimal amount){ ...} public void closeOverdraft(){ ...} public void openOverdraft(){ ...} public BigDecimal balance(){ return this.balance; } public BigDecimal getAvaliableBalance(){ BigDecimal totalAvaliableBalance = this.balance.substract(this.frozenAmount); if(isAllowedOverdraft){ totalAvaliableBalance += this.overdraftAmount; } return totalAvaliableBalance; } public void debit(BigDecimal amount){ BigDecimal totalAvaliableBalance = this.balance.substract(this.frozenAmount); if(totalAvaliableBalance.compareTo(amount)<0){ throw new NoSufficientBalanceException(...); } this.balance.substract(amount); } public void credit(BigDecimal amount){ if(amount.compareTo(BigDecimal.ZERO)<0){ throw new InvalidAmountException(...); } this.balance.add(amount); } } 当然,随着功能的演进,可增加更加细化的冻结策略、透支策略、支持钱包账号(VirtualWalletId字段)自动生成的逻辑(不是通过构造函数经外部传入id,而是分布式id生成算法自动生成id)等。业务越复杂,就很值得设计为充血模型。 ### 5. 两个问题 ### #### 问题1 #### 基于充血模型的DDD开发模式,将业务逻辑移到domain,service类变得很薄,能否直接将service类去掉?service类的职责是什么?哪些逻辑放到service中的? service类主要有以下几个职责: 1. service类与dao层交互。我们需要保持领域模型domain的独立性,不和其他层的代码耦合,让domain更加可复用。 2. service类负责跨domain的业务逻辑聚合功能。如VirtualWalletService类的transfer()转账行数会涉及两个钱包的操作,无法放到VirtualWallet类中。将转账业务放到VIrtualWalletService类中,随着功能的演进,转账业务更复杂后,可以将转账业务抽取出来,设计为一个独立的domain。 3. service类负责一些非功能性和与三方系统交互的工作。如幂等、事务、发邮件、发消息、记录日志、调用其他系统的RPC接口等,都可以放到service类中。 #### 问题2 #### 基于充血模型的DDD开发模式,service层被改造为充血模型,但controller和dao层都没改,是否有必要改造? 没必要,这两层包含的业务逻辑并不多。 controller的vo,实际上是一种DTO(Data Transfer Object,数据传输对象)。主要作为接口的数据传输承载体,将数据发送给其他系统,理应不包含业务逻辑,只包含数据。 ## 实战2:如何对接口鉴权这样一个功能开发做面向对象分析 ## ### 1. 案例介绍和难点分析 ### 假设正在参与开发一个微服务,通过http协议暴露接口给其他系统调用,也就是其他系统通过URL调用微服务的接口。一天,leader给你说,“为保证接口调用的安全性,希望设计实现一个接口调用鉴权功能,只有经过认证后的系统才能调我们的接口,没有认证过的调用会被拒绝,希望你负责这个任务的开发,争取尽快上线”。 leader很忙,说完就走了,你该如何做?是否感觉无从下手? 原因: 1. 需求不明确 需求过于笼统,不够具体,离落地编码还有一定距离。当然,真实的开发中,需求几乎都是不很明确。需求分析,首先做的就是将笼统的需求细化到可执行。通过沟通、挖掘、分析、假设、梳理,搞清楚具体的需求有哪些,哪些是现在要做的,哪些是未来可能要做的,哪些不用考虑的。 2. 缺少锻炼 相比单纯的业务CRUD,鉴权更有难度。作为和具体业务无关的功能,完全可以把它开发为一个独立的框架,集成到多个业务系统,对代码质量要求更高。开发通用的框架,对需求分析能力、设计能力、编码能力额逻辑思维能力的要求,都较高,平时简单的业务开发,这种锻炼机会不多,因此无从下手,没有思路。 ### 2. 对案例需求分析 ### 先要想到个MVP,最简单的可实现的demo #### 1. 第一轮基础分析 #### 最简单的就是通过用户名、密码认证。给每个调用方,派发一个应用名appId,和一个密码(或者叫密钥)。调用方每次对接口请求时,都携带自己的appId和密钥。微服务接收到请求后,解析appId和密码,跟存储在服务端的appId和密码对比,如果一致,说明认证成功,允许调用,否则拒绝请求。 #### 2. 第二轮优化 #### 这种每次都是明文传输密码,不安全。借助加密算法如SHA,加密后传到服务端验证,也不安全,如重放攻击。 如何解决?借助OAuth的验证思路。调用方将请求接口的URL跟appId和密码拼接到一起,再加密,生成token。调用方请求时,将token和appId跟随url一起传到服务端,服务端解析后,根据appId从数据库中获取密码,通过同样的token生成算法,生成新token,跟调用方传来的token对比。一致,允许请求,否则拒绝。 #### 3. 第三轮优化 #### 这种仍不太安全,url拼接appId、密码生成的token都是固定的,未认证系统截获url、token和appId后,还能重放攻击,调用这个url对应的接口。 我们需要引入一个随机变量,如时间戳,让每次生成的token都不一样。微服务端收到数据后,验证当前时间戳跟传递过来的时间戳,是否在一定的时间窗口内(如一分钟),如果超时,判定token过期,拒绝请求;没有超时,用同样的token生成算法,在服务端生成新token,对比是否一致。 #### 4. 第四轮优化 #### 攻防之间,本就没有绝对的安全,只是提高攻击的成本。该方案足够简单,也不过度影响接口本身的性能如响应时间。权衡安全性、开发成本、对系统性能的影响,该方案较合理。 其实还有个细节,就是如何在服务端存储每个授权调用方的appId和密码。鉴权这种非业务功能,最好不要和具体的第三方系统有过度的耦合,最好能灵活的支持不同存储方式,如zookeeper、本地配置文件、自研配置中心、mysql、redis等。最好留下扩展点,保证系统有足够的灵活性和扩展性。 #### 5. 最终确定需求 #### 跟leader描述清楚细化的需求。 > 调用方在进行接口请求的时候,将url、appId、密码、时间戳拼接在一起,通过加密算法生成token,并将token、appId、时间戳拼接到url中,一起发送到服务端。 > > 服务端接收到调用方请求后,解析出token、appId和时间戳。首先检查时间戳跟当前时间,是否在token失效时间窗口内,如果超过失效时间,调用鉴权失败,拒绝调用请求。 > > 如果没有过期,从自己的存储中取出appId对应的密码,用同样的token生成算法,生成另一个token,与调用方传的token匹配,一致允许调用,否则拒绝调用 > ps:可以和10X程序员一起服用,效果更好 ### 3. 如何进行面向对象设计 ### 面向对象分析产出的是详细的需求描述,面向对象设计的产出就是类。把设计环节细化后: * 划分职责进而识别有哪些类 * 定义类及其属性和方法 * 定义类与类之间的交互关系 * 将类组装并提供执行入口 #### 1. 划分职责进而识别有哪些类 #### 识别类的方法,把需求描述中的名词罗列出来,作为可能的候选项,再筛选。 另一种,根据需求描述,把其中涉及到的功能点,一个个罗列出来,再看哪些功能点职责相近,操作同样的属性,可否归为同一个类。 逐句拆解鉴权的需求描述后得到的功能点: 1. 把url、appId、密码、时间戳拼接为一个字符串 2. 对字符串通过加密算法加密生成token 3. 将token、appId、时间戳拼接到url,形成新的url 4. 解析url得到token、appId和时间戳 5. 从存储中取出appId和对应密码 6. 根据时间戳判断token是否过期 7. 验证两个token是否匹配 其中,1、2、6、7都跟token有关,负责token的生成、验证;3、4都在处理url,负责url的拼接和解析;5操作appId和密码。粗略得到三个核心类:AuthToken、Url、CredentialStorage。 这是初步的划分,编程本身就是不断迭代优化的过程。先有个基础的设计方案。 真正的大型软件开发、复杂的需求开发,涉及到的功能点较多,对应类也较多,需要先进行模块划分,将需求简单划分为几个小的功能模块,再在模块内部,应用刚才的方法,进行面向对象设计。套路类似。 #### 2. 定义类及其属性和方法 #### **AuthToken类的功能点有4个**: * 把url、appId、密码、时间戳拼接为一个字符串 * 对字符串通过加密算法加密生成token * 根据时间戳判断token是否过期 * 验证两个token是否匹配 对于方法的识别,一般都是识别出需求描述的动词,作为候选的方法,再筛选。把功能点涉及到的名词,作为候选属性,同样筛选。 AuthToken类 属性: private static final long DEFAULT_EXPIRED_TIME_INTERVAL = 1*60*1000; private String token; private long createTime; private long expiredTimeInterval = DEFAULT_EXPIRED_TIME_INTERVAL; 构造函数: public AuthToken(String token,long createTime); public AuthToken(String token,long createTime,long expiredTimeInterval); 方法: public static AuthToken create(String baseUrl,long createTime,Map<String,String> params); public String getToken(); public boolean isExpired(); public boolean match(AuthToken authToken); 有三个细节: 1. 并不是所有出现的名词都被定义为类的属性,如url、appId、密码、时间戳,我们把它作为方法的参数 2. 还需要挖掘一些没有出现在描述中的属性,如createTime,expireTimeInterval,用在isExpired()方法中,判定token是否过期 3. 还给AuthToken类添加了一个功能点描述中没提到的方法getToken() 第一个细节说明,从业务模型上说,不应该属于这个类的属性和方法,不要放到这个类里,如url、appId信息。 第二三个细节说明,不能简单的依赖当下的需求,还要从整个业务模型触发,看应该具备哪些属性和方法。一方面保证类定义的完整性,另一方面也为未来的需求做准备。 **url类的功能点有两个**: * 将token、appId、时间戳拼接到url,形成新的url * 解析url得到token、appId和时间戳 虽然需求描述中是url来指代接口请求,但接口请求不一定是url的形式,也可能是dubbo rpc等其他形式。为了让该类更加通用,命名更贴切,起名为ApiRequest。 ApiRequest类 属性: private String baseUrl; private String token; private String appId; private long timestamp; 构造函数: public ApiRequest(String baseUrl,String token,String appId,long timestamp); 方法: public static ApiRequest createFromFullUrl(String url); public String getBaseUrl(); public String getToken(); public String getAppId(); public long getTimestamp(); **而CredentialStorage类的相关的功能点有一个**: 从存储中取出appId和对应密码 该类很简单,为了做到抽象封装具体的存储方式,将其设计为接口,基于接口而非具体的实现编程 CredentialStorage接口 接口方法: String getPasswordByAppId(String appId); #### 3. 定义类与类之间的交互关系 #### UML统一建模语言定义了六种类之间的关系:泛化、实现、关联、聚合、组合、依赖。 泛化generalization可以简单理解为java的继承关系。 实现realization一般指接口和实现类之间的关系。 聚合aggregation是一种包含关系,A类对象包含B类对象,B类对象的生命周期可以不依赖A类对象的生命周期,如课程与学生之间的关系。 组合composition也是一种包含关系,只是B类对象的生命周期依赖A类对象的生命周期,B不能单独存在,如鸟跟翅膀之间的关系。 关联association是一种弱关系,只要B类对象是A类的成员变量,B类和A类就是关联关系。 依赖dependency是一种比关联关系更弱的关系,包含关联关系。只要B类对象和A类对象有任何使用关系,都称为依赖关系。 从更贴近编程的角度,对类与类之间的关系做调整,只保留4个关系:泛化、实现、组合、依赖。组合关系替代了UML的组合、聚合、关联三个概念,只要B类对象是A类对象的成员变量,就称A类跟B类是组合关系。 那刚定义的三个类之间有哪些关系?只用到了实现关系,也就是CredentialStorage和MysqlCredentialStorage之间的关系。 #### 4. 将类组装起来并提供执行入口 #### 这个入口可能是main函数,也可能是一组给外部调用的Api接口,通过该入口,触发代码跑起来。 接口鉴权不是一个独立运行的系统,而是一个集成在系统上运行的组件,封装所有实现细节,设计一个顶层的ApiAuthencator接口类,暴露给外部调用者使用的API接口,作为触发执行鉴权逻辑的入口。 ApiAuthencator接口 接口方法: void auth(String url); void auth(ApiRequest apiRequest); DefaultApiAuthencatorImpl实现类 属性: private CredentialStorage credentailStorage; 构造方法: public ApiAuthencator(); public ApiAuthencator(CredentialStorage credentialStorage); 方法: void auth(String url); void auth(ApiRequest apiRequest); #### 4. 如何面向对象编程 #### 只需要将类图翻译为代码,只给出较为复杂的ApiAuthencator的实现 public interface ApiAuthencator{ void auth(String url); void auth(ApiRequest apiRequest); } public class DefaultApiAuthencatorImpl implements ApiAuthencator{ private CredentialStorage credentialStorage; public ApiAuthencator(){ this.credentialStorage = new MysqlCredentialStorage(); } public ApiAuthencator(CredentialStorage credentialStorage){ this.credentialStorage = credentialStorage; } @Override public void auth(String url){ ApiRequest apiRequest = ApiRequest.buildFromUrl(url); auth(apiRequest); } @Override public void auth(ApiRequest apiRequest){ String appId = apiRequest.getAppId(); String token = apiRequest.getToken(); long timestamp = apiRequest.getTimestamp(); String originalUrl = apiRequest.getOriginalUrl(); AuthToken clientAuthToken = new AuthToken(token,timestamp); if(clientAuthToken.isExpired()){ throw new RuntimeException("token is expired"); } String password = credentialStorage.getPasswordByAppId(appId); AuthToken serverAuthToken = AuthToken.generate(originalUrl,appId,password); if(!serverAuthToken.match(clientAuthToken)){ throw new RuntimeException("token verfication failed"); } } } #### 5. 辩证思考和灵活运用 #### 其实代码一般都是边写边重构、迭代,就像学驾照,驾校的流程很正规,按照流程顺利倒库,实际开车熟练后,都是根据经验和感觉。 > 遵循这套SOP,方法单个不超过五十行,我们平时都是将各种东西都塞到方法里,导致面向过程编程,非常臃肿。 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy_size_16_color_FFFFFF_t_70]: /images/20230208/386dacb76cf743c49192a905c58a2e0d.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy_size_16_color_FFFFFF_t_70 1]: https://img-blog.csdnimg.cn/20200801110328412.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy_size_16_color_FFFFFF_t_70 2]: https://img-blog.csdnimg.cn/20200801110346758.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy_size_16_color_FFFFFF_t_70 3]: https://img-blog.csdnimg.cn/20200801110357977.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy_size_16_color_FFFFFF_t_70 4]: https://img-blog.csdnimg.cn/20200801110408850.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy_size_16_color_FFFFFF_t_70 5]: https://img-blog.csdnimg.cn/2020080111042365.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy_size_16_color_FFFFFF_t_70 6]: https://img-blog.csdnimg.cn/20200801110500161.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dqbDMxODAy,size_16,color_FFFFFF,t_70
相关 设计模式之美笔记2 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 实战案例:虚拟钱包 1. 业务背景 布满荆棘的人生/ 2023年03月01日 13:52/ 0 赞/ 19 阅读
相关 设计模式之美笔记16 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 解释器模式 解释器模式的原理和实现 深藏阁楼爱情的钟/ 2022年12月01日 11:53/ 0 赞/ 207 阅读
相关 设计模式之美笔记15 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 访问者模式 访问者模式的诞生 我就是我/ 2022年12月01日 05:16/ 0 赞/ 205 阅读
相关 设计模式之美笔记14 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 状态模式 背景 什么 水深无声/ 2022年11月30日 15:51/ 0 赞/ 236 阅读
相关 设计模式之美笔记13 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 策略模式 策略模式的原理和实现 忘是亡心i/ 2022年11月30日 12:27/ 0 赞/ 225 阅读
相关 设计模式之美笔记12 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 观察者模式 原理及应用场景剖析 深碍√TFBOYSˉ_/ 2022年11月30日 04:18/ 0 赞/ 255 阅读
相关 设计模式之美笔记11 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 门面模式 门面模式的原理和实现 ゞ 浴缸里的玫瑰/ 2022年11月28日 13:41/ 0 赞/ 238 阅读
相关 设计模式之美笔记10 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 序言 代理模式 桥接模式 柔情只为你懂/ 2022年11月28日 10:36/ 0 赞/ 234 阅读
相关 设计模式之美笔记9 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 工厂模式 1. 简单工厂 待我称王封你为后i/ 2022年11月28日 00:41/ 0 赞/ 243 阅读
相关 设计模式之美笔记7 > 记录学习王争的设计模式之美 课程 笔记和练习代码,以便回顾复习,共同进步 文章目录 实战1:id生成器的重构 1. 需求背景 女爷i/ 2022年11月25日 13:19/ 0 赞/ 266 阅读
还没有评论,来说两句吧...