提前为一个冗长的问题道歉。反馈在这里特别赞赏。 。
在我的工作中,我们使用日期范围做很多事情(日期期间,如果你愿意的话)。我们需要进行各种测量,比较两个日期之间的重叠等。我设计了一个接口,一个基类和几个派生类,它们满足了我的需求:
根据其基本要求,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的正确遵守,而不会影响可用性和/或重复代码?
答案 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
的方法,并保持所有内容完全不可变,或者创建不会尝试处理数年,数月,数周等的备用可变子类。