Наследование и LSP - PullRequest
       55

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

5 голосов
/ 19 июня 2011

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

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

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

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

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

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

Ответы [ 4 ]

6 голосов
/ 19 июня 2011

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

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

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

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

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

2 голосов
/ 19 июня 2011

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

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

1 голос
/ 19 июня 2011

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

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

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

1 голос
/ 19 июня 2011

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

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

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

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

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

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

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

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

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

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

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

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

...