一.利用工厂模式+模板方法模式
我们以做蛋糕为例来演示如何消除重复代码。
假设我们要做3种不同口味的蛋糕,分别是抹茶,可可和草莓蛋糕,实际上3种蛋糕的制作方法是极其相似的,只有添加的粉剂不
同,如果用代码来实现蛋糕制作流程,要写大量重复代码,容易产生BUG,我们可以使用工厂模式和模板方法模式来避免重复。
首先定义一个蛋糕类Cake:
@Data
public class Cake {
// 蛋糕名称
String cakeName;
Integer sugar;
Integer eggs;
Integer flour;
// 添加剂(可可粉,抹茶粉,草莓)
String supplement;
}
再定义一个制作蛋糕的抽象父类:
@Service
public abstract class AbstractCakeService {
//处理做蛋糕的重复逻辑在父类实现
public Cake doCake(){
Cake cake = new Cake();
cake.setEggs(2);
cake.setFlour(250);
cake.setSugar(30);
//让子类实现不同的蛋糕处理
addOtherMaterial(cake);
return cake;
}
// 不同属性的赋值留给子类实现
protected abstract void addOtherMaterial(Cake cake);
}
我们定义3个不同的子类:抹茶蛋糕,可可蛋糕,草莓蛋糕制作,他们都继承抽象的父类AbstractCakeService,分别为
TeaCakeService,CocoaCakeService,StrawberryCakeService。
抹茶蛋糕TeaCakeService的实现:
@Service(value = "TeaCakeService")
public class TeaCakeService extends AbstractCakeService{
@Override
protected void addOtherMaterial(Cake cake) {
cake.setCakeName("抹茶蛋糕");
cake.setSupplement("抹茶粉");
System.out.println("当前正在制作好吃的抹茶蛋糕");
}
}
可可蛋糕CocoaCakeService的实现:
@Service(value = "CocoaCakeService")
public class CocoaCakeService extends AbstractCakeService{
@Override
protected void addOtherMaterial(Cake cake) {
cake.setCakeName("可可蛋糕");
cake.setSupplement("可可粉");
System.out.println("当前正在制作好吃的可可蛋糕");
}
}
草莓蛋糕CocoaCakeService的实现:
@Service(value = "StrawberryCakeService")
public class StrawberryCakeService extends AbstractCakeService{
@Override
protected void addOtherMaterial(Cake cake) {
cake.setCakeName("草莓蛋糕");
cake.setSupplement("新鲜草莓");
System.out.println("当前正在制作好吃的草莓蛋糕");
}
}
3种蛋糕制作都叫 XXXCakeService,那我们就可以把蛋糕类型字符串拼接 CakeService构成 Bean 的名称,然后利用 Spring 的
IoC 容器,通过 Bean 的名称直接获取到 AbstractCakeService,调用其 doCake方法来实现不同蛋糕的制作调用,这就是借助
Spring 容器实现了工厂模式。
调用的控制层代码如下:
@RestController
public class cakeController {
@Autowired
private ApplicationContext applicationContext;
@GetMapping("doCake")
public Cake doCake(@RequestParam("supplement") String supplement) {
AbstractCakeService cake = (AbstractCakeService) applicationContext.getBean(supplement + "CakeService");
return cake.doCake();
}
}
传入需要的添加剂,实现蛋糕的制作:
这样一来,我们利用工厂模式 + 模板方法模式,不仅消除了重复代码,还避免了修改既有代码的风险。这就是设计模式中的开闭
原则:对修改关闭,对扩展开放。
git代码实现路径:
https://github.com/jichunyang19931023/dailyDemo/tree/master/cake
二.利用注解 + 反射消除重复代码
假如我们要和外部系统进行接口服务对接,需要一个接口来输出接口的定义和传参等信息,此时我们可以利用注解+反射的机制来解决这个问题。
如果有一个创建票据的接口需要生成API的信息,要实现接口逻辑和逻辑实现的剥离,首先需要以 POJO 类(只有属性没有
任何业务逻辑的数据类)的方式定义所有的接口参数,如下所示:
@Data
public class CreateTicketAPI {
// 用户名
String userName;
// 密码
String password;
// 当前时间的时间戳
Long currentTime;
}
我们可以通过自定义注解为接口和所有参数增加一些元数据。如下所示,我们定义一个接口的注解 API,包含接口接口说明, URL 地址和请求方式:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Inherited
public @interface API {
String desc() default "";
String url() default "";
String type() default "GET";
}
然后,我们再定义一个自定义注解 @APIField,用于描述接口的每一个字段规范,包含参数的可否为空、类型和说明三个属性:
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@Documented
@Inherited
public @interface APIField {
// 可否为空
boolean isRequired() default false;
// 参数类型
String type() default "";
// 参数说明,备注
String remark() default "";
}
修改原始的POJO 类,补充接口和每个参数字段的元数据(也就是对应的属性相关信息),继承的 AbstractAPI 类是一个空实
现,这个案例中的接口并没有公共数据可以抽象放到基类:
@API(url = "/creatTicket", desc = "创建票据接口", type = "GET")
@Data
public class CreateTicketAPI extends AbstractAPI{
@APIField(isRequired = true, type = "String", remark = "用户名称")
String userName;
@APIField(isRequired = true, type = "String", remark = "密码")
String password;
@APIField(isRequired = false, type = "Long", remark = "当前时间的时间戳")
Long currentTime;
}
以上,我们通过注解实现了对 API 参数的描述。接下来,我们再看看反射如何配合注解实现接口属性和参数说明:
@Slf4j
@Service
public class APIService {
public JSONObject apiCreate(AbstractAPI api){
JSONObject result = new JSONObject();
// 从API注解获取请求说明
API apiObj = api.getClass().getAnnotation(API.class);
result.put("apiDesc", apiObj.desc());
result.put("apiUrl", apiObj.url());
result.put("apiType", apiObj.type());
JSONArray apiParams = JSONUtil.createArray();
Arrays.stream(api.getClass().getDeclaredFields()) // 获得所有字段
.filter(field -> field.isAnnotationPresent(APIField.class)) // 查找标记了注解的字段
.peek(field -> field.setAccessible(true)) // 设置可以访问私有字段
.forEach(field -> {
JSONObject paramObj = new JSONObject();
// 获得注解
APIField apiField = field.getAnnotation(APIField.class);
Object value = "";
try {
// 反射获取字段值
value = field.get(api);
} catch (IllegalAccessException e) {
log.error("反射获取字段值发生异常", e);
}
paramObj.put("paramName", field.getName());
paramObj.put("paramType", apiField.type());
paramObj.put("isRequired", apiField.isRequired());
paramObj.put("remark", apiField.remark());
paramObj.put("paramValue", value);
apiParams.add(paramObj);
});
result.put("apiParams", apiParams);
return result;
}
}
最后控制层进行调用:
@RestController
public class APIController {
@Autowired
private APIService apiService;
@GetMapping(value = "/getApi")
public JSONObject getApi(String userName, String password, Long currentTime) {
CreateTicketAPI ticketAPI = new CreateTicketAPI();
ticketAPI.setUserName(userName);
ticketAPI.setPassword(password);
ticketAPI.setCurrentTime(currentTime);
return apiService.apiCreate(ticketAPI);
}
}
许多涉及类结构性的通用处理,都可以按照这个模式来减少重复代码。反射给予了我们在不知晓类结构的时候,按照固定的逻辑
处理类的成员;而注解给了我们为这些成员补充元数据的能力,使得我们利用反射实现通用逻辑的时候,可以从外部获得更多我
们关心的数据。
git代码实现路径:
https://github.com/jichunyang19931023/dailyDemo/tree/master/api
三.利用属性拷贝工具消除重复代码
对于三层架构的系统,考虑到层之间的解耦隔离以及每一层对数据的不同需求,通常每一层都会有自己的 POJO 作为数据实体。
比如,数据访问层的实体一般叫作 DataObject 或 DO,业务逻辑层的实体一般叫作 Domain,表现层的实体一般叫作 Data
Transfer Object 或 DTO。如果手动写这些实体之间的赋值代码,同样容易出错,所以我们可以利用属性拷贝工具来消除重复代
码。
使用类似 BeanUtils 这种 Mapping 工具来做 Bean 的转换,copyProperties 方法还允许我们提供需要忽略的属性:
ComplicatedOrderDTO orderDTO = new ComplicatedOrderDTO();
ComplicatedOrderDO orderDO = new ComplicatedOrderDO();
BeanUtils.copyProperties(orderDTO, orderDO, "id");
return orderDO;
或者可以使用一个很棒的工具包hutool来实现,下面是官方文档网址,支持不同类型的拷贝,十分方便: