继承和LSP

XIV*_*ons 5 java oop inheritance liskov-substitution-principle

为一个冗长的问题提前道歉.反馈在这里特别赞赏...

在我的工作中,我们做了很多与日期范围的事情(日期时间,如果你愿意).我们需要进行各种测量,比较两个日期之间的重叠等.我设计了一个接口,一个基类和几个派生类,它们满足了我迄今为止的需求:

  • IDatePeriod
  • DatePeriod
  • CalendarMonth
  • CalendarWeek
  • 财政年度

除了它的基本要素之外,DatePeriod超类如下(省略了所有迷人的特性,这些特征是我们为什么需要这组类的基础......):

(Java伪代码):

class datePeriod implements IDatePeriod

protected Calendar periodStartDate
protected Calendar periodEndDate

    public DatePeriod(Calendar startDate, Calendar endDate) throws DatePeriodPrecedenceException
    {
        periodStartDate = startDate
        . . . 
        // Code to ensure that the endDate cannot be set to a date which 
        // precedes the start date (throws exception)
        . . . 
        periodEndDate = endDate
    {

    public void setStartDate(Calendar startDate)
    {
        periodStartDate = startDate
        . . . 
        // Code to ensure that the current endDate does not 
        // precede the new start date (it resets the end date
        // if this is the case)
        . . . 
    {


    public void setEndDate(Calendar endDate) throws datePeriodPrecedenceException
    {
        periodEndDate = EndDate
        . . . 
        // Code to ensure that the new endDate does not 
        // precede the current start date (throws exception)
        . . . 
    {


// a bunch of other specialty methods used to manipulate and compare instances of DateTime

}
Run Code Online (Sandbox Code Playgroud)

基类包含一堆用于操作日期周期类的相当专业的方法和属性.派生类更改设置相关时段的起点和终点的方式.例如,对我来说,CalendarMonth对象确实是"is-a"DatePeriod是有道理的.但是,由于显而易见的原因,日历月具有固定的持续时间,并且具有特定的开始和结束日期.实际上,虽然CalendarMonth类的构造函数与超类的构造函数匹配(因为它具有startDate和endDate参数),但实际上这是一个简化构造函数的重载,它只需要一个Calendar对象.

对于CalendarMonth,提供任何日期将导致CalendarMonth实例从该月的第一天开始,并在同一个月的最后一天结束.

public class CalendarMonth extends DatePeriod

    public CalendarMonth(Calendar dateInMonth)
    {
        // call to method which initializes the object with a periodStartDate
        // on the first day of the month represented by the dateInMonth param,
        // and a periodEndDate on the last day of the same month.
    }

    // For compatibility with client code which might use the signature
    // defined on the super class:
    public CalendarMonth(Calendar startDate, Calendar endDate)
    {
        this(startDate)
        // The end date param is ignored. 
    }

    public void setStartDate(Calendar startDate)
    {
        periodStartDate = startDate
        . . . 
    // call to method which resets the periodStartDate
    // to the first day of the month represented by the startDate param,
    // and the periodEndDate to the last day of the same month.
        . . . 
    {


    public void setEndDate(Calendar endDate) throws datePeriodPrecedenceException
    {
        // This stub is here for compatibility with the superClass, but
        // contains either no code, or throws an exception (not sure which is best).
    {
}
Run Code Online (Sandbox Code Playgroud)

为长序言道歉.鉴于上述情况,似乎这种类结构违反了Liskov替代原则.虽然一个CAN在任何情况下都使用CalendarMonth的实例,其中一个可能使用更通用的DatePeriod类,但关键方法的输出行为将是不同的.换句话说,必须意识到在给定情况下正在使用CalendarMonth的实例.

虽然CalendarMonth(或CalendarWeek等)遵守通过基类使用IDatePeriod建立的合同,但在使用CalendarMonth并且预期使用普通旧DatePeriod的行为的情况下,结果可能会变得严重偏差...(请注意,基类上定义的所有其他时髦方法都能正常工作 - 只有CalendarMonth实现中的开始日期和结束日期的设置不同).

有没有更好的方法来构建它,以便可以保持对LSP的正确遵守,而不会影响可用性和/或重复代码?

And*_*mas 6

这似乎与关于正方形和矩形的通常讨论类似.虽然正方形是一个矩形,但Square从Rectangle继承是没有用的,因为它不能满足Rectangle的预期行为.

DatePeriod有一个setStartDate()和setEndDate()方法.使用DatePeriod,您可以期望两者可以按任何顺序调用,不会相互影响,也可能是它们的值将精确指定开始日期和结束日期.但是对于CalendarMonth实例,情况并非如此.

也许,不是让CalendarMonth扩展DatePeriod,而是两者都可以扩展一个公共抽象类,它只包含与两者兼容的方法.

顺便说一下,根据你问题的深思熟虑,我猜你已经考虑过寻找现有的日期库.如果你没有,请务必查看Joda时间库,其中包括可变和不可变时段的类.如果现有的库可以解决您的问题,您可以专注于自己的软件,让其他人支付设计,开发和维护时间库的成本.

编辑:注意我已将CalendarMonth类称为日历.为清楚起见固定.