一、背景
最近我负责的活动促销系统中要在审批的时候增加计算参加活动的商品的毛利率的需求。但是我负责打辅助,主要是同事负责具体开发,我了解了他的实现方式思路以后,果断拒绝了,并给出了我的解决方案以及优点,他发现我的方案确实扩展性和可维护性更好以后就采用了,本文就来通过这个实例来说明如何让本腐朽的代码变得优雅起来。
二、需求描述
活动系统*有7中活动类型,分别为:价格折扣活动、满减活动、满赠活动、换购活动、满折活动、抢购活动、N元任选活动。每种活动类型都有自己的毛利率的计算方式。要求根据不同的活动类型来通过不同的计算方式计算参加活动的商品的毛利率。
三、开发运行环境
- Maven 3.3.9
- Spring 4.2.6.RELEASE
- JDK 1.7
- IDEA 15.04
四、同事方案1
直接通过switch/case的方式判断不同的活动类型,然后每种类型给出不同的计算方式。
package com.hafiz.www.domain;
/**
* @author hafiz.zhang
* @description: 活动毛利率计算器
* @date Created in 2017/11/28 20:52.
*/
public class Calculator {
public static String calculate(Integer campaignType) {
switch (campaignType) {
case 1:
return "价格折扣活动计算毛利率";
case 2:
return "满减活动计算毛利率";
case 3:
return "满赠活动计算毛利率";
case 4:
return "换购活动计算毛利率";
case 5:
return "满折活动计算毛利率";
case 6:
return "抢购活动计算毛利率";
case 7:
return "N元任选活动计算毛利率";
default:
return "错误的活动类型";
}
}
}
缺点:虽然写起来很简单,但是可扩展性差,或者说不具备可扩展性,若每种活动类型的计算毛利率方式都比较复杂,则Calculator类就会变得臃肿不堪。可维护性很差。完全就是面向过程的开发方式。被我一票拒绝。并告诉他通过定义接口,然后各种活动类型实现自己的计算方式,然后使用简单工厂模式通过Java的多态来实现。
五、同事方案2
定义计算接口,被针对每种活动给出不同的实现。
1.定义计算接口
package com.hafiz.www.handler;
import com.hafiz.www.enums.CampaignTypeEnum;
/**
* @author hafiz.zhang
* @description: 计算毛利率接口
* @date Created in 2017/11/28 20:57.
*/
public interface ICampaignHandler {
/**
* 计算毛利率
* @return
*/
String calculate();
}
2.价格折扣活动实现
package com.hafiz.www.handler.impl;
import com.hafiz.www.enums.CampaignTypeEnum;
import com.hafiz.www.event.CampaignHandlerEvent;
import com.hafiz.www.handler.ICampaignHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @Description: 价格折扣活动操作器
* @author hafiz.zhang
* @create 2017/11/28 20:52.
*/
public class PriceDiscountCampaignHandler implements ICampaignHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(PriceDiscountCampaignHandler.class);
@Override
public String calculate() {
LOGGER.info("价格折扣活动计算毛利率");
return "价格折扣活动计算毛利率";
}
}
3.抢购类型活动实现
package com.hafiz.www.handler.impl;
import com.hafiz.www.enums.CampaignTypeEnum;
import com.hafiz.www.event.CampaignHandlerEvent;
import com.hafiz.www.handler.ICampaignHandler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* @Description: 抢购活动操作器
* @author: hafiz.zhang
* @create: 2017/11/28 20:52.
*/
public class PanicBuyCampaignHandler implements ICampaignHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(PanicBuyCampaignHandler.class);
@Override
public String calculate() {
LOGGER.info("抢购活动计算毛利率");
return "抢购活动计算毛利率";
}
}
等等还有剩下各种活动自己的实现,此处为避免篇幅过长略去。
4.简单工厂
package com.hafiz.www.handler;
import com.hafiz.www.handler.impl.CheapenOtherCampaignHandler;
import com.hafiz.www.handler.impl.FullCutCampaignHandler;
import com.hafiz.www.handler.impl.FullDiscountCampaignHandler;
import com.hafiz.www.handler.impl.FullGIftCampaignHandler;
import com.hafiz.www.handler.impl.OptionCampaignHandler;
import com.hafiz.www.handler.impl.PanicBuyCampaignHandler;
import com.hafiz.www.handler.impl.PriceDiscountCampaignHandler;
/**
* @author hafiz.zhang
* @description: 操作器工厂类
* @date Created in 2017/11/28 22:06.
*/
public class CampaignHandlerFactory {
public static ICampaignHandler getHandler(Integer campaignType) {
switch (campaignType) {
case 1:
return new PriceDiscountCampaignHandler();
case 2:
return new FullCutCampaignHandler();
case 3:
return new FullGIftCampaignHandler();
case 4:
return new CheapenOtherCampaignHandler();
case 5:
return new FullDiscountCampaignHandler();
case 6:
return new PanicBuyCampaignHandler();
case 7:
return new OptionCampaignHandler();
default:
throw new RuntimeException("错误的活动类型");
}
}
}
这样比第一版稍好一点,代码已经优雅了很多,可扩展性也好了很多,如果一旦增加新的活动类型,只需要新写一个新活动计算毛利率的操作器实现类就好了,然后再在工厂类中增加对应的case.但是还是没有很完美,这样需要每次都修改工厂类,不完美!
六、我的方案:使用Spring事件通知来实现简单工厂
1.接口定义
package com.hafiz.www.handler;
import com.hafiz.www.enums.CampaignTypeEnum;
/**
* @author hafiz.zhang
* @description: 计算毛利率接口
* @date Created in 2017/11/28 20:57.
*/
public interface ICampaignHandler {
CampaignTypeEnum getCampaignType();
/**
* 计算毛利率
* @return
*/
String calculate();
}
2.活动操作器自定义事件
package com.hafiz.www.event;
import com.hafiz.www.handler.ICampaignHandler;
import org.springframework.context.ApplicationEvent;
/**
* @author hafiz.zhang
* @description: 活动操作器事件
* @date Created in 2017/11/28 21:02.
*/
public class CampaignHandlerEvent extends ApplicationEvent {
public CampaignHandlerEvent(ICampaignHandler source) {
super(source);
}
}
2.价格折扣现类
package com.hafiz.www.handler.impl;
import com.hafiz.www.enums.CampaignTypeEnum;
import com.hafiz.www.event.CampaignHandlerEvent;
import com.hafiz.www.handler.ICampaignHandler;
import com.hafiz.www.spring.SpringAware;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
* @Description: 价格折扣活动操作器
* @author hafiz.zhang
* @create 2017/11/28 20:52.
*/
@Component
public class PriceDiscountCampaignHandler implements ICampaignHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(PriceDiscountCampaignHandler.class);
@PostConstruct
public void init() {
CampaignHandlerEvent event = new CampaignHandlerEvent(this);
SpringAware.getApplicationContext().publishEvent(event);
}
@Override
public CampaignTypeEnum getCampaignType() {
return CampaignTypeEnum.PRICE_DISCOUNT;
}
@Override
public String calculate() {
LOGGER.info("价格折扣活动计算毛利率");
return "价格折扣活动计算毛利率";
}
}
3.抢购类活动实现类
package com.hafiz.www.handler.impl;
import com.hafiz.www.enums.CampaignTypeEnum;
import com.hafiz.www.event.CampaignHandlerEvent;
import com.hafiz.www.handler.ICampaignHandler;
import com.hafiz.www.spring.SpringAware;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
import javax.annotation.PostConstruct;
/**
* @Description: 抢购活动操作器
* @author: hafiz.zhang
* @create: 2017/11/28 20:52.
*/
@Component
public class PanicBuyCampaignHandler implements ICampaignHandler {
private static final Logger LOGGER = LoggerFactory.getLogger(PanicBuyCampaignHandler.class);
@PostConstruct
public void init() {
CampaignHandlerEvent event = new CampaignHandlerEvent(this);
SpringAware.getApplicationContext().publishEvent(event);
}
@Override
public CampaignTypeEnum getCampaignType() {
return CampaignTypeEnum.PANIC_BUY;
}
@Override
public String calculate() {
LOGGER.info("抢购活动计算毛利率");
return "抢购活动计算毛利率";
}
}
还有另外几种活动类型的实现方式,为了避免篇幅过长不一一列举。
4.新工厂实现方式
package com.hafiz.www.handler;
import com.hafiz.www.enums.CampaignTypeEnum;
import com.hafiz.www.event.CampaignHandlerEvent;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author hafiz.zhang
* @description: 活动操作器工厂类
* @date Created in 2017/11/28 20:59.
*/
@Component
public class CampaignHandlerFactory implements ApplicationListener<CampaignHandlerEvent> {
private static final Logger LOGGER = LoggerFactory.getLogger(CampaignHandlerFactory.class);
private static Map<CampaignTypeEnum, ICampaignHandler> handlerMap = new ConcurrentHashMap<>();
/**
* 通过活动类型获取对应的操作器
*
* @param discountType 活动类型
*
* @return
*/
public static ICampaignHandler getHandler(Integer discountType) {
CampaignTypeEnum discountTypeEnum = CampaignTypeEnum.getEnumById(discountType);
ICampaignHandler handler = handlerMap.get(discountTypeEnum);
return handler;
}
/**
* 注册绑定不同类型活动对应的活动操作器
*
* @param handler 活动操作器
*
*/
private void registerHandler(ICampaignHandler handler) {
CampaignTypeEnum discountType = handler.getCampaignType();
LOGGER.info("开始绑定{}类型的活动处理器", discountType.getName());
handlerMap.put(discountType, handler);
}
@Override
public void onApplicationEvent(CampaignHandlerEvent event) {
ICampaignHandler handler = (ICampaignHandler) event.getSource();
this.registerHandler(handler);
}
}
说明 :新的工厂类中通过实现Spring的事件监听,接收到监听以后,直接获取事件源,保存在本地Map中,就很优雅。这样新增活动类型的时候工厂类完全不需要修改,而且现有类也不需要修改,只需要进行对新的活动类型扩展就好了。符合了软件开发中的开闭环原则。看起来很棒~
5.工具类SpringAware
package com.hafiz.www.spring;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.transaction.interceptor.TransactionAspectSupport;
/**
* @author hafiz.zhang
* @description: spring 上下文工具类
* @date Created in 2017/11/28 21:07.
*/
public class SpringAware implements ApplicationContextAware {
private static ApplicationContext applicationContext = null;
public SpringAware() {
}
@Override
public void setApplicationContext(ApplicationContext ac) throws BeansException {
applicationContext = ac;
}
public static ApplicationContext getApplicationContext() {
return applicationContext;
}
public static <T> T getBean(String name) {
return (T) applicationContext.getBean(name);
}
public static void rollBack() {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
其他Spring的配置以及测试用例等代码不再单独贴出,源码地址:https://github.com/hafizzhang/code-optimize.git
七、总结
在实际工作中,我们会碰到很多这种可以通过设计模式以及Java特性来实现优雅代码的机会,这个时候我们一定不能只为了省事写出烂代码,这样不但对自己的成长没有任何的好处,而且会对以后维护者造成很大困扰,我们要在保证工期和质量的前提下尽量的把代码写的优雅一点,尽量考虑到可扩展性以及可维护性等。这样才能在技术上有所提高,才能够自我成长。两全其美,何乐而不为呢?