代码契约、继承和里氏原则

发布于 2024-12-07 10:59:58 字数 2179 浏览 2 评论 0原文

我的代码中有命令的概念:

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);
    }
}

现在,我正在尝试使用代码契约,因为我非常确信它对于降低错误风险的有用性。如果我像这样重写该方法:

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”,因此无法添加 Requires。

我知道发出此警告是因为我违反了里氏原则。

然而,就我而言,不同具体类别的条件有所不同。我的 BaseCommand 类实际上定义了一些常见属性,例如 CommandIdentifier、状态以及我在此处删除的其他最终功能,以保持问题简单。

虽然我理解这一原则的概念,但我不知道要正确删除警告需要执行哪些步骤(不要告诉我#pragma warning remove)。

  1. 在这种情况下,具体实现有特定要求,我是否应该停止使用代码契约?
  2. 我是否应该重写我的命令机制,例如将命令“参数”和命令“执行”分开? (每个具体类有一个 CommandeExecutor)。这将导致我的项目中出现更多的类。
  3. 还有其他建议吗?
  4. [编辑]按照adrianm的建议,将属性转换为只读,添加构造函数参数来填充属性并检查构造函数中的属性

I have in my code the concept of command :

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;
    }
}

And some concrete implementation like this one :

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

Now I want to add some validation. The first thing I can do is add a if/throw check:

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);
    }
}

Now, I'm trying to use Code Contracts, because I'm quite convinced of its usefulness to reduce bug risk. If I rewrote the method like this :

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

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

The method compiles, the check is done at run-time. However, I got warning :

warning CC1032: CodeContracts: Method 'MyProject.DeleteItemCommand.Execute' overrides 'MyProject.BaseCommand.Execute', thus cannot add Requires.

I understand this warning is issued because I'm breaking the Liskov Principle.

However, in my case, conditions are different from one concrete class to another. My BaseCommand class is actually defining some common attributes like CommandIdentifier, state, and other ultimate features I removed here to keep the question simple.

While I understand the concepts of this principle, I don't know what are the step I have to do to remove the warning properly (don't tell me about the #pragma warning remove).

  1. Should I stop using code contracts in this case, where concrete implementations have specific requirements ?
  2. Should I rewrite my commanding mechanism to have, for example, separation between the Command "arguments" and the Command "execution" ? (having one CommandeExecutor<TCommand> per concrete class). This would result in a lot more classes in my project.
  3. Any other suggestion ?
  4. [Edit] As suggested by adrianm, convert properties to readonly, add constructor parameters to populate the properties and check properties in the contructor

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

一场信仰旅途 2024-12-14 10:59:58

我认为在这种情况下你不能使用代码契约。我认为,您不能在重写方法中添加前置条件,那里只能添加不变量和后置条件。您最好的选择可能是将继承重构为组合:

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);
    }
}

I don't think you can use code contracts in this case. I think, you cannot add preconditions in overridden methods, only invariants and postconditions are possible there. Your best bet might be be to refactor from inheritance to composition:

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);
    }
}
半城柳色半声笛 2024-12-14 10:59:58

您可以更改代码以使用不同的方法执行:

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);
    }
}

You could change the code to perform the execution in a different method:

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);
    }
}
空城仅有旧梦在 2024-12-14 10:59:58

在这种情况下,代码契约将您的注意力引向类结构中的设计错误,您最好注意此警告。

要了解问题,请考虑如何使用这些类。

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

如果变量 command 碰巧保存了 DeleteItemCommand 类型的对象,则该对象具有必须满足的先决条件,否则将引发异常。我们想避免这种异常,那么如何验证前提条件是否满足呢?

不幸的是,没有简单的方法可以做到这一点。我们无法推断出可能存在于该变量中的每种类型的派生对象的所有可能前提条件。事实上,该变量可能包含编写此代码时尚未发明的对象类型。事实上,如果该对象的类型是由另一个程序集中的工厂提供的,则该类型甚至可能不在该方法的可访问域中。

由于无法验证该对象是否满足先决条件,因此我们无法确保该代码的正确性。由此我们可以得出结论,代码合约毫无用处,或者代码设计不正确。

我知道发出此警告是因为我违反了里氏原则。

那你就承认了!

但是,就我而言,条件与一个具体类不同
到另一个。我的 BaseCommand 类实际上定义了一些常见的
CommandIdentifier、状态和其他终极属性
为了使问题简单起见,我在此处删除了功能。

您的分析本身提出了正确的选择。

您应该创建一个 CommandAttributes 具体密封类,而不是 BaseCommand 抽象类。然后在所有命令对象中包含此类的实例。

通过使用组合而不是继承,每个命令类都可以获得它们所需的功能,并且它们可以定义它们所需的任何类型的前置条件或后置条件。任何使用这些类的方法都可以验证是否满足这些先决条件并利用后置条件。

In this case the code contracts are directing your attention to a design error in your class structure, and you would be wise to heed this warning.

To see the problem, consider how the classes may be used.

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

If the variable command happens to hold an object of the type DeleteItemCommand, then that object has preconditions that must be met or an exception will be thrown. We would like to avoid this exception, so how can we verify that the precondition is met?

Unfortunately, there is no simple way to do this. We cannot reason about all the possible preconditions about every type of derived object that may inhabit that variable. In fact, that variable may contain a type of object that was not invented when this code was written. In fact, the type for that object may not even be in the accessibility domain of this method, if it was provided by a factory in another assembly.

Since there is no way to verify that preconditions are met for this object, we cannot ensure the correctness of this code. We can either conclude from this that code contracts are useless, or that the code is designed incorrectly.

I understand this warning is issued because I'm breaking the Liskov Principle.

So you admit it!

However, in my case, conditions are different from one concrete class
to another. My BaseCommand class is actually defining some common
attributes like CommandIdentifier, state, and other ultimate
features I removed here to keep the question simple.

Your analysis itself suggests the proper alternative.

Instead of a BaseCommand abstract class, you should create a CommandAttributes concrete sealed class. Then include an instance of this class within all of your command objects.

By using composition rather than inheritence, each of your command classes get the functionality they need, and they can define whatever sorts of preconditions or postconditions they need. And any methods that use those classes can verify those preconditions are met and take advantage of the postconditions.

笑梦风尘 2024-12-14 10:59:58

我将使用构造函数来设置所有正确的值,而不是公共属性设置器。

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);
    }
}

I would use the constructor to set all the correct values, instead of public property setters.

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);
    }
}
失与倦" 2024-12-14 10:59:58

显然,代码契约在域对象上效果最好。我猜你的 Command 类不是域对象。

Apparently, Code Contracts work best on Domain Objects. I am guessing your Command class is not a domain object.

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文