【问题标题】:Inheritance and LSP继承和 LSP
【发布时间】:2011-06-18 22:06:27
【问题描述】:

提前为一个冗长的问题道歉。反馈特别感谢这里。 . .

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

  • IDatePeriod
  • 日期期间
  • 日历月
  • 日历周
  • 财政年度

从本质上来说,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 对象确实“是一个”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).
    {
}

对于冗长的序言表示歉意。鉴于上述情况,这种类结构似乎违反了 Liskov 替换原则。虽然可以在任何可能使用更通用的 DatePeriod 类的情况下使用 CalendarMonth 的实例,但关键方法的输出行为会有所不同。换句话说,必须意识到在给定情况下正在使用 CalendarMonth 的实例。

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

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

【问题讨论】:

  • 感谢所有花时间阅读本文并提供有用反馈的人 - 谢谢!很难选择一个“正确”的答案。我选择了看起来“技术上”最正确的那个(找到一个已经存在的库!除此之外,从一个通用的基类扩展)。但是,我认为提供的所有答案都具有同等价值。再次感谢!

标签: java oop inheritance liskov-substitution-principle


【解决方案1】:

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

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

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

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

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

【讨论】:

  • +1:Joda 确实是一个很好的实现,并且通过使许多东西不可变来处理这个问题。
  • 同意,事实上,当我遇到正方形/矩形参数时,我意识到我需要检查这个。 . .
  • 我不知道 Joda 这样的图书馆,但你可以打赌我会去看看! - 但请注意,我是 Java 的新手,部分原因是为了让自己熟悉该语言。我在工作中使用的这个实现实际上是在 vb.net 中。我决定学习新语言的最好方法是重建我曾经知道该怎么做的东西!
  • Java 库的数量是巨大的!
  • @XIVSolutions: 1) 很好。 2)好主意! 3) 是不是很酷?
【解决方案2】:

我认为建模问题是您的CalendarMonth 类型并不是真正的不同时期种类。相反,它是一个 constructor 或者,如果您愿意,也可以是 factory function 来创建这样的周期。

我将删除 CalendarMonth 类并创建一个名为 Periods 之类的实用程序类,它具有私有构造函数和各种返回各种 IDatePeriod 实例的公共静态方法实例。 p>

这样就可以写了

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);

这应该足够了。如果您需要任何说明,请告诉我。

【讨论】:

  • 这看起来很优雅,因为我确实想保留 WholeMonthBonding 的想法。好的!我会玩这个并回复你!
  • +1:如果您想保持事物的可变性并保持简单的层次结构,这是一种合理的方法。
  • 是的,看起来我添加了很多派生类只是为了完成一些固定的日期周期。我还已经实现了一个工厂,它基于一个枚举参数和一个 switch 语句(有点幸运,那个)产生了各种不同的派生类型。我认为不变性+工厂是要走的路。当然,这使我不得不返回 startDate 和 EndDate 属性的克隆副本,这样就不能更改包含的成员实例。这也让人觉得很幸运。 . .
  • @Don Roby - 我完全错过了你的观点。我同意 - 我也可以通过这种方法使事情保持可变。现在我必须在两者之间做出决定。 Immutable 有一些吸引力,但对超类来说感觉不自然。 Mutable 对 super 来说很酷,但不是派生的。我也在考虑创建一个公共基础(上面的 Andy Thomas-Cramer),可变的 DatePeriod 和不可变的 FixedDatePeriod 可以从中派生。当然,这增加了复杂性。 . .
  • 对这篇文章的补充很好——我正在做一些收容测试,但我的实施还没有那么先进。我基本上创建了一些方法来确定一些事情,例如第二个周期是否 a) 包含在当前周期中,b) 第二个周期包含当前周期,c) 第二个周期在当前周期之前开始并在当前周期内结束(我们在房地产行业将全部“Pro-Out”)等。我现在将研究 Java 上下文中的谓词。谢谢你的例子!
【解决方案3】:

通常,遵守 LSP 就是要仔细记录基类或接口的作用。

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

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

但如果确实如此,那么维护无重复不变量的Set 将很难不违反 LSP。因此,add(E) 是这样记录的:

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

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

我没有对您的示例进行过深入研究,但令我印象深刻的是,您也许可以如此小心。如果您在日期期间界面中,setStartDate() 的记录如下:

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

没有进一步说明?甚至,

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

setEndDate() 可以实现并被类似地记录。那么具体的实现会如何破坏 LSP?

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

【讨论】:

  • 同意,我正在考虑采用这种方法!
  • 恕我直言,Java 和 .NET 中集合的主要问题之一是它们没有更好地定义可选功能以及 确定支持它们的方式的方法。在界面的消费者倾向于有千差万别的需求的情况下,尝试为每个能力子集定义不同的类型会适得其反。最好为接口编写契约,这样实现可以提供任何能力组合,而不必承诺任何它无法提供的能力。
【解决方案4】:

这确实违反了 LSP,与经典的椭圆和圆示例完全相同。

如果你想让CalendarMonth 扩展DatePeriod,你应该让DatePeriod 不可变。

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

【讨论】:

  • 我考虑过让 DatePeriod 和派生类不可变。我们会看看它是怎么回事。 . .
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-10-07
  • 2015-12-09
  • 1970-01-01
  • 1970-01-01
  • 2013-12-12
  • 2020-06-19
相关资源
最近更新 更多