Могу ли я получить кодовые контракты, чтобы предупредить меня о «незаконных» подтипах? - PullRequest
3 голосов
/ 08 августа 2011

Извините, если этот вопрос кажется слишком длинным. Прежде чем я смогу спросить об этом, мне нужно показать, откуда оно.

Настройка:

Имеется следующий неизменяемый тип Rectangle:

class Rectangle
{
    public Rectangle(double width, double height) { … }

    public double Width  { get { … } }
    public double Height { get { … } }
}

& hellip; кажется вполне законным извлечь из него тип Square:

using System.Diagnostics.Contracts;

class Square : Rectangle
{
    public Square(double sideLength) : base(sideLength, sideLength) { }

    [ContractInvariantMethod]
    void WidthAndHeightAreAlwaysEqual()
    {
        Contract.Invariant(Width == Height);
    }
}

& hellip; потому что производный класс может убедиться, что его собственный инвариант никогда не нарушается.

Но как только я сделаю Rectangle изменяемым:

class Rectangle
{
    public double Width  { get; set; }
    public double Height { get; set; }
    …
}

& hellip; Я больше не должен получать Square из него, потому что Square никогда не должен иметь независимых сеттеров для Width и Height.

Вопрос:

Что я могу сделать с Code Contracts, чтобы он предупреждал меня о нарушении контракта, как только я выведу Square из изменяемого класса Rectangle? Предпочтительно, статический анализ Code Contracts уже дал бы мне предупреждение во время компиляции.

Другими словами, моя цель - кодировать следующие правила с помощью контрактов кода:

  • Width и Height для Rectangle могут быть изменены независимо друг от друга.
  • Width и Height для Square не могут быть изменены независимо друг от друга, и это не имело бы смысла в первую очередь.

& hellip; и делать это таким образом, чтобы Кодовые контракты обращали внимание, когда эти правила «сталкиваются».

То, что я до сих пор считал:

1. Добавление инварианта в Rectangle:

class Rectangle
{
    …
    [ContractInvariantMethod]
    void WidthAndHeightAreIndependentFromOneAnother()
    {
        Contract.Invariant(Width != Height || Width == Height);
    }
}

Проблема с этим подходом состоит в том, что хотя инвариант правильно заявляет: «Ширина и высота не должны быть равными, но они могут быть одинаковыми», это неэффективно, (1) потому что это тавтология, и (2 ) потому что он менее строг, чем инвариант Width == Height в производном классе Square. Возможно, он даже оптимизирован компилятором до того, как Code Contracts его увидит.

2. Добавление постусловий к сеттерам Rectangle:

public double Width
{
    get { … }
    set { Contract.Ensures(Height == Contract.OldValue(Height)); … }
}

public double Height
{
    get { … }
    set { Contract.Ensures(Width == Contract.OldValue(Width)); … }
}

Это запретит производному классу Square просто обновлять Height до Width всякий раз, когда изменяется Width, и наоборот, это не остановит меня per se от получения Square класс от Rectangle. Но это моя цель: заставить Code Contracts предупредить меня, что Square не должен быть производным от изменяемого Rectangle.

Ответы [ 3 ]

4 голосов
/ 08 августа 2011

В недавнем журнале MSDN была очень важная статья, в которой обсуждалась в основном та же проблема: "Кодовые контракты: наследование и принцип Лискова" - я бы не смог придумать лучший ответ.

То, что вы считаете «незаконным» подтипом, в основном является нарушением принципа подстановки Лискова, в статье показано, как контракты кода могут помочь обнаружить эту проблему.

2 голосов
/ 08 августа 2011

В журнале MSDN Magazine за за июль 2011 года есть статья , в которой подробно описывается ваша проблема с тем же примером и обсуждается использование контрактов кода и принципа подстановки Лискова.

0 голосов
/ 08 августа 2011

Спасибо @BrokenGlass и @Mathias за ссылку на недавнюю статью в журнале MSDN, названную Контракты кодов: наследование и принцип Лискова .Хотя я полагаю, что в этом что-то не так (я вернусь к этому через секунду ... см. Вторую часть этого ответа), это помогло мне выбрать решение.

Решение, которое я выбрал:

  1. Я добавляю следующие базовые условия к базовому классу Rectangle:

    public double Width
    {
        set { Contract.Ensures(Height == Contract.OldValue(Height)); }
    }
    
    public double Height
    {
        set { Contract.Ensures(Width == Contract.OldValue(Width)); }
    }
    

    Они в основном утверждают, что Width и Height могут бытьустанавливается независимо друг от друга.

  2. Один инвариант добавляется в производный класс Square:

    Contract.Invariant(Width == Height);
    

    Это в основном утверждает прямо противоположное.

Взятые вместе, эти в конечном итоге приведут к нарушению контракта - по общему признанию, не во время компиляции, но кажется, что это может быть сделано только с Code Contracts в определенных угловых случаях (а именно, когда производный класс начинает добавлять предварительные условия.)

Почему решение в статье MSDN Magazine бесполезно?

В статье MSDN, по-видимому, показано решение с преимуществом компиляции.время предупреждения.Почему я придерживаюсь вышеуказанного решения, у которого нет этого бонуса?

Краткий ответ:

Необъяснимое изменение в примерах кода дает понять, что статьясоздает искусственную ситуацию для демонстрации статического анализа Code Contracts.

Длинный ответ:

Статья начинается с примера кода (→ Рисунок 1)класса Rectangle с индивидуальными установщиками для Width и Height.В следующий раз, когда будет показан класс Rectangle (→ рисунок 2), установщики были сделаны private и используются только с помощью добавленного метода SetSize(width, height).

В статье не говорится объяснять, почемуэто изменение было тихо введено.На самом деле, это изменение, вероятно, не будет иметь никакого смысла вообще в контексте только Rectangle, если, конечно, вы уже не знали, что получите класс, подобный Square, где вам нужно добавить предварительное условие width == height,Вы не можете добавить это как предварительное условие, когда сеттеры для Width и Height отделены друг от друга.И если вы не можете добавить это предварительное условие, вы не получите предупреждение во время компиляции из Code Contracts.

...