一、场景介绍
假设有这么一个朝廷,它有 县-->府-->省-->朝廷,四级行政机构。
这四级行政机构的关系如下表:
1、县-->府-->省-->朝廷:有些地方有完整的四级行政机构。
2、县-->府-->朝廷:直隶府,朝廷直隶。
3、县-->省-->朝廷:有些地方可以没有府级行政机构。
4、县-->朝廷:直隶县,朝廷直隶。
朝廷规定,
县地方收上来的赋税,县衙可以留存10%,府署可以留存20%,省署可以留存30%。
但倘若下一级行政机构缺失,它的比例累加到上一级。
最后可能的赋税分配比例如下:
现在有一个县,收上来10万两白银,请设计一种模型,来计算各个层级行政机构所能分配到的赋税金额。
二、思路分析
如果按照正常的程序设计思路,伪代码可能如下:
public void computeTax() {
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) {
return
Tax.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;
@Override
public 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;
@Override
public 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;
@Override
public 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 {
@Override
public 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;
@Autowired
private TaxMapper taxMapper;
@Autowired
private CountryTaxHandler countryTaxHandler;
@Autowired
private GovDivisionMapper govDivisionMapper;
@Before
public 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));
}
@Test
public 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;
@Autowired
private TaxMapper taxMapper;
@Autowired
private CountryTaxHandler countryTaxHandler;
@Autowired
private GovDivisionMapper govDivisionMapper;
@Before
public 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));
}
@Test
public 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;
@Autowired
private TaxMapper taxMapper;
@Autowired
private CountryTaxHandler countryTaxHandler;
@Autowired
private GovDivisionMapper govDivisionMapper;
@Before
public 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));
}
@Test
public 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;
@Autowired
private TaxMapper taxMapper;
@Autowired
private CountryTaxHandler countryTaxHandler;
@Autowired
private GovDivisionMapper govDivisionMapper;
@Before
public 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));
}
@Test
public 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,在加一个处理类,并把处理类加入到责任链里面,这样的可扩展性大大提高。