Кодекс договора, наследство и принцип Лискова - PullRequest
7 голосов
/ 29 сентября 2011

У меня в коде есть понятие команды:

public abstract class BaseCommand
{
    public BaseCommand() { this.CommandId = Guid.NewGuid(); this.State = CommandState.Ready; }
    public Guid CommandId { get; private set; }
    public CommandState State {get; private set; }
    protected abstract void OnExecute();
    public void Execute() {
         OnExecute();
         State = CommandState.Executed;
    }
}

И некоторые конкретные реализации, подобные этой:

public class DeleteItemCommand
{
    public int ItemId {get; set;}
    protected override void OnExecute()
    {
        var if = AnyKindOfFactory.GetItemRepository();
        if.DeleteItem(ItemId);
    }
}    

Теперь я хочу добавить подтверждение. Первое, что я могу сделать, это добавить проверку if / throw:

public class DeleteItemCommand
{
    public int ItemId {get; set;}
    protected override void Execute()
    {
        if(ItemId == default(int)) throw new VeryBadThingHappendException("ItemId is not set, cannot delete the void");
        var if = AnyKindOfFactory.GetItemRepository();
        if.DeleteItem(ItemId);
    }
}

Теперь я пытаюсь использовать Code Contracts, потому что я вполне убежден в его полезности для снижения риска ошибок. Если я переписал метод так:

public class DeleteItemCommand
{
    public int ItemId {get; set;}
    public void Execute()
    {
        Contract.Requires<VeryBadThingHappendException>(ItemId != default(int));

        var if = AnyKindOfFactory.GetItemRepository();
        if.DeleteItem(ItemId);
    }
}

Метод компилируется, проверка выполняется во время выполнения. Однако я получил предупреждение:

предупреждение CC1032: CodeContracts: метод «MyProject.DeleteItemCommand.Execute» переопределяет «MyProject.BaseCommand.Execute», поэтому не может добавлять «Требуется».

Я понимаю, что это предупреждение выдается, потому что я нарушаю принцип Лискова.

Однако в моем случае условия отличаются от одного конкретного класса к другому. Мой класс BaseCommand на самом деле определяет некоторые общие атрибуты, такие как CommandIdentifier, состояние и другие основные функции, которые я удалил здесь, чтобы упростить вопрос.

Хотя я понимаю концепции этого принципа, я не знаю, какой шаг мне нужно сделать, чтобы правильно удалить предупреждение (не говорите мне о #pragma warning remove).

  1. Должен ли я прекратить использование контрактов кода в этом случае, когда к конкретным реализациям предъявляются особые требования?
  2. Должен ли я переписать свой командный механизм, чтобы, например, было разделение между командой "аргументы" и командой "выполнение"? (имеющий один CommandeExecutor<TCommand> на конкретный класс). Это привело бы к гораздо большему количеству занятий в моем проекте.
  3. Любое другое предложение?
  4. [Редактировать] В соответствии с предложением adrianm , преобразовать свойства только для чтения, добавить параметры конструктора для заполнения свойств и проверить свойства в конструкторе

Ответы [ 5 ]

5 голосов
/ 29 сентября 2011

Я не думаю, что вы можете использовать кодовые контракты в этом случае.Я думаю, вы не можете добавить предусловия в переопределенные методы, там возможны только инварианты и постусловия.Лучшим вариантом может быть рефакторинг от наследования к составу:

ICommandExecutor
{
    Execute(BaseCommand source);
}

public abstract class BaseCommand
{
    public ICommandExecutor Executor { get; private set; }
    public void Execute() 
    {
        this.Executor.Execute(this);
        State = CommandState.Executed;
    }
}

public class DeleteCommandExecutor : ICommandExecutor
{
    public void Execute(BaseCommand source)
    {
        Contract.Requires<VeryBadThingHappendException>(source.ItemId != default(int));
        var if = AnyKindOfFactory.GetItemRepository();
        if.DeleteItem(source.ItemId);
    }
}
2 голосов
/ 30 сентября 2011

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

Чтобы увидеть проблему, подумайте, как можно использовать классы.

protected void executeButton_Click(object sender, EventArgs args) 
{
    BaseCommand command = GetCurrenctlySelectedCommand();
    command.Execute();
}

Если переменная command содержит объект типа DeleteItemCommand, то этот объект имеет предварительные условия, которые должны быть выполнены, или будет выдано исключение. Мы хотели бы избежать этого исключения, так как мы можем проверить, что предварительное условие выполнено?

К сожалению, не существует простого способа сделать это. Мы не можем рассуждать обо всех возможных предварительных условиях для каждого типа производного объекта, который может обитать в этой переменной. Фактически, эта переменная может содержать тип объекта, который не был изобретен при написании этого кода. Фактически, тип этого объекта может даже не находиться в домене доступности этого метода, если он был предоставлен фабрикой в ​​другой сборке.

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

Я понимаю, что это предупреждение выдается, потому что я нарушаю принцип Лискова.

Итак, вы признаете это!

Однако в моем случае условия отличаются от одного конкретного класса другому. Мой класс BaseCommand на самом деле определяет некоторые общие атрибуты, такие как CommandIdentifier, состояние и другие конечные функции, которые я здесь убрал, чтобы упростить вопрос.

Ваш анализ сам предлагает правильную альтернативу.

Вместо BaseCommand абстрактного класса вы должны создать CommandAttributes конкретный запечатанный класс. Затем включите экземпляр этого класса во все объекты вашей команды.

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

2 голосов
/ 29 сентября 2011

Вы можете изменить код для выполнения выполнения другим способом:

public class DeleteItemCommand: BaseCommand
{
    public int ItemId {get; set;}
    public override void Execute()
    {
        PrivateExecute(ItemId);
    }

    private void PrivateExecute(int itemId)
    {
        Contract.Requires<VeryBadThingHappendException>(itemId != default(int));

        var rep = AnyKindOfFactory.GetItemRepository();
        rep.DeleteItem(itemId);
    }
}
1 голос
/ 30 сентября 2011

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

public class DeleteItemCommand
{
    public DeleteItemCommand(int itemId)
    {
        Contract.Requires<VeryBadThingHappendException>(itemId!= default(int));
        ItemId = itemId;
    }

    public int ItemId {get; private set;}
    public void Execute()
    {   
        var if = AnyKindOfFactory.GetItemRepository();
        if.DeleteItem(ItemId);
    }
}
0 голосов
/ 07 ноября 2011

Очевидно, что кодовые контракты лучше всего работают с объектами домена. Я предполагаю, что ваш класс Command не является объектом домена.

...