一、场景介绍
假设有这么一个朝廷,它有 县-->府-->省-->朝廷,四级行政机构。
这四级行政机构的关系如下表:
1、县-->府-->省-->朝廷:有些地方有完整的四级行政机构。
2、县-->府-->朝廷:直隶府,朝廷直隶。
3、县-->省-->朝廷:有些地方可以没有府级行政机构。
4、县-->朝廷:直隶县,朝廷直隶。
朝廷规定,
县地方收上来的赋税,县衙可以留存10%,府署可以留存20%,省署可以留存30%。
但倘若下一级行政机构缺失,它的比例累加到上一级。
最后可能的赋税分配比例如下:
现在有一个县,收上来10万两白银,请设计一种模型,来计算各个层级行政机构所能分配到的赋税金额。
二、思路分析
如果按照正常的程序设计思路,伪代码可能如下:
public void computerTax() {if (县的上级是朝廷) {计算县的赋税计算朝廷的赋税} else if(县的上级是府) {计算县的赋税获取县的上级府if (府的上级是朝廷) {计算府的赋税计算朝廷的赋税} else if(府的上级是省) {计算省的赋税计算朝廷的赋税}} else if(县的上级是省) {计算省的赋税计算朝廷的赋税}}
分支语句特别多,条件判断又臭又长,可读性跟可维护性都很差,这还只是四级,如果行政区划层级再多一点,那么这个方法可能会有上千行,堆屎山一样。
下面,我们用责任链来改造这个方法。
三、代码实现
1、枚举设计
设计一个枚举,用来表示行政等级
import lombok.AllArgsConstructor;
import lombok.Getter;/*** 行政等级枚举*/
@AllArgsConstructor
@Getter
public enum GovGradeEnum {IMPERIAL_COURT(0, "朝廷"),PROVINCE(1, "省"),RESIDENCE(2, "府"),COUNTY(3, "县");private final Integer code;private final String name;
}
2、表结构设计
create table gov_division
(id int auto_increment comment '主键'primary key,name varchar(20) not null comment '区划名称',grade int not null comment '区划所处等级',parent_id int not null comment '区划上一级ID'
)comment '行政区划表';
create table tax
(id int auto_increment comment '主键'primary key,grade int not null comment '区划等级',division_id int not null comment '区划ID',rate decimal(10, 2) not null comment '赋税比例',amount decimal(10, 2) not null comment '赋税金额'
) comment '赋税分配表';
3、对象设计
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;import lombok.Data;@Data
@TableName("gov_division")
public class GovDivision implements Serializable {private static final long serialVersionUID = 1L;/** 主键 */@TableId(value = "id", type = IdType.AUTO)private Integer id;/** 区划名称 */private String name;/** 区划所处等级 */private Integer grade;/** 区划上一级ID */private Integer parentId;
}
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.*;/*** 行政区划表 Mapper 接口*/
@Mapper
public interface GovDivisionMapper extends BaseMapper<GovDivision> {
}
给对象添加 @Builder 注解是为了方便后面用构造器方式构造对象。
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import java.io.Serializable;
import java.math.BigDecimal;import lombok.Builder;
import lombok.Data;@Data
@TableName("tax")
@Builder
public class Tax implements Serializable {private static final long serialVersionUID = 1L;/** 主键 */@TableId(value = "id", type = IdType.AUTO)private Integer id;/** 区划等级 */private Integer grade;/** 区划ID */private Integer divisionId;/** 赋税比例 */private BigDecimal rate;/** 赋税金额 */private BigDecimal amount;
}
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.*;import java.util.List;/*** 赋税 Mapper 接口*/
@Mapper
public interface TaxMapper extends BaseMapper<Tax> {void batchInsert(List<Tax> list);
}
TaxMapper.xml:
<insert id="batchInsert" parameterType="java.util.List">INSERT INTO tax (grade, division_id, rate, amount) VALUES<foreach collection="list" item="item" index="index" separator=",">(#{item.grade}, #{item.divisionId}, #{item.rate}, #{item.amount})</foreach>
</insert>
4、责任链设计
4.1 责任处理接口
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;/** 责任处理接口 */
public interface TaxHandler {/*** 责任链处理接口* @param param 入参* @param config 赋税比例配置* @param sum 累计比例* @param use 使用了多少比例* @param taxList 生成的赋税列表*/void handle(ReqParam param, Map<Integer, BigDecimal> config, BigDecimal sum, BigDecimal use, List<Tax> taxList);/** 构造赋税默认方法 */default Tax buildTax(Integer divisionId, Integer grade, BigDecimal rate, BigDecimal amount) {returnTax.builder().divisionId(divisionId).grade(grade).rate(rate).amount(amount).build();}
}
import lombok.Data;
import java.math.BigDecimal;@Data
public class ReqParam {/** 区划ID */private Integer id;/** 区划等级 */private Integer grade;/** 上级区划ID */private Integer parentId;/** 今年赋税总额 */private BigDecimal totalTax;
}
4.2 责任处理实现类
我们实现 县、府、省、朝廷 四个实现类,让他们组成
县-->府-->省-->朝廷 这样一条责任链,并在请求参数中带上 grade,让每一个层级只处理自己 grade 的请求。
import lombok.AllArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;import java.math.BigDecimal;
import java.util.List;
import java.util.Map;/** 县处理者 */
@Service
@AllArgsConstructor
public class CountryTaxHandler implements TaxHandler {/** 引用府处理者 */private final ResidenceTaxHandler residenceTaxHandler;private final GovDivisionMapper govDivisionMapper;@Overridepublic void handle(ReqParam param, Map<Integer, BigDecimal> config,BigDecimal sum, BigDecimal use, List<Tax> taxList) {sum = sum.add(config.get(GovGradeEnum.COUNTY.getCode()));if (GovGradeEnum.COUNTY.getCode().equals(param.getGrade())) {taxList.add(buildTax(param.getId(), param.getGrade(), sum,param.getTotalTax().multiply(sum)));use = use.add(sum);sum = BigDecimal.ZERO;// 获取上级GovDivision govDivision = govDivisionMapper.selectById(param.getParentId());BeanUtils.copyProperties(govDivision, param);// 责任链向下传递residenceTaxHandler.handle(param, config, sum, use, taxList);} else {// 责任链向下传递residenceTaxHandler.handle(param, config, sum, use, taxList);}}
}
import lombok.AllArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;import java.math.BigDecimal;
import java.util.List;
import java.util.Map;/** 府处理者 */
@Service
@AllArgsConstructor
public class ResidenceTaxHandler implements TaxHandler {/** 引用省处理者 */private final ProvinceTaxHandler provinceTaxHandler;private final GovDivisionMapper govDivisionMapper;@Overridepublic void handle(ReqParam param, Map<Integer, BigDecimal> config,BigDecimal sum, BigDecimal use, List<Tax> taxList) {sum = sum.add(config.get(GovGradeEnum.RESIDENCE.getCode()));if (GovGradeEnum.RESIDENCE.getCode().equals(param.getGrade())) {taxList.add(buildTax(param.getId(), param.getGrade(), sum,param.getTotalTax().multiply(sum)));use = use.add(sum);sum = BigDecimal.ZERO;// 获取上级GovDivision govDivision = govDivisionMapper.selectById(param.getParentId());BeanUtils.copyProperties(govDivision, param);provinceTaxHandler.handle(param, config, sum, use, taxList);} else {provinceTaxHandler.handle(param, config, sum, use, taxList);}}
}
import lombok.AllArgsConstructor;
import org.springframework.beans.BeanUtils;
import org.springframework.stereotype.Service;import java.math.BigDecimal;
import java.util.List;
import java.util.Map;/** 省处理者 */
@Service
@AllArgsConstructor
public class ProvinceTaxHandler implements TaxHandler {/** 引用朝廷 */private ImperialCourtTaxHandler imperialCourtTaxHandler;private final GovDivisionMapper govDivisionMapper;@Overridepublic void handle(ReqParam param, Map<Integer, BigDecimal> config,BigDecimal sum, BigDecimal use, List<Tax> taxList) {sum = sum.add(config.get(GovGradeEnum.PROVINCE.getCode()));if (GovGradeEnum.PROVINCE.getCode().equals(param.getGrade())) {taxList.add(buildTax(param.getId(), param.getGrade(), sum,param.getTotalTax().multiply(sum)));use = use.add(sum);sum = BigDecimal.ZERO;// 获取上级GovDivision govDivision = govDivisionMapper.selectById(param.getParentId());BeanUtils.copyProperties(govDivision, param);imperialCourtTaxHandler.handle(param, config, sum, use, taxList);} else {imperialCourtTaxHandler.handle(param, config, sum, use, taxList);}}
}
import lombok.AllArgsConstructor;
import org.springframework.stereotype.Service;import java.math.BigDecimal;
import java.util.List;
import java.util.Map;/** 朝廷 */
@Service
@AllArgsConstructor
public class ImperialCourtTaxHandler implements TaxHandler {@Overridepublic void handle(ReqParam param, Map<Integer, BigDecimal> config,BigDecimal sum, BigDecimal use, List<Tax> taxList) {BigDecimal rate = BigDecimal.ONE.subtract(use);if (GovGradeEnum.IMPERIAL_COURT.getCode().equals(param.getGrade())) {taxList.add(buildTax(param.getId(), param.getGrade(), rate,param.getTotalTax().multiply(rate)));}// 责任链结束}
}
五、测试用例
1、直隶县1今年交了10万两白银:
import lombok.extern.slf4j.Slf4j;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;@RunWith(SpringRunner.class)
@SpringBootTest(classes = AlMallApplication.class)
@Slf4j
public class TaxTest {/** 税收留存比例配置类,这里为了简单自己写死,也可以来自数据库 */private Map<Integer, BigDecimal> config;@Autowiredprivate TaxMapper taxMapper;@Autowiredprivate CountryTaxHandler countryTaxHandler;@Autowiredprivate GovDivisionMapper govDivisionMapper;@Beforepublic void init() {config = new HashMap<>();config.put(GovGradeEnum.COUNTY.getCode(), BigDecimal.valueOf(0.1));config.put(GovGradeEnum.RESIDENCE.getCode(), BigDecimal.valueOf(0.2));config.put(GovGradeEnum.PROVINCE.getCode(), BigDecimal.valueOf(0.3));}@Testpublic void test1() {// 直隶县1-->朝廷GovDivision country = govDivisionMapper.selectById(2);ReqParam param = new ReqParam();// 直隶县1 今年交了10万两白银param.setTotalTax(BigDecimal.valueOf(10));BeanUtils.copyProperties(country, param);List<Tax> list = new ArrayList<>();countryTaxHandler.handle(param, config, BigDecimal.ZERO, BigDecimal.ZERO, list);taxMapper.batchInsert(list);}
}
2、无府县1今年交了10万两白银
import lombok.extern.slf4j.Slf4j;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;@RunWith(SpringRunner.class)
@SpringBootTest(classes = AlMallApplication.class)
@Slf4j
public class TaxTest {/** 税收留存比例配置类,这里为了简单自己写死,也可以来自数据库 */private Map<Integer, BigDecimal> config;@Autowiredprivate TaxMapper taxMapper;@Autowiredprivate CountryTaxHandler countryTaxHandler;@Autowiredprivate GovDivisionMapper govDivisionMapper;@Beforepublic void init() {config = new HashMap<>();config.put(GovGradeEnum.COUNTY.getCode(), BigDecimal.valueOf(0.1));config.put(GovGradeEnum.RESIDENCE.getCode(), BigDecimal.valueOf(0.2));config.put(GovGradeEnum.PROVINCE.getCode(), BigDecimal.valueOf(0.3));}@Testpublic void test2() {// 无府县1-->无府省1-->朝廷GovDivision country = govDivisionMapper.selectById(4);ReqParam param = new ReqParam();// 无府县1 今年交了10万两白银param.setTotalTax(BigDecimal.valueOf(10));BeanUtils.copyProperties(country, param);List<Tax> list = new ArrayList<>();countryTaxHandler.handle(param, config, BigDecimal.ZERO, BigDecimal.ZERO, list);taxMapper.batchInsert(list);}
}
3、无省县1今年交了10万两白银
import lombok.extern.slf4j.Slf4j;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;@RunWith(SpringRunner.class)
@SpringBootTest(classes = AlMallApplication.class)
@Slf4j
public class TaxTest {/** 税收留存比例配置类,这里为了简单自己写死,也可以来自数据库 */private Map<Integer, BigDecimal> config;@Autowiredprivate TaxMapper taxMapper;@Autowiredprivate CountryTaxHandler countryTaxHandler;@Autowiredprivate GovDivisionMapper govDivisionMapper;@Beforepublic void init() {config = new HashMap<>();config.put(GovGradeEnum.COUNTY.getCode(), BigDecimal.valueOf(0.1));config.put(GovGradeEnum.RESIDENCE.getCode(), BigDecimal.valueOf(0.2));config.put(GovGradeEnum.PROVINCE.getCode(), BigDecimal.valueOf(0.3));}@Testpublic void test3() {// 无省县1-->无省府1-->朝廷GovDivision country = govDivisionMapper.selectById(6);ReqParam param = new ReqParam();// 无省县1 今年交了10万两白银param.setTotalTax(BigDecimal.valueOf(10));BeanUtils.copyProperties(country, param);List<Tax> list = new ArrayList<>();countryTaxHandler.handle(param, config, BigDecimal.ZERO, BigDecimal.ZERO, list);taxMapper.batchInsert(list);}
}
4、正常县1今年交了10万两白银
import lombok.extern.slf4j.Slf4j;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;@RunWith(SpringRunner.class)
@SpringBootTest(classes = AlMallApplication.class)
@Slf4j
public class TaxTest {/** 税收留存比例配置类,这里为了简单自己写死,也可以来自数据库 */private Map<Integer, BigDecimal> config;@Autowiredprivate TaxMapper taxMapper;@Autowiredprivate CountryTaxHandler countryTaxHandler;@Autowiredprivate GovDivisionMapper govDivisionMapper;@Beforepublic void init() {config = new HashMap<>();config.put(GovGradeEnum.COUNTY.getCode(), BigDecimal.valueOf(0.1));config.put(GovGradeEnum.RESIDENCE.getCode(), BigDecimal.valueOf(0.2));config.put(GovGradeEnum.PROVINCE.getCode(), BigDecimal.valueOf(0.3));}@Testpublic void test4() {// 正常县1-->正常府1-->正常省1-->朝廷GovDivision country = govDivisionMapper.selectById(9);ReqParam param = new ReqParam();// 正常县1 今年交了10万两白银param.setTotalTax(BigDecimal.valueOf(10));BeanUtils.copyProperties(country, param);List<Tax> list = new ArrayList<>();countryTaxHandler.handle(param, config, BigDecimal.ZERO, BigDecimal.ZERO, list);taxMapper.batchInsert(list);}}
六、总结
先说缺点:
责任链的缺点,是造成类的膨胀。
大家仔细观察上面的代码,会发现,责任处理类,好像是把刚开始的 if else 伪代码,分到一个个处理类里面去了而已。
而且使用设计模式,它甚至并没有提高代码的执行效率。
优点:
写代码并不是做外包,写完就扔,它还得考虑可读性、可维护性和可扩展性。
可维护性和可扩展性的前提,就是可读性。
一段代码如果过几天,连自己都看不明白,那这种代码,它基本就没有什么可维护性了。
if else 不是不能写,而是如果分支太多,自己调试的时候,都不知道要走过多少 if else 才能到达自己的断点,那这样的代码,你怎么修改?改完你又怎么回归测试?
责任链的优点,恰恰就是它的可读性、可扩展性非常好。
每个处理类只处理自己职责范围内的消息,对于其他消息一律往下传。把变化封装在每一个类里面。对于上面的例子,如果现在有新的层级,我们只需要加一个枚举类型 grade,在加一个处理类,并把处理类加入到责任链里面,这样的可扩展性大大提高。