面向对象编程:无法修改父类时如何实现OpenTicket?
面向对象设计:无修改权限下实现OpenTicket的最优方案
问题背景
现有一个不可修改的Java Ticket 类实现车票功能,代码如下:
public class Ticket { private LocalDateTime dateAndTime; private String departureCity; private String arrivalCity; public Ticket(LocalDateTime dateAndTime, String departureCity, String arrivalCity) { assert (isDateAndTimeCorrect(dateAndTime)); assert (isRouteCorrect(departureCity, arrivalCity)); /** ... **/ } public double getPrice() { /** ... **/ } @Override public String toString() { /** ... **/ } public String getDepartureCity() { /** ... **/ } public String getArrivalCity() { /** ... **/ } public LocalDateTime getDateAndTime() { /** ... **/ } public boolean isDateAndTimeCorrect(LocalDateTime dateAndTime) { return dateAndTime != null && dateAndTime.isAfter(LocalDateTime.now()); } public boolean isRouteCorrect(String departureCity, String arrivalCity) { return departureCity != null && arrivalCity != null && !departureCity.isEmpty() && !arrivalCity.isEmpty() && !departureCity.equals(arrivalCity); } }
需要在不修改原Ticket类的前提下,新增支持未确定日期时间的OpenTicket类型,现有两种方案待评估:
- 子类调用父类构造器时传入当前时间之后的某个未来日期
- 传入
null并设法规避断言
方案对比与结论
方案2(传入null并规避断言):绝对不可取
- 断言是原类用来验证前置条件的设计,规避断言本质上直接破坏了
Ticket的设计契约。原类明确要求dateAndTime必须是非空且在未来的合法值,强行传入null会让Ticket内部状态完全不符合预期,后续调用getDateAndTime()等方法大概率触发NullPointerException,或让依赖Ticket状态的业务逻辑出现异常。 - 这种行为违背里氏替换原则:
OpenTicket作为子类,应当能安全替换父类使用,但非法状态的子类根本无法保证父类所有方法正常工作。 - 规避断言的手段(比如全局关闭断言开关)极不可靠:开关是JVM级别的,无法仅针对
OpenTicket生效,会导致其他依赖断言的代码也失去前置验证,引入更多潜在bug。
方案1(传入合法未来日期):当前约束下的最优选择
这个方案严格遵守父类的设计契约,保证OpenTicket的父类部分处于合法状态,符合里氏替换原则,不会破坏原类逻辑。但需要补充子类的封装逻辑,明确“占位日期”的业务含义:
public class OpenTicket extends Ticket { private boolean isDateUnconfirmed = true; public OpenTicket(String departureCity, String arrivalCity) { // 传入一个较远的未来日期作为占位,比如10年后的同一时刻 super(LocalDateTime.now().plusYears(10), departureCity, arrivalCity); } // 对外暴露未确定日期的标识 public boolean isDateUnconfirmed() { return isDateUnconfirmed; } // 重写getDateAndTime,明确告知调用者当前日期未确认 @Override public LocalDateTime getDateAndTime() { if (isDateUnconfirmed) { throw new IllegalStateException("该未确认日期的车票暂无确定出行时间"); } return super.getDateAndTime(); } // 提供确认日期的方法,验证并更新状态 public void confirmDate(LocalDateTime confirmedDate) { if (!super.isDateAndTimeCorrect(confirmedDate)) { throw new IllegalArgumentException("确认日期必须是未来的时间"); } isDateUnconfirmed = false; // 注:如果父类没有提供修改dateAndTime的入口,可根据业务调整逻辑,比如后续返回确认后的日期 } }
核心思路是在遵守父类契约的基础上,通过子类扩展实现“未确定日期”的业务特性,既保证了与原系统的兼容性,又符合面向对象的封装、继承原则。
额外优化建议:优先考虑组合而非继承
如果业务允许调整依赖结构,OpenTicket可以不继承Ticket,而是通过组合方式持有Ticket实例,确认日期时再创建合法的Ticket对象。这种方式更符合组合优于继承的原则,彻底避免继承带来的契约冲突:
public class OpenTicket { private String departureCity; private String arrivalCity; private Ticket confirmedTicket; public OpenTicket(String departureCity, String arrivalCity) { // 复用父类的路线合法性验证逻辑 if (!new Ticket(null, departureCity, arrivalCity).isRouteCorrect(departureCity, arrivalCity)) { throw new IllegalArgumentException("出发地与到达地不合法"); } this.departureCity = departureCity; this.arrivalCity = arrivalCity; } public boolean isConfirmed() { return confirmedTicket != null; } public void confirmDate(LocalDateTime dateTime) { this.confirmedTicket = new Ticket(dateTime, departureCity, arrivalCity); } public Ticket getConfirmedTicket() { if (!isConfirmed()) { throw new IllegalStateException("车票尚未确认日期"); } return confirmedTicket; } // 提供必要的属性访问方法 public String getDepartureCity() { return departureCity; } public String getArrivalCity() { return arrivalCity; } }
这种方案的缺点是需要适配原系统对Ticket类型的依赖,但从面向对象设计的健壮性来看,这是更优的长期方案。
内容的提问来源于stack exchange,提问作者Cardstdani
相关产品推荐
相关产品推荐

