一、背景
最近我負責的活動促銷系統中要在審批的時候增加計算參加活動的商品的毛利率的需求。但是我負責打輔助,主要是同事負責具體開發,我了解了他的實現方式思路以后,果斷拒絕了,並給出了我的解決方案以及優點,他發現我的方案確實擴展性和可維護性更好以后就采用了,本文就來通過這個實例來說明如何讓本腐朽的代碼變得優雅起來。
二、需求描述
活動系統中共有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特性來實現優雅代碼的機會,這個時候我們一定不能只為了省事寫出爛代碼,這樣不但對自己的成長沒有任何的好處,而且會對以后維護者造成很大困擾,我們要在保證工期和質量的前提下盡量的把代碼寫的優雅一點,盡量考慮到可擴展性以及可維護性等。這樣才能在技術上有所提高,才能夠自我成長。兩全其美,何樂而不為呢?