利用設計模式消除業務程式碼中的 if-else
準備工作:
假設這樣的一個業務場景:有一個自動開票的功能需要實現,在程式裡面需要根據賬單的型別執行對應的處理邏輯。
以下使用了 Lombok 簡化程式碼!!!
賬單型別列舉:
/**
* @author ly-az
* @date 12/23/2020 11:34
* 賬單型別
*/
public enum BillType {
/**
* 賬單的型別
*/
BT0001, BT0002
}
賬單類:
/** * @author ly-az * @date 12/23/2020 11:31 * 賬單實體 */ @Data @NoArgsConstructor @AllArgsConstructor public class Bill { /** * 賬單資訊 */ String message; /** * 賬單型別型別(BT0001、BT0002) */ BillType type; }
賬單的模擬生成:用於生成一下模擬資料
/** * @author ly-az * @date 12/23/2020 11:42 * 生成用於測試的賬單 */ public class BillBuilder { /** * 生成賬單 * * @return 賬單集合 */ public static List<Bill> generateBillList() { val billList = new ArrayList<Bill>(); billList.add(new Bill("我是第一種型別的賬單", BillType.BT0001)); billList.add(new Bill("我是第二種型別的賬單", BillType.BT0002)); return billList; } }
使用if-else來實現的話:
/** * @author ly-az * @date 12/24/2020 10:31 * if-else */ public class Test01 { public static void main(String[] args) { List<Bill> billList = BillBuilder.generateBillList(); billList.forEach(bill -> { if (bill.getType().equals(BillType.BT0001)) { // 模擬實現業務處理 System.out.println("開始處理BT0001型別的賬單 > > > " + bill.getMessage() + ". . . "); System.out.println("處理成功!!!"); } else if (bill.getType().equals(BillType.BT0002)) { System.out.println("開始處理BT0002型別的賬單 > > > " + bill.getMessage() + ". . . "); System.out.println("處理成功!!!"); } // 。。。 未來還有數不清的else if分支 。。。 else { System.out.println("賬單型別不匹配!!!"); } }); } }
在遇到if-else的分支業務邏輯比較複雜時,為了程式碼的可讀性我們會將其整理成一個方法或者封裝成一個工具類去呼叫,避免整個if-else的結構就顯得過於臃腫(這裡就模擬了兩種型別,偷懶...ψ(`∇´)ψ)。
但是當賬單型別越來越多時,if-else分支就會越來越多,每增加一種型別,就需要去修改或新增if-else分支,違反了開閉原則(對擴充套件開放,對修改關閉)
方案一:使用策略模式+Map字典
策略模式屬於物件的行為模式。其用意是針對一組演算法,將每一個演算法封裝到具有共同介面的獨立的類中,從而使得它們可以相互替換。策略模式使得演算法可以在不影響到客戶端的情況下發生變化。
------《JAVA與模式》
也就是說讓我們自己的程式碼不再依賴於呼叫業務程式碼的時的客戶端,能夠根據不同的客戶端執行不同的程式碼,而客戶端只需要呼叫我們的策略介面即可。
在上述場景中,可以把 if-else 分支的程式碼抽取為各種不同的策略實現,而且我們可以把選擇不同策略的實現的程式碼邏輯抽取到一個工廠類中去,這就是策略模式+簡單工廠模式,為了進一步簡化,我們還可以把策略的實現放到一個Map中封裝成字典
看程式碼:
- 抽象策略:通常由一個介面或抽象類實現。是所有的具體策略類所需的介面。
/**
* @author ly-az
* @date 12/23/2020 11:46
* @see BillHandleStrategyFactory 策略工廠
* 賬單處理的策略介面<br>
* 有新的型別的賬單時只需要實現這個介面重寫接口裡的實現方法並新增到策略工廠裡面的字典裡即可
*/
public interface BillHandleStrategy {
/**
* 處理方法
*
* @param bill 賬單
*/
void handleBill(Bill bill);
}
- 環境:持有一個策略介面的引用。
/**
* @author ly-az
* @date 12/23/2020 11:46
*/
public class BillStrategyContext {
private BillHandleStrategy billHandleStrategy;
/**
* 設定策略介面
*
* @param billHandleStrategy 策略介面
*/
public void setBillHandleStrategy(BillHandleStrategy billHandleStrategy) {
this.billHandleStrategy = billHandleStrategy;
}
public void handleBill(Bill bill) {
if (billHandleStrategy != null) {
billHandleStrategy.handleBill(bill);
}
}
}
- 具體的策略實現:就是處理賬單的策略介面的具體實現,賬單對應的處理者。
/**
* @author ly-az
* @date 12/23/2020 13:01
* 處理BT0001賬單的策略介面的實現類
*/
public class Bt0001BillHandleStrategy implements BillHandleStrategy {
@Override
public void handleBill(Bill bill) {
// 模擬業務處理
System.out.println("開始處理BT0001型別的賬單 > > > " + bill.getMessage() + ". . .");
System.out.println("處理成功!!!");
}
}
/**
* @author ly-az
* @date 12/23/2020 13:02
* 處理BT0002型別的策略實現類
*/
public class Bt0002BillHandleStrategy implements BillHandleStrategy {
@Override
public void handleBill(Bill bill) {
// 模擬實現業務處理
System.out.println("開始處理BT0002型別的賬單 > > > " + bill.getMessage() + ". . . ");
System.out.println("處理成功!!!");
}
}
- 策略工廠:用於獲取具體策略的實現類物件
/**
* @author ly-az
* @date 12/23/2020 13:05
* 策略工廠
*/
public class BillHandleStrategyFactory {
private static final Map<BillType, BillHandleStrategy> BILL_HANDLE_STRATEGY_MAP;
static {
// 初始化,
BILL_HANDLE_STRATEGY_MAP = new HashMap<>();
BILL_HANDLE_STRATEGY_MAP.put(BillType.BT0001, new Bt0001BillHandleStrategy());
BILL_HANDLE_STRATEGY_MAP.put(BillType.BT0002, new Bt0002BillHandleStrategy());
}
/**
* 根據賬單型別直接從Map字典裡獲取對應的處理策略實現類
*
* @param billType 賬單型別
* @return 處理策略的實現類
*/
public static BillHandleStrategy getBillHandleStrategy(BillType billType) {
return BILL_HANDLE_STRATEGY_MAP.get(billType);
}
}
測試程式碼:
/**
* @author ly-az
* @date 12/23/2020 13:12
*/
public class Test02 {
public static void main(String[] args) {
//模擬賬單
List<Bill> billList = BillBuilder.generateBillList();
//策略上下文
BillStrategyContext billStrategyContext = new BillStrategyContext();
billList.forEach(bill -> {
//獲取並設定策略
BillHandleStrategy billHandleStrategy = BillHandleStrategyFactory.getBillHandleStrategy(bill.getType());
billStrategyContext.setBillHandleStrategy(billHandleStrategy);
//執行策略
billStrategyContext.handleBill(bill);
});
}
}
測試結果:
**
每當有一種新的賬單型別,只需要新增新的賬單處理策略,並新增到BillHandleStrategyFactory中的Map集合。
如果要使得程式符合開閉原則,則需要調整BillHandleStrategyFactory中處理策略的獲取方式。
(好吧,我攤牌了,其實是我太菜了沒有想到怎麼寫ヾ(•ω•`)o)
改進思路:策略模式+註解,通過自定義註解,使用註解標記具體的策略實現類,我們就可以通過反射獲取到具體的策略實現類的例項,然後放置到容器的Map字典中!
方案二:使用責任鏈模式
顧名思義,責任鏈模式(Chain of Responsibility Pattern)為呼叫請求建立了一個接收者物件的鏈。讓多個物件都有可能接收請求,並將這些物件連線成一條鏈,並且沿著這條鏈傳遞呼叫請求,直到有物件處理它為止。這種型別的設計模式也屬於行為型模式。
發出這個呼叫請求的客戶端並不知道鏈上的哪一個物件會最終處理這個呼叫請求,這使得我們的程式可以在不影響客戶端的情況下動態地重新組織和分配責任。
- 賬單處理的抽像介面
/**
* @author ly-az
* @date 12/23/2020 14:51
* 賬單的抽象處理介面
*/
public interface IBillHandler {
/**
* 利用責任鏈處理賬單的抽象方法
* @param bill 賬單
* @param handleChain 處理鏈
*/
void handleBill(Bill bill, IBillHandleChain handleChain);
}
- 賬單處理的的具體實現
/**
* @author ly-az
* @date 12/23/2020 15:14
* 賬單的具體處理者的實現
*/
public class Bt0001BillHandler implements IBillHandler {
@Override
public void handleBill(Bill bill, IBillHandleChain handleChain) {
if (BillType.BT0001.equals(bill.getType())) {
// 模擬實現業務處理
System.out.println("開始處理BT0001型別的賬單 > > > " + bill.getMessage() + ". . . ");
System.out.println("處理成功!!!");
}
//處理不了該回執就往下傳遞
else {
handleChain.handleBill(bill);
}
}
}
/**
* @author ly-az
* @date 12/23/2020 15:42
* todo
*/
public class Bt0002BillHandler implements IBillHandler {
@Override
public void handleBill(Bill bill, IBillHandleChain handleChain) {
if (BillType.BT0002.equals(bill.getType())) {
// 模擬實現業務處理
System.out.println("開始處理BT0002型別的賬單 > > > " + bill.getMessage() + ". . . ");
System.out.println("處理成功!!!");
}
//處理不了該回執就往下傳遞
else {
handleChain.handleBill(bill);
}
}
}
- 責任連結口
/**
* @author ly-az
* @date 12/23/2020 14:52
* 責任連結口
*/
public interface IBillHandleChain {
/**
* 處理賬單的抽象方法
*
* @param bill 賬單
*/
void handleBill(Bill bill);
}
- 責任連結口的實現
/**
* @author ly-az
* @date 12/23/2020 15:08
* 處理賬單的責任鏈
*/
public class BillHandleChain implements IBillHandleChain {
/**
* 記錄當前處理者位置
*/
private int index = 0;
/**
* 處理者集合
*/
private static final List<IBillHandler> BILL_HANDLER_LIST;
static {
//從容器中獲取處理器物件
BILL_HANDLER_LIST = BillHandlerContext.getBillHandlerList();
}
@Override
public void handleBill(Bill bill) {
if (BILL_HANDLER_LIST != null && BILL_HANDLER_LIST.size() > 0) {
if (index != BILL_HANDLER_LIST.size()) {
IBillHandler billHandler = BILL_HANDLER_LIST.get(index++);
billHandler.handleBill(bill, this);
}
}
}
}
- 責任鏈處理者容器(如果專案中有Spring的IOC容器 ,則可以直接通過依賴注入的方式獲取到
IBillHandler
的具體實現)
/**
* @author ly-az
* @date 12/23/2020 15:43
* 處理容器
*/
public class BillHandlerContext {
private BillHandlerContext() {
}
public static List<IBillHandler> getBillHandlerList() {
val billHandlerList = new ArrayList<IBillHandler>();
billHandlerList.add(new Bt0001BillHandler());
billHandlerList.add(new Bt0002BillHandler());
return billHandlerList;
}
}
- 測試用例
/**
* @author ly-az
* @date 12/23/2020 15:48
* 責任鏈模式下的測試
*/
public class TestClient2 {
public static void main(String[] args) {
List<Bill> billList = BillBuilder.generateBillList();
billList.forEach(bill -> {
//回執處理鏈的例項物件
BillHandleChain billHandleChain = new BillHandleChain();
billHandleChain.handleBill(bill);
});
}
}
結果:
同樣,如果要使得程式符合開閉原則,則需要調整BillHandlerContext中處理者的獲取方式,通過反射的方式,獲取指定包下的所有IBillHandler的實現類。
**
o( ̄▽ ̄)ブ,我想到了這種解決方案下該如何改造使其符合開閉原則!!!
我們需要引入一個反射用的工具類:
/**
* @author ly-az
* @date 12/23/2020 16:00
* 反射工具類
*/
public class ReflectionUtil {
/**
* 定義類集合(用於存放所有載入的類的映象)
*/
private static final Set<Class<?>> CLASS_SET;
static {
//指定載入包路徑
CLASS_SET = getClassSet("com.az");
}
/**
* 獲取類載入器
*
* @return 類載入器
*/
public static ClassLoader getClassLoader() {
return Thread.currentThread().getContextClassLoader();
}
/**
* 載入類
*
* @param className 類全限定名稱
* @param isInitialized 是否在載入完成後執行靜態程式碼塊
* @return 類的映象
*/
public static Class<?> loadClass(String className, boolean isInitialized) {
Class<?> cls;
try {
cls = Class.forName(className, isInitialized, getClassLoader());
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
return cls;
}
public static Class<?> loadClass(String className) {
return loadClass(className, true);
}
/**
* 獲取指定包下所有類
*
* @param packageName 全限包名
* @return 映象的Set集合
*/
public static Set<Class<?>> getClassSet(String packageName) {
Set<Class<?>> classSet = new HashSet<>();
try {
Enumeration<URL> urls = getClassLoader().getResources(packageName.replace(".", "/"));
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
if (url != null) {
String protocol = url.getProtocol();
if ("file".equals(protocol)) {
String packagePath = url.getPath().replace("%20", "");
addClass(classSet, packagePath, packageName);
} else if ("jar".equals(protocol)) {
JarURLConnection jarUrlConnection = (JarURLConnection) url.openConnection();
if (jarUrlConnection != null) {
JarFile jarFile = jarUrlConnection.getJarFile();
if (jarFile != null) {
Enumeration<JarEntry> jarEntries = jarFile.entries();
while (jarEntries.hasMoreElements()) {
JarEntry jarEntry = jarEntries.nextElement();
String jarEntryName = jarEntry.getName();
if (jarEntryName.endsWith(".class")) {
String className = jarEntryName.substring(0, jarEntryName.lastIndexOf(".")).replaceAll("/", ".");
doAddClass(classSet, className);
}
}
}
}
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return classSet;
}
private static void doAddClass(Set<Class<?>> classSet, String className) {
Class<?> cls = loadClass(className, false);
classSet.add(cls);
}
private static void addClass(Set<Class<?>> classSet, String packagePath, String packageName) {
final File[] fileList = new File(packagePath).listFiles(file -> (file.isFile() && file.getName().endsWith(".class")) || file.isDirectory());
assert fileList != null;
Arrays.asList(fileList).forEach(file -> {
String fileName = file.getName();
if (file.isFile()) {
String className = fileName.substring(0, fileName.lastIndexOf("."));
if (StringUtils.isNotEmpty(packageName)) {
className = packageName + "." + className;
}
doAddClass(classSet, className);
} else {
String subPackagePath = fileName;
if (StringUtils.isNotEmpty(packagePath)) {
subPackagePath = packagePath + "/" + subPackagePath;
}
String subPackageName = fileName;
if (StringUtils.isNotEmpty(packageName)) {
subPackageName = packageName + "." + subPackageName;
}
addClass(classSet, subPackagePath, subPackageName);
}
});
}
public static Set<Class<?>> getClassSet() {
return CLASS_SET;
}
/**
* 獲取應用包名下某父類(或介面)的所有子類(或實現類)
*
* @param superClass 父類(或介面)映象
* @return 所有子類(或實現類)的映象集合
*/
public static Set<Class<?>> getClassSetBySuper(Class<?> superClass) {
Set<Class<?>> classSet = new HashSet<>();
for (Class<?> cls : CLASS_SET) {
if (superClass.isAssignableFrom(cls) && !superClass.equals(cls)) {
classSet.add(cls);
}
}
return classSet;
}
/**
* 獲取應用包名下帶有某註解的類
*
* @param annotationClass 註解的映象
* @return 映象集合
*/
public static Set<Class<?>> getClassSetByAnnotation(Class<? extends Annotation> annotationClass) {
Set<Class<?>> classSet = new HashSet<>();
for (Class<?> cls : CLASS_SET) {
if (cls.isAnnotationPresent(annotationClass)) {
classSet.add(cls);
}
}
return classSet;
}
}
對責任鏈模式的處理容器修改一下:
/**
* @author ly-az
* @date 12/23/2020 15:43
* 處理容器
*/
public class BillHandlerContext {
private BillHandlerContext() {
}
public static List<IBillHandler> getBillHandlerList() {
val billHandlerList = new ArrayList<IBillHandler>();
//獲取IBillHandler介面的實現類
Set<Class<?>> classList = ReflectionUtil.getClassSetBySuper(IBillHandler.class);
if (classList.size() > 0) {
classList.forEach(clazz -> {
try {
billHandlerList.add((IBillHandler) clazz.getDeclaredConstructor().newInstance());
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | NoSuchMethodException e) {
e.printStackTrace();
}
});
}
return billHandlerList;
}
}
至此,責任鏈方案就符合開閉原則,如果新增一個賬單型別,只需要新增一個新的賬單處理處理器的實現類即可,無需做其它改動。
小結
if-else以及switch-case 這種分支判斷的方式對於分支邏輯不多的簡單業務,還是更加直觀高效的。但是對於業務複雜,分支邏輯多的情況下,採用一下適當的設計模式,會讓程式碼更加清晰,容易維護,但同時類或方法數量也是倍增的。我們需要對業務做好充分分析,避免一上來就設計模式,避免過度設計!