Наследование и LSP

Заранее извиняюсь за многословный вопрос. Обратная связь особенно ценится здесь.,,

В моей работе мы многое делаем с диапазонами дат (периоды дат, если хотите). Нам нужно проводить всевозможные измерения, сравнивать перекрытия между двумя периодами дат и т. Д. Я разработал Интерфейс, базовый класс и несколько производных классов, которые хорошо соответствуют моим потребностям на сегодняшний день:

  • 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

}

Базовый класс содержит набор довольно специализированных методов и свойств для манипулирования классом периода даты. Производные классы изменяют только способ установки начальной и конечной точек рассматриваемого периода. Например, для меня имеет смысл, что объект 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).
    {
}

Извиняюсь за длинную преамбулу. Учитывая ситуацию выше, казалось бы, эта классовая структура нарушает принцип подстановки Лискова. В то время как один МОЖЕТ использовать экземпляр CalendarMonth в любом случае, когда можно использовать более общий класс DatePeriod, поведение ключевых методов будет другим. Другими словами, нужно знать, что в данной ситуации используется экземпляр CalendarMonth.

Хотя CalendarMonth (или CalendarWeek и т. Д.) Придерживаются контракта, заключенного с помощью использования базового класса IDatePeriod, результаты могут стать ужасно искаженными в ситуации, когда использовался CalendarMonth и ожидалось поведение простого старого DatePeriod.,, (Обратите внимание, что ВСЕ другие фанки-методы, определенные в базовом классе, работают правильно - это только установка даты начала и окончания, которая отличается в реализации CalendarMonth).

Есть ли лучший способ структурировать это так, чтобы можно было поддерживать надлежащее соблюдение LSP, не ставя под угрозу удобство использования и / или дублирование кода?

4 ответа

Решение

Это похоже на обычное обсуждение квадратов и прямоугольников. Хотя квадрат - это прямоугольник, квадрат не наследует прямоугольник, потому что он не может удовлетворить ожидаемое поведение прямоугольника.

Ваш DatePeriod имеет метод setStartDate() и setEndDate(). С DatePeriod вы ожидаете, что эти два могут быть вызваны в любом порядке, не будут влиять друг на друга, и, возможно, что их значения будут точно указывать дату начала и окончания. Но с экземпляром CalendarMonth это не так.

Возможно, вместо того, чтобы CalendarMonth расширял DatePeriod, оба могли бы расширять общий абстрактный класс, который содержит только методы, совместимые с обоими.

Кстати, исходя из вдумчивости вашего вопроса, я предполагаю, что вы уже думали искать существующие библиотеки дат. На всякий случай, если вы этого не сделали, обязательно посмотрите библиотеку времени Joda, которая включает в себя классы для изменяемых и неизменяемых периодов. Если существующая библиотека решит вашу проблему, вы можете сконцентрироваться на собственном программном обеспечении и позволить кому-то еще оплатить стоимость проектирования, разработки и обслуживания библиотеки времени.

Изменить: заметил, что я назвал ваш класс CalendarMonth как календарь. Исправлено для ясности.

Я думаю, что проблема моделирования заключается в том, что ваш 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);

Это должно быть достаточно толчка. Дайте мне знать, если вам нужны какие-либо разъяснения.

Часто соблюдение LSP требует тщательного документирования того, что делает базовый класс или интерфейс.

Например, в Java Collection имеет метод с именем add(E), Это может иметь эту документацию:

Добавляет указанный элемент в эту коллекцию.

Но если это произойдет, то это будет очень трудно для Set, который поддерживает инвариант без дубликатов, чтобы не нарушать LSP. Так что вместо этого add(E) задокументировано так:

Гарантирует, что эта коллекция содержит указанный элемент (необязательная операция).

Теперь ни один клиент не может использовать Collection и ожидаем, что элемент всегда будет добавлен, даже если он уже существовал в коллекции.

Я не слишком подробно изучил ваш пример, но мне кажется, что вы можете быть осторожны. Что делать, если вы в вашем интерфейсе даты периода, setStartDate() было задокументировано так:

Гарантирует, что датой начала является указанная дата.

Не уточняя ничего дальше? Или даже,

Гарантирует, что начальной датой является указанная дата, при необходимости изменяя конечную дату, чтобы поддерживать любые конкретные инварианты подкласса.

setEndDate() могут быть реализованы и аналогичным образом задокументированы. Как тогда конкретная реализация сломает LSP?

Примечание. Стоит также отметить, что удовлетворять LSP намного проще, если вы сделаете свой класс неизменным.

Это, безусловно, нарушает LSP, точно так же, как классический пример Ellipse и Circle.

Если ты хочешь CalendarMonth расширить DatePeriod, ты должен сделать DatePeriod неизменный.

Затем вы можете либо изменить все методы мутации на методы, которые возвращают новый DatePeriod и сохраняйте все в неизменном виде или создайте альтернативный изменяемый подкласс, который не пытается иметь дело с годами, месяцами, неделями и тому подобным.

Другие вопросы по тегам