Нарушает ли это принцип подстановки Лискова, и если да, то что с этим делать?

Пример использования: я использую шаблоны данных для сопоставления представления с моделью представления. Шаблоны данных работают, проверяя наиболее производный тип предоставленного конкретного типа, и они не смотрят, какие интерфейсы он предоставляет, поэтому мне приходится делать это без интерфейсов.

Я упрощаю пример здесь и опускаю NotifyPropertyChanged и т. д., но в реальном мире представление будет привязано к свойству Text. Для простоты представьте, что представление с TextBlock будет привязано к ReadOnlyText, а представление с TextBox будет связано с WritableText.

class ReadOnlyText
{
    private string text = string.Empty;

    public string Text
    {
        get { return text; }
        set
        {
            OnTextSet(value);
        }
    }

    protected virtual void OnTextSet(string value)
    {
        throw new InvalidOperationException("Text is readonly.");
    }

    protected void SetText(string value)
    {
        text = value;
        // in reality we'd NotifyPropertyChanged in here
    }
}

class WritableText : ReadOnlyText
{
    protected override void OnTextSet(string value)
    {
        // call out to business logic here, validation, etc.
        SetText(value);
    }
}

Переопределяя OnTextSet и изменяя его поведение, не нарушаю ли я LSP? Если да, то как лучше это сделать?


person Scott Whitlock    schedule 22.10.2010    source источник
comment
en.wikipedia.org/wiki/Liskov_substitution_principle (для тех, кто еще не пил кофе)   -  person Christopher Klein    schedule 22.10.2010
comment
@SomeMiscGuy: извините, добавил ссылку :)   -  person Scott Whitlock    schedule 22.10.2010
comment
Кстати, можно разрешить шаблон данных на основе класса, реализующего интерфейс, с помощью DataTemplateSelector. Это сработало для меня хорошо: complexdatatemplates.codeplex.com   -  person Dan Bryant    schedule 22.10.2010


Ответы (4)


LSP утверждает, что подкласс должен быть заменяемым для своего суперкласса (см. вопрос о stackoverflow здесь ). Вопрос, который следует задать себе: "Является ли записываемый текст типом текста только для чтения?" Ответ однозначно «нет», на самом деле они взаимоисключающие. Итак, да, этот код нарушает LSP. Однако является ли текст с возможностью записи типом читаемого текста (а не текста только для чтения)? Ответ "да". Поэтому я думаю, что ответ заключается в том, чтобы посмотреть, что вы пытаетесь сделать в каждом случае, и, возможно, немного изменить абстракцию следующим образом:

class ReadableText
{
    private string text = string.Empty;
    public ReadableText(string value)
    {
        text = value;
    }

    public string Text
    {
        get { return text; }
    }
}          

class WriteableText : ReadableText
{
    public WriteableText(string value):base(value)
    {

    }

    public new string Text
    {
        set
        {
            OnTextSet(value);
        }
        get
        {
            return base.Text;
        }
    }
    public void SetText(string value)
    {
        Text = value;
        // in reality we'd NotifyPropertyChanged in here       
    }
    public void OnTextSet(string value)
    {
        // call out to business logic here, validation, etc.       
        SetText(value);
    }
}     

Просто для ясности: мы скрываем свойство Text из класса Readable, используя ключевое слово new в свойстве Text в классе Writeable.
Из http://msdn.microsoft.com/en-us/library/ms173152(VS.80).aspx: когда используется новое ключевое слово, вместо замененных членов базового класса вызываются новые члены класса. Эти члены базового класса называются скрытыми членами. Скрытые члены класса по-прежнему могут вызываться, если экземпляр производного класса приводится к экземпляру базового класса.

person Brandon    schedule 22.10.2010
comment
Это не совсем компилируется, но может быть изменено для работы. Я понятия не имел, что вы можете определить метод установки свойства в производном классе для свойства с методом получения в базовом классе. Я узнал кое-что новое. Это сработает. Спасибо! - person Scott Whitlock; 22.10.2010
comment
Интересно, что мне нужно объявить свойство Text в производном классе как новое, чтобы избежать предупреждения компилятора, но, на мой взгляд, это не совсем новое, потому что я не переопределяю геттер. - person Scott Whitlock; 22.10.2010
comment
Я не верю, что вы хотите, чтобы SetText и OnTextSet были общедоступными. - person Steve Ellinger; 22.10.2010
comment
Я отредактировал сообщение, чтобы включить новое ключевое слово. C# говорит нам, что мы скрываем свойство Text в классе Readable, потому что это свойство доступно только для чтения (геттер), а в классе Writeable свойство доступно для чтения (геттер) и записи (сеттер). - person Brandon; 22.10.2010
comment
@Steve Ellinger: Правда, в моей реализации базовый класс обрабатывает обновление свойства и уведомляет представление, когда бизнес-логика меняет значение. - person Scott Whitlock; 22.10.2010

Только если спецификация ReadOnlyText.OnTextSet() обещает кинуть.

Представьте такой код

public void F(ReadOnlyText t, string value)
{
    t.OnTextSet(value);
}

Имеет ли это смысл для вас, если это не бросило? Если нет, то WritableText нельзя заменить.

Мне кажется, что WritableText должен наследоваться от Text. Если между ReadOnlyText и WritableText есть какой-то общий код, поместите его в Text или в другой класс, от которого они оба наследуются (который наследуется от Text)

person Lou Franco    schedule 22.10.2010
comment
Вы правы, основываясь на моем конкретном вопросе, но Брэндон указал на мое неправильное представление о установщиках и получателях свойств, которое позволило мне решить проблему более элегантно. Спасибо за информацию. - person Scott Whitlock; 22.10.2010

Я бы сказал, зависит от контракта.

Если в контракте для ReadOnlyText сказано, что «любая попытка установить Text вызовет исключение», вы, безусловно, нарушаете LSP.

Если нет, у вас все еще есть неуклюжесть в вашем коде: сеттер для текста только для чтения.

Это приемлемая «денормализация» при данных обстоятельствах. Я еще не нашел лучшего способа, который не требует большого количества кода. Чистый интерфейс будет в большинстве случаев:

IThingieReader
{
    string Text { get; }
    string Subtext { get; }
    // ...
}

IThingieWriter
{
    string Text { get; set; }
    string Subtext { get; set; }
    // ...
}

... и реализация интерфейсов только при необходимости. Однако это не работает, если вам приходится иметь дело со случаями, когда, например. Text доступен для записи, а Subtext - нет, и это сложно сделать для многих объектов/свойств.

person peterchen    schedule 22.10.2010
comment
Как я уже сказал, это было бы идеально, но я не могу использовать интерфейсы, потому что шаблоны данных не отключают интерфейс, им нужен конкретный тип. - person Scott Whitlock; 22.10.2010

Да, это не так, если бы защищенное переопределение void OnTextSet(string value) также вызывало исключение типа «InvalidOperationException» или унаследовано от него.

У вас должен быть базовый класс Text и оба ReadOnlyText и WritableText, наследуемые от него.

person Thanos Papathanasiou    schedule 22.10.2010