继承和LSP

时间:2011-06-18 22:06:28

标签: java oop inheritance liskov-substitution-principle

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

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

  • IDatePeriod
  • DatePeriod
  • CalendarMonth
  • CalendarWeek
  • FiscalYear

根据其基本要求,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

}

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

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

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).
    {
}

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

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

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

4 个答案:

答案 0 :(得分:6)

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

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

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

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

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

答案 1 :(得分:2)

我认为建模问题是您的CalendarMonth类型与句点不是真正不同的种类。相反,它是构造函数,或者,如果您愿意,还可以使用工厂函数来创建这样的句点。

我将消除CalendarMonth类并创建一个名为Periods的实用程序类,其中包含一个私有构造函数和各种返回各种IDatePeriod 实例的公共静态方法< / em>的

有了这个,就可以写

final IDatePeriod period = Periods.wholeMonthBounding(Calendar day);

wholeMonthBounding()函数的文档将解释调用者对返回的IDatePeriod实例的期望。 Bikeshedding,此函数的替代名称可以是wholeMonthContaining()


考虑您打算如何处理“期间”。如果目标是进行“遏制测试”,如“这一刻是否在某个时期内?”那么,那么你可能想要承认无限和半有界的时期。

这表明你定义了一些包含谓词类型,例如

interface PeriodPredicate
{
  boolean containsMoment(Calendar day);
}

然后前面提到的Periods类 - 或许用这个详细说明命名为PeriodPredicates - 可能会暴露更多的函数,比如

// First, some absolute periods:
PeriodPredicate allTime(); // always returns true
PeriodPredicate everythingBefore(Calendar end);
PeriodPredicate everythingAfter(Calendar start);
enum Boundaries
{
  START_INCLUSIVE_END_INCLUSIVE,
  START_INCLUSIVE_END_EXCLUSIVE,
  START_EXCLUSIVE_END_INCLUSIVE,
  START_EXCLUSIVE_END_EXCLUSIVE
}
PeriodPredicate durationAfter(Calendar start, long duration, TimeUnit unit,
                              Boundaries boundaries);
PeriodPredicate durationBefore(Calendar end, long duration, TimeUnit unit
                               Boundaries boundaries);

// Consider relative periods too:
PeriodPredicate inThePast();   // exclusive with now
PeriodPredicate inTheFuture(); // exclusive with now
PeriodPredicate withinLastDuration(long duration, TimeUnit unit); // inclusive from now
PeriodPredicate withinNextDuration(long duration, TimeUnit unit); // inclusive from now
PeriodPredicate withinRecentDuration(long pastOffset, TimeUnit offsetUnit,
                                     long duration, TimeUnit unit,
                                     Boundaries boundaries);
PeriodPredicate withinFutureDuration(long futureOffset, TimeUnit offsetUnit,
                                     long duration, TimeUnit unit,
                                     Boundaries boundaries);

这应该足够了。如果您需要任何澄清,请与我们联系。

答案 2 :(得分:1)

通常,遵守LSP是关于记录基类或接口所做的一丝不苟的事情。

例如,在Java Collection中有一个名为add(E)的方法。它可以有这个文档:

  

将指定的元素添加到此集合中。

但如果确实如此,那么保持不重复不变量的Set将不会违反LSP。相反,add(E)的记录如下:

  

确保此集合包含指定的元素(可选操作)。

现在没有客户可以使用Collection并期望即使元素已经存在于该集合中,也会始终添加该元素。

我对你的例子并没有太深入了解,但是我觉得你可能会小心翼翼。如果您在日期期间界面中setStartDate()的记录如下:

  

确保开始日期是指定的日期。

没有进一步说明?甚至,

  

确保开始日期是指定的日期,可选择更改结束日期以维护子类的任何特定不变量。

setEndDate()可以实施并且可以类似地记录下来。具体实现如何打破LSP?

注意还值得一提的是,如果你让你的类不可变,那么满足LSP要容易得多。

答案 3 :(得分:1)

这肯定违反了LSP,与经典的Ellipse和Circle示例完全相同。

如果您希望CalendarMonth扩展DatePeriod,则应使DatePeriod不可变。

然后,您可以将所有变异方法更改为返回新DatePeriod的方法,并保持所有内容完全不可变,或者创建不会尝试处理数年,数月,数周等的备用可变子类。