子類不依賴泛型,重寫父類方法,通過強制類型轉(zhuǎn)換父類方法參數(shù)出現(xiàn)的問題。——— 一個例子引發(fā)的思考
-
使用泛型(推薦)
-
public interface FlowHandlerGateway<P extends FlowApprovalPageCondition> { Page<FlowApprovalPage> pageQuery(P condition); } //父類 @Slf4j @Component @RequiredArgsConstructor public class FlowHandlerGatewayImpl<P extends FlowApprovalPageCondition> implements FlowHandlerGateway<P>{ private final FlowApprovalWrapper flowApprovalWrapper; private final InfraConverter converter; private final CommonAdapter commonAdapter; @Override public Page<FlowApprovalPage> pageQuery(P condition) { Page<FlowApprovalPO> pageInfo = commonAdapter.toPage(condition); LambdaQueryWrapper<FlowApprovalPO> wrapper = Wrappers.<FlowApprovalPO>lambdaQuery() .eq(StrUtil.isNotBlank(condition.getBizNo()), FlowApprovalPO::getBizNo, condition.getBizNo()) .eq(StrUtil.isNotBlank(condition.getFlowType()), FlowApprovalPO::getFlowType, condition.getFlowType()) .eq(ObjectUtil.isNotNull(condition.getInstanceId()), FlowApprovalPO::getInstanceId, condition.getInstanceId()) .eq(StrUtil.isNotBlank(condition.getFlowStatus()), FlowApprovalPO::getFlowStatus, condition.getFlowStatus()) .eq(ObjectUtil.isNotNull(condition.getApplyBy()), FlowApprovalPO::getApplyBy, condition.getApplyBy()) .like(StrUtil.isNotBlank(condition.getApplyByName()), FlowApprovalPO::getApplyByName, condition.getApplyByName()) .ge(ObjectUtil.isNotNull(condition.getApplyStartTime()), FlowApprovalPO::getCreatedAt, condition.getApplyStartTime()) .le(ObjectUtil.isNotNull(condition.getApplyEndTime()), FlowApprovalPO::getCreatedAt, condition.getApplyEndTime()); // Wrapper拓展方法 pageQueryWrapperExpand(wrapper, condition); return converter.toFlowApprovalPage(flowApprovalWrapper.page(pageInfo, wrapper)); } protected void pageQueryWrapperExpand(LambdaQueryWrapper<FlowApprovalPO> wrapper, P conditionPage){} } /** * 子類 */ @Component @Slf4j public class SimulationLoginGatewayImpl extends FlowHandlerGatewayImpl<SimulationLoginPageCondition>{ public SimulationLoginGatewayImpl(FlowApprovalWrapper flowApprovalWrapper, InfraConverter converter, CommonAdapter commonAdapter) { super(flowApprovalWrapper, converter, commonAdapter); } @Override protected void pageQueryWrapperExpand(LambdaQueryWrapper<FlowApprovalPO> wrapper, SimulationLoginPageCondition condition){ wrapper.apply(ObjectUtil.isNotNull(condition.getTenantId()),"biz_data->>'$.tenant_id' = {0}", condition.getTenantId()) .apply(ObjectUtil.isNotNull(condition.getApplicationId()),"biz_data->>'$.application_id' = {0}", condition.getApplicationId()) .apply(ObjectUtil.isNotNull(condition.getEnableStatus()),"biz_data->>'$.enable_status' = {0}", condition.getEnableStatus()) .apply(ObjectUtil.isNotNull(condition.getEmail()),"biz_data->>'$.email' = {0}", condition.getEmail()); } }
-
-
通過強制類型轉(zhuǎn)換。其中SimulationLoginPageCondition實體繼承FlowApprovalPageCondition實體,子類SimulationLoginPageCondition為什么不能使用多態(tài)特性,直接重寫父類pageQueryWrapperExpand方法
-
// 父類 @Slf4j @Component @RequiredArgsConstructor public class FlowHandlerGatewayImpl implements FlowHandlerGateway{ private final FlowApprovalWrapper flowApprovalWrapper; private final InfraConverter converter; private final CommonAdapter commonAdapter; @Override public <P extends FlowApprovalPageCondition> Page<FlowApprovalPage> pageQuery(P condition) { Page<FlowApprovalPO> pageInfo = commonAdapter.toPage(condition); LambdaQueryWrapper<FlowApprovalPO> wrapper = Wrappers.<FlowApprovalPO>lambdaQuery() .eq(StrUtil.isNotBlank(condition.getBizNo()), FlowApprovalPO::getBizNo, condition.getBizNo()) .eq(StrUtil.isNotBlank(condition.getFlowType()), FlowApprovalPO::getFlowType, condition.getFlowType()) .eq(ObjectUtil.isNotNull(condition.getInstanceId()), FlowApprovalPO::getInstanceId, condition.getInstanceId()) .eq(StrUtil.isNotBlank(condition.getFlowStatus()), FlowApprovalPO::getFlowStatus, condition.getFlowStatus()) .eq(ObjectUtil.isNotNull(condition.getApplyBy()), FlowApprovalPO::getApplyBy, condition.getApplyBy()) .like(StrUtil.isNotBlank(condition.getApplyByName()), FlowApprovalPO::getApplyByName, condition.getApplyByName()) .ge(ObjectUtil.isNotNull(condition.getApplyStartTime()), FlowApprovalPO::getCreatedAt, condition.getApplyStartTime()) .le(ObjectUtil.isNotNull(condition.getApplyEndTime()), FlowApprovalPO::getCreatedAt, condition.getApplyEndTime()); // Wrapper拓展方法 pageQueryWrapperExpand(wrapper, condition); return converter.toFlowApprovalPage(flowApprovalWrapper.page(pageInfo, wrapper)); } protected void pageQueryWrapperExpand(LambdaQueryWrapper<FlowApprovalPO> wrapper, FlowApprovalPageCondition conditionPage){} } /** * 子類 */ @Component @Slf4j public class SimulationLoginGatewayImpl extends FlowHandlerGatewayImpl{ public SimulationLoginGatewayImpl(FlowApprovalWrapper flowApprovalWrapper, InfraConverter converter, CommonAdapter commonAdapter) { super(flowApprovalWrapper, converter, commonAdapter); } //出現(xiàn)報錯,重寫方法參數(shù)和父類不一樣、違反了java規(guī)范 @Override protected void pageQueryWrapperExpand(LambdaQueryWrapper<FlowApprovalPO> wrapper, SimulationLoginPageCondition condition){ wrapper.apply(ObjectUtil.isNotNull(condition.getTenantId()),"biz_data->>'$.tenant_id' = {0}", condition.getTenantId()) .apply(ObjectUtil.isNotNull(condition.getApplicationId()),"biz_data->>'$.application_id' = {0}", condition.getApplicationId()) .apply(ObjectUtil.isNotNull(condition.getEnableStatus()),"biz_data->>'$.enable_status' = {0}", condition.getEnableStatus()) .apply(ObjectUtil.isNotNull(condition.getEmail()),"biz_data->>'$.email' = {0}", condition.getEmail()); } // 強制類型轉(zhuǎn)換,子類中重寫父類方法,也不推薦后面有原因,違反了開閉原則,下面有講解 @Override protected void pageQueryWrapperExpand(LambdaQueryWrapper<FlowApprovalPO> wrapper, FlowApprovalPageCondition condition) { // 強制類型轉(zhuǎn)換 SimulationLoginPageCondition simCondition = (SimulationLoginPageCondition) condition; // 使用轉(zhuǎn)換后的條件 wrapper.apply(ObjectUtil.isNotNull(simCondition.getTenantId()), "biz_data->>'$.tenant_id' = {0}", simCondition.getTenantId()) .apply(ObjectUtil.isNotNull(simCondition.getApplicationId()), "biz_data->>'$.application_id' = {0}", simCondition.getApplicationId()) .apply(ObjectUtil.isNotNull(simCondition.getEnableStatus()), "biz_data->>'$.enable_status' = {0}", simCondition.getEnableStatus()) .apply(ObjectUtil.isNotNull(simCondition.getEmail()), "biz_data->>'$.email' = {0}", simCondition.getEmail()); } }
-
-
原理
-
1. 單一職責(zé)原則 (Single Responsibility Principle - SRP)
核心思想:一個類應(yīng)該只有一個引起它變化的原因。換句話說,一個類應(yīng)該只負責(zé)一項職責(zé)。
在Java中的理解:
-
好處:類的職責(zé)越單一,它的內(nèi)聚性就越高,就越容易被理解、維護和修改。修改一個功能不會意外影響到其他不相關(guān)的功能。
-
** violation (違反)的例子**:如果一個類既負責(zé)數(shù)據(jù)庫操作,又負責(zé)業(yè)務(wù)邏輯計算,還負責(zé)發(fā)送郵件,那它就違反了SRP。此時,修改數(shù)據(jù)庫連接方式、業(yè)務(wù)算法或郵件服務(wù)器配置都會修改這個類,風(fēng)險很高。
結(jié)合你的代碼:
-
FlowHandlerGatewayImpl的職責(zé)非常明確:構(gòu)建查詢流程審批單的分頁條件。它不關(guān)心具體的SQL執(zhí)行(由FlowApprovalWrapper負責(zé)),也不關(guān)心PO到Domain的轉(zhuǎn)換(由InfraConverter負責(zé))。它通過依賴注入將其他職責(zé)委托給了專門的類,這很好地遵循了SRP。
2. 開閉原則 (Open/Closed Principle - OCP)
核心思想:軟件實體(類、模塊、函數(shù)等)應(yīng)該對擴展開放,但對修改關(guān)閉。
在Java中的理解:
-
“對修改關(guān)閉”:意味著一個已經(jīng)完成并測試通過的類的核心代碼不應(yīng)該再被修改。
-
“對擴展開放”:意味著當(dāng)有新的需求時,你應(yīng)該能夠通過擴展這個類(如通過繼承、組合、實現(xiàn)接口等方式)來添加新功能,而不是修改它。
-
實現(xiàn)手段:抽象(接口、抽象類)和多態(tài)是實現(xiàn)OCP的關(guān)鍵。
結(jié)合你的代碼:
-
FlowHandlerGatewayImpl的pageQueryWrapperExpand方法是一個空實現(xiàn)(鉤子方法)。這本身就是為擴展留下的“窗口”。 -
當(dāng)需要為
SimulationLoginPageCondition添加特殊的查詢條件時,你沒有修改FlowHandlerGatewayImpl的核心pageQuery方法,而是擴展了它,創(chuàng)建了SimulationLoginGatewayImpl子類并重寫了pageQueryWrapperExpand方法。 -
這正是對擴展開放,對修改關(guān)閉的完美體現(xiàn)。父類代碼穩(wěn)定,新功能通過子類擴展實現(xiàn)。
3. 里氏替換原則 (Liskov Substitution Principle - LSP)
核心思想:所有引用基類(父類)的地方必須能透明地使用其子類的對象,而程序的行為不會發(fā)生變化。
在Java中的理解:
-
子類可以擴展父類的功能,但不能改變父類原有的功能和行為約定(如方法簽名、返回值、異常拋出等)。
-
子類不應(yīng)該比父類有更嚴(yán)格的前置條件(比如,父類方法參數(shù)是
Integer,子類重寫時卻要求參數(shù)必須大于0,這就違反了LSP)。 -
** violation (違反)的例子:這正是你之前遇到的問題。如果強行在子類中將
FlowApprovalPageCondition參數(shù)轉(zhuǎn)換為SimulationLoginPageCondition,那么該子類對象就無法透明替換父類對象。因為父類方法可以接受任何FlowApprovalPageCondition,而你的子類方法實際上只接受特定的子類型,傳入其他類型會導(dǎo)致ClassCastException,行為被破壞了**。-
為什么行為被破壞了呢,和使用泛型有什么區(qū)別呢,有什么區(qū)別,用個例子講述下么,后果是什么?
-
里氏替換原則(LSP)的深入理解:強制轉(zhuǎn)換 vs 泛型
你提出的這個問題非常好,確實是理解LSP的一個關(guān)鍵點。讓我用一個更具體的例子來解釋為什么行為被破壞了,以及和使用泛型有什么區(qū)別。
生活化類比:通用插座 vs 特定插座
想象一下:
-
父類就像一個通用電源插座,承諾可以接受任何標(biāo)準(zhǔn)插頭(兩腳或三腳)
-
子類就像一個特定插座,但外觀和通用插座一樣
違反LSP的情況(強制轉(zhuǎn)換):
java// 這是一個"通用插座"(父類) class UniversalSocket { public void plugIn(Object plug) { // 承諾接受任何插頭 System.out.println("通用插座正在供電"); } } // 這是一個"特定插座"(子類),但外觀和通用插座一樣 class TypeASocket extends UniversalSocket { @Override public void plugIn(Object plug) { // 但實際上它只接受TypeA插頭 TypeAPlug typeAPlug = (TypeAPlug) plug; // 強制轉(zhuǎn)換! System.out.println("TypeA插座正在供電"); } } // 測試 public class Test { public static void main(String[] args) { UniversalSocket socket = new TypeASocket(); // 里氏替換:用子類代替父類 // 根據(jù)父類的約定,這里應(yīng)該可以插入任何插頭 socket.plugIn(new TypeBPlug()); // 但運行時這里會爆炸!ClassCastException } }符合LSP的情況(使用泛型):
java// 通用插座,但明確聲明自己支持的類型 class UniversalSocket<T extends Plug> { public void plugIn(T plug) { // 明確聲明接受的插頭類型 System.out.println("通用插座正在供電"); } } // 特定插座,繼承時明確指定類型 class TypeASocket extends UniversalSocket<TypeAPlug> { @Override public void plugIn(TypeAPlug plug) { // 現(xiàn)在參數(shù)類型匹配了 System.out.println("TypeA插座正在供電"); } } // 測試 public class Test { public static void main(String[] args) { UniversalSocket<TypeAPlug> socket = new TypeASocket(); // 類型安全 socket.plugIn(new TypeAPlug()); // 編譯通過,運行正常 // socket.plugIn(new TypeBPlug()); // 這行根本編譯不過!編譯器就報錯了 } }回到你的代碼案例
違反LSP的版本(強制轉(zhuǎn)換):
java// 父類:承諾接受任何FlowApprovalPageCondition class FlowHandlerGatewayImpl { protected void pageQueryWrapperExpand(LambdaQueryWrapper wrapper, FlowApprovalPageCondition condition) { // 空實現(xiàn),等待子類擴展 } } // 子類:違反LSP class SimulationLoginGatewayImpl extends FlowHandlerGatewayImpl { @Override protected void pageQueryWrapperExpand(LambdaQueryWrapper wrapper, FlowApprovalPageCondition condition) { // 破壞約定!實際上只接受SimulationLoginPageCondition SimulationLoginPageCondition simCondition = (SimulationLoginPageCondition) condition; // 使用simCondition的特有方法... } } // 使用場景 FlowHandlerGatewayImpl handler = new SimulationLoginGatewayImpl(); handler.pageQueryWrapperExpand(wrapper, new OtherPageCondition()); // 運行時爆炸!符合LSP的版本(泛型):
java// 父類:使用泛型明確約束 class FlowHandlerGatewayImpl<C extends FlowApprovalPageCondition> { protected void pageQueryWrapperExpand(LambdaQueryWrapper wrapper, C condition) { // 空實現(xiàn) } } // 子類:符合LSP class SimulationLoginGatewayImpl extends FlowHandlerGatewayImpl<SimulationLoginPageCondition> { @Override protected void pageQueryWrapperExpand(LambdaQueryWrapper wrapper, SimulationLoginPageCondition condition) { // 現(xiàn)在安全了,參數(shù)類型明確匹配 // 直接使用condition的特有方法... } } // 使用場景 FlowHandlerGatewayImpl<SimulationLoginPageCondition> handler = new SimulationLoginGatewayImpl(); handler.pageQueryWrapperExpand(wrapper, new SimulationLoginPageCondition()); // 安全 // handler.pageQueryWrapperExpand(wrapper, new OtherPageCondition()); // 編譯錯誤!關(guān)鍵區(qū)別和后果
方面 強制轉(zhuǎn)換(違反LSP) 泛型(符合LSP) 錯誤發(fā)現(xiàn)時間 運行時才發(fā)現(xiàn)錯誤 編譯時就能發(fā)現(xiàn)錯誤 錯誤類型 ClassCastException(運行時異常) 編譯錯誤 代碼安全性 低,容易產(chǎn)生隱藏bug 高,類型安全 可維護性 差,需要閱讀實現(xiàn)才知道限制 好,接口明確聲明了限制 替換透明度 不透明,子類有隱藏要求 透明,子類完全符合父類約定 擴展性 差,添加新類型需要修改多處 好,添加新類型只需新增代碼 嚴(yán)重后果示例
假設(shè)你的系統(tǒng)中有多種流程類型:
java// 模擬登錄流程 SimulationLoginGatewayImpl simulationHandler = new SimulationLoginGatewayImpl(); // 數(shù)據(jù)導(dǎo)出流程 DataExportGatewayImpl exportHandler = new DataExportGatewayImpl(); // 一個通用的流程處理器(違反LSP的情況) List<FlowHandlerGatewayImpl> allHandlers = Arrays.asList(simulationHandler, exportHandler); // 處理所有流程 for (FlowHandlerGatewayImpl handler : allHandlers) { // 這里傳入的參數(shù)可能是任何FlowApprovalPageCondition子類 handler.pageQueryWrapperExpand(wrapper, getRandomCondition()); // 俄羅斯輪盤賭! // 不知道哪個handler會在什么時候爆炸 }這種代碼就像一顆定時炸彈,你不知道它什么時候會爆炸,也不知道爆炸時會影響多少用戶。
總結(jié)
里氏替換原則的核心思想是:子類應(yīng)該能夠透明地替換父類,而使用者不需要知道也不需要關(guān)心這種替換。
-
強制轉(zhuǎn)換破壞了這種透明性,子類有隱藏的、更嚴(yán)格的要求
-
泛型通過類型參數(shù)明確聲明了這種要求,保持了透明性
關(guān)鍵區(qū)別在于:一個是運行時才發(fā)現(xiàn)問題(太晚了),一個是編譯時就發(fā)現(xiàn)問題(正是我們想要的)。
遵守LSP可以讓你的系統(tǒng)更加健壯、可靠,而違反LSP則會在系統(tǒng)中埋下難以發(fā)現(xiàn)的定時炸彈。這就是為什么我們要使用泛型而不是強制轉(zhuǎn)換的原因。
-
-
-
正確的做法(結(jié)合你的代碼):
-
使用泛型來保證類型安全,從而遵守LSP。
java// 父類 public class FlowHandlerGatewayImpl<C extends FlowApprovalPageCondition> { public Page<FlowApprovalPage> pageQuery(C condition) { ... } protected void pageQueryWrapperExpand(LambdaQueryWrapper<FlowApprovalPO> wrapper, C condition) {} } // 子類 public class SimulationLoginGatewayImpl extends FlowHandlerGatewayImpl<SimulationLoginPageCondition> { @Override protected void pageQueryWrapperExpand(LambdaQueryWrapper<FlowApprovalPO> wrapper, SimulationLoginPageCondition condition) { // 這里直接使用SimulationLoginPageCondition,無需強制轉(zhuǎn)換,且類型絕對安全 } } -
現(xiàn)在,任何期望使用
FlowHandlerGatewayImpl<SimulationLoginPageCondition>的地方,都可以安全地用SimulationLoginGatewayImpl來替換,因為子類方法完全滿足父類方法的契約(參數(shù)是SimulationLoginPageCondition,它是FlowApprovalPageCondition的子類),行為一致且不會出錯。
4. 擴展性 (Extensibility)
核心思想:軟件系統(tǒng)能夠容易地適應(yīng)新需求、添加新功能,而所需的工作量和成本最低,且對現(xiàn)有系統(tǒng)的影響最小。
在Java中的理解:
-
擴展性不是某個單一原則,而是良好應(yīng)用上述所有原則(SRP, OCP, LSP)以及依賴倒置、接口隔離等原則后的自然結(jié)果。
-
一個高擴展性的系統(tǒng),其結(jié)構(gòu)是松耦合的,通過抽象和接口定義契約,使得添加新模塊就像“插拔組件”一樣簡單。
結(jié)合你的代碼:
-
你目前的設(shè)計(尤其是使用泛型重構(gòu)后)具有很高的擴展性。
-
如何添加一個新的流程類型(如
DataExportPageCondition)?-
創(chuàng)建新的條件類:
DataExportPageCondition extends FlowApprovalPageCondition。 -
創(chuàng)建新的網(wǎng)關(guān)子類:
DataExportGatewayImpl extends FlowHandlerGatewayImpl<DataExportPageCondition>。 -
重寫擴展方法:在子類中重寫
pageQueryWrapperExpand,添加數(shù)據(jù)導(dǎo)出特有的查詢邏輯。
-
-
你做到了什么?
-
沒有修改任何現(xiàn)有的、穩(wěn)定的父類代碼(
FlowHandlerGatewayImpl) -> 遵循OCP。 -
新功能在獨立的、職責(zé)單一的新類中完成 -> 遵循SRP。
-
新的子類可以完全替換父類,行為一致 -> 遵循LSP。
-
整個過程的代價極小,風(fēng)險極低,因為只是添加新代碼而不是修改老代碼。
-
總結(jié)
原則 核心思想 在示例中的體現(xiàn)(良好設(shè)計后) 單一職責(zé) (SRP) 一個類只干一件事 FlowHandlerGatewayImpl只負責(zé)構(gòu)建查詢Wrapper,其他職責(zé)外包。開閉原則 (OCP) 對擴展開放,對修改關(guān)閉 通過泛型和可重寫的鉤子方法,允許無限擴展新的查詢條件類型,而無需修改基類。 里氏替換 (LSP) 子類必須能透明替換父類 使用泛型后,子類 SimulationLoginGatewayImpl可以安全替換FlowHandlerGatewayImpl<SimulationLoginPageCondition>。擴展性 容易添加新功能 添加新流程類型非常簡單、快速且安全,是應(yīng)用上述原則后的必然結(jié)果。 這些原則共同作用,指導(dǎo)我們構(gòu)建出健壯、靈活、易于維護和擴展的Java應(yīng)用程序。你代碼中最初遇到的問題,正是因為沒有很好地滿足LSP,進而影響了擴展的優(yōu)雅性。通過引入泛型,你同時完美地踐行了SRP、OCP和LSP,最終獲得了極高的擴展性。
-
-

浙公網(wǎng)安備 33010602011771號