子类不依赖泛型,重写父类方法,通过强制类型转换父类方法参数出现的问题。——— 一个例子引发的思考
-
使用泛型(推荐)
-
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()); } }
-
-
通过强制类型转换。其中SimulationLoginPageCondition实体继承FlowApprovalPageCondition实体,子类SimulationLoginPageCondition为什么不能使用多态特性,直接重写父类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); } //出现报错,重写方法参数和父类不一样、违反了java规范 @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()); } // 强制类型转换,子类中重写父类方法,也不推荐后面有原因,违反了开闭原则,下面有讲解 @Override protected void pageQueryWrapperExpand(LambdaQueryWrapper<FlowApprovalPO> wrapper, FlowApprovalPageCondition condition) { // 强制类型转换 SimulationLoginPageCondition simCondition = (SimulationLoginPageCondition) condition; // 使用转换后的条件 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. 单一职责原则 (Single Responsibility Principle - SRP)
核心思想:一个类应该只有一个引起它变化的原因。换句话说,一个类应该只负责一项职责。
在Java中的理解:
-
好处:类的职责越单一,它的内聚性就越高,就越容易被理解、维护和修改。修改一个功能不会意外影响到其他不相关的功能。
-
** violation (违反)的例子**:如果一个类既负责数据库操作,又负责业务逻辑计算,还负责发送邮件,那它就违反了SRP。此时,修改数据库连接方式、业务算法或邮件服务器配置都会修改这个类,风险很高。
结合你的代码:
-
FlowHandlerGatewayImpl
的职责非常明确:构建查询流程审批单的分页条件。它不关心具体的SQL执行(由FlowApprovalWrapper
负责),也不关心PO到Domain的转换(由InfraConverter
负责)。它通过依赖注入将其他职责委托给了专门的类,这很好地遵循了SRP。
2. 开闭原则 (Open/Closed Principle - OCP)
核心思想:软件实体(类、模块、函数等)应该对扩展开放,但对修改关闭。
在Java中的理解:
-
“对修改关闭”:意味着一个已经完成并测试通过的类的核心代码不应该再被修改。
-
“对扩展开放”:意味着当有新的需求时,你应该能够通过扩展这个类(如通过继承、组合、实现接口等方式)来添加新功能,而不是修改它。
-
实现手段:抽象(接口、抽象类)和多态是实现OCP的关键。
结合你的代码:
-
FlowHandlerGatewayImpl
的pageQueryWrapperExpand
方法是一个空实现(钩子方法)。这本身就是为扩展留下的“窗口”。 -
当需要为
SimulationLoginPageCondition
添加特殊的查询条件时,你没有修改FlowHandlerGatewayImpl
的核心pageQuery
方法,而是扩展了它,创建了SimulationLoginGatewayImpl
子类并重写了pageQueryWrapperExpand
方法。 -
这正是对扩展开放,对修改关闭的完美体现。父类代码稳定,新功能通过子类扩展实现。
3. 里氏替换原则 (Liskov Substitution Principle - LSP)
核心思想:所有引用基类(父类)的地方必须能透明地使用其子类的对象,而程序的行为不会发生变化。
在Java中的理解:
-
子类可以扩展父类的功能,但不能改变父类原有的功能和行为约定(如方法签名、返回值、异常抛出等)。
-
子类不应该比父类有更严格的前置条件(比如,父类方法参数是
Integer
,子类重写时却要求参数必须大于0,这就违反了LSP)。 -
** violation (违反)的例子:这正是你之前遇到的问题。如果强行在子类中将
FlowApprovalPageCondition
参数转换为SimulationLoginPageCondition
,那么该子类对象就无法透明替换父类对象。因为父类方法可以接受任何FlowApprovalPageCondition
,而你的子类方法实际上只接受特定的子类型,传入其他类型会导致ClassCastException
,行为被破坏了**。-
为什么行为被破坏了呢,和使用泛型有什么区别呢,有什么区别,用个例子讲述下么,后果是什么?
-
里氏替换原则(LSP)的深入理解:强制转换 vs 泛型
你提出的这个问题非常好,确实是理解LSP的一个关键点。让我用一个更具体的例子来解释为什么行为被破坏了,以及和使用泛型有什么区别。
生活化类比:通用插座 vs 特定插座
想象一下:
-
父类就像一个通用电源插座,承诺可以接受任何标准插头(两脚或三脚)
-
子类就像一個特定插座,但外观和通用插座一样
违反LSP的情况(强制转换):
// 这是一个"通用插座"(父类) 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; // 强制转换! System.out.println("TypeA插座正在供电"); } } // 测试 public class Test { public static void main(String[] args) { UniversalSocket socket = new TypeASocket(); // 里氏替换:用子类代替父类 // 根据父类的约定,这里应该可以插入任何插头 socket.plugIn(new TypeBPlug()); // 但运行时这里会爆炸!ClassCastException } }
符合LSP的情况(使用泛型):
// 通用插座,但明确声明自己支持的类型 class UniversalSocket<T extends Plug> { public void plugIn(T plug) { // 明确声明接受的插头类型 System.out.println("通用插座正在供电"); } } // 特定插座,继承时明确指定类型 class TypeASocket extends UniversalSocket<TypeAPlug> { @Override public void plugIn(TypeAPlug plug) { // 现在参数类型匹配了 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的版本(强制转换):
// 父类:承诺接受任何FlowApprovalPageCondition class FlowHandlerGatewayImpl { protected void pageQueryWrapperExpand(LambdaQueryWrapper wrapper, FlowApprovalPageCondition condition) { // 空实现,等待子类扩展 } } // 子类:违反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的版本(泛型):
// 父类:使用泛型明确约束 class FlowHandlerGatewayImpl<C extends FlowApprovalPageCondition> { protected void pageQueryWrapperExpand(LambdaQueryWrapper wrapper, C condition) { // 空实现 } } // 子类:符合LSP class SimulationLoginGatewayImpl extends FlowHandlerGatewayImpl<SimulationLoginPageCondition> { @Override protected void pageQueryWrapperExpand(LambdaQueryWrapper wrapper, SimulationLoginPageCondition condition) { // 现在安全了,参数类型明确匹配 // 直接使用condition的特有方法... } } // 使用场景 FlowHandlerGatewayImpl<SimulationLoginPageCondition> handler = new SimulationLoginGatewayImpl(); handler.pageQueryWrapperExpand(wrapper, new SimulationLoginPageCondition()); // 安全 // handler.pageQueryWrapperExpand(wrapper, new OtherPageCondition()); // 编译错误!
关键区别和后果
方面 强制转换(违反LSP) 泛型(符合LSP) 错误发现时间 运行时才发现错误 编译时就能发现错误 错误类型 ClassCastException(运行时异常) 编译错误 代码安全性 低,容易产生隐藏bug 高,类型安全 可维护性 差,需要阅读实现才知道限制 好,接口明确声明了限制 替换透明度 不透明,子类有隐藏要求 透明,子类完全符合父类约定 扩展性 差,添加新类型需要修改多处 好,添加新类型只需新增代码 严重后果示例
假设你的系统中有多种流程类型:
// 模拟登录流程 SimulationLoginGatewayImpl simulationHandler = new SimulationLoginGatewayImpl(); // 数据导出流程 DataExportGatewayImpl exportHandler = new DataExportGatewayImpl(); // 一个通用的流程处理器(违反LSP的情况) List<FlowHandlerGatewayImpl> allHandlers = Arrays.asList(simulationHandler, exportHandler); // 处理所有流程 for (FlowHandlerGatewayImpl handler : allHandlers) { // 这里传入的参数可能是任何FlowApprovalPageCondition子类 handler.pageQueryWrapperExpand(wrapper, getRandomCondition()); // 俄罗斯轮盘赌! // 不知道哪个handler会在什么时候爆炸 }
这种代码就像一颗定时炸弹,你不知道它什么时候会爆炸,也不知道爆炸时会影响多少用户。
总结
里氏替换原则的核心思想是:子类应该能够透明地替换父类,而使用者不需要知道也不需要关心这种替换。
-
强制转换破坏了这种透明性,子类有隐藏的、更严格的要求
-
泛型通过类型参数明确声明了这种要求,保持了透明性
关键区别在于:一个是运行时才发现问题(太晚了),一个是编译时就发现问题(正是我们想要的)。
遵守LSP可以让你的系统更加健壮、可靠,而违反LSP则会在系统中埋下难以发现的定时炸弹。这就是为什么我们要使用泛型而不是强制转换的原因。
-
-
-
正确的做法(结合你的代码):
-
使用泛型来保证类型安全,从而遵守LSP。
// 父类 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,无需强制转换,且类型绝对安全 } }
-
现在,任何期望使用
FlowHandlerGatewayImpl<SimulationLoginPageCondition>
的地方,都可以安全地用SimulationLoginGatewayImpl
来替换,因为子类方法完全满足父类方法的契约(参数是SimulationLoginPageCondition
,它是FlowApprovalPageCondition
的子类),行为一致且不会出错。
4. 扩展性 (Extensibility)
核心思想:软件系统能够容易地适应新需求、添加新功能,而所需的工作量和成本最低,且对现有系统的影响最小。
在Java中的理解:
-
扩展性不是某个单一原则,而是良好应用上述所有原则(SRP, OCP, LSP)以及依赖倒置、接口隔离等原则后的自然结果。
-
一个高扩展性的系统,其结构是松耦合的,通过抽象和接口定义契约,使得添加新模块就像“插拔组件”一样简单。
结合你的代码:
-
你目前的设计(尤其是使用泛型重构后)具有很高的扩展性。
-
如何添加一个新的流程类型(如
DataExportPageCondition
)?-
创建新的条件类:
DataExportPageCondition extends FlowApprovalPageCondition
。 -
创建新的网关子类:
DataExportGatewayImpl extends FlowHandlerGatewayImpl<DataExportPageCondition>
。 -
重写扩展方法:在子类中重写
pageQueryWrapperExpand
,添加数据导出特有的查询逻辑。
-
-
你做到了什么?
-
没有修改任何现有的、稳定的父类代码(
FlowHandlerGatewayImpl
) -> 遵循OCP。 -
新功能在独立的、职责单一的新类中完成 -> 遵循SRP。
-
新的子类可以完全替换父类,行为一致 -> 遵循LSP。
-
整个过程的代价极小,风险极低,因为只是添加新代码而不是修改老代码。
-
总结
原则 核心思想 在示例中的体现(良好设计后) 单一职责 (SRP) 一个类只干一件事 FlowHandlerGatewayImpl
只负责构建查询Wrapper,其他职责外包。开闭原则 (OCP) 对扩展开放,对修改关闭 通过泛型和可重写的钩子方法,允许无限扩展新的查询条件类型,而无需修改基类。 里氏替换 (LSP) 子类必须能透明替换父类 使用泛型后,子类 SimulationLoginGatewayImpl
可以安全替换FlowHandlerGatewayImpl<SimulationLoginPageCondition>
。扩展性 容易添加新功能 添加新流程类型非常简单、快速且安全,是应用上述原则后的必然结果。 这些原则共同作用,指导我们构建出健壮、灵活、易于维护和扩展的Java应用程序。你代码中最初遇到的问题,正是因为没有很好地满足LSP,进而影响了扩展的优雅性。通过引入泛型,你同时完美地践行了SRP、OCP和LSP,最终获得了极高的扩展性。
-
-