5

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

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

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

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 的正确遵守?

4

4 回答 4

6

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

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

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

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

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

于 2011-06-18T22:11:02.527 回答
2

我认为建模问题在于您的CalendarMonth类型并不是真正不同时期。相反,它是一个构造函数,或者,如果您愿意,它是用于创建此类周期的工厂函数。

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

有了这个,一个人可以写

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

这应该是足够的推动力。如果您需要任何澄清,请告诉我。

于 2011-06-18T22:40:26.283 回答
1

很多时候,遵守 LSP 就是要仔细记录基类或接口的作用。

例如,在 JavaCollection中有一个名为add(E). 它可能有这个文档:

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

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

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

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

我没有对您的示例进行过深入研究,但是让我感到震惊的是,您可能可以同样小心。如果您在日期期间界面setStartDate()中的记录如下:

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

没有进一步说明?甚至,

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

setEndDate()可以实施并以类似方式记录在案。那么具体的实现将如何破坏 LSP?

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

于 2011-06-18T22:21:58.257 回答
1

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

如果要CalendarMonth扩展DatePeriod,则应使其DatePeriod不可变。

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

于 2011-06-18T22:31:21.323 回答