当课堂上的提问顺序很重要时,这是最佳实践

本文关键字:最佳 课堂 顺序 | 更新日期: 2023-09-27 18:12:06

我有一个类,它有两个重要的函数:

public class Foo {
    //plenty of properties here
    void DoSomeThing(){/*code to calculate results*/}
    void SaveSomething(){/* code to save the results in DB*/}
}

SaveSomething()使用DoSomeThing()的计算结果。

问题是我们不能在DoSomeThing()之前调用SaveSomething(),否则如果发生这种情况,结果不是真实的结果。我的意思是调用的顺序很重要,这是维护代码的一个问题。(当新成员加入团队时)

有办法处理这个吗?

我认为有以下三种方法

  1. DoSomeThing()之前调用SaveSomething()抛出异常
  2. 具有boolDoSomeThing()SaveSomething()代码更改为:

    bool resultsAreCalculated = false;
    void SaveSomething(){
        if (!resultsAreCalculated) {
            DoSomeThing();
            // the resultsAreCalculated = true; is set in DoSomeThing();
            // can we throw some exception?
        }
        /* code to save the results in DB*/
    }
    
  3. 实现它流畅像:

    Foo x = new Foo();
    x.DoSomeThing().SaveSomething();
    

    在这种情况下,重要的是要保证这不会发生:

    x.SaveSomething().DoSomeThing();
    

现在,我使用第二个方法。有更好的办法吗?

当课堂上的提问顺序很重要时,这是最佳实践

理想情况下,需要遵循一定的执行顺序的方法表示或暗示需要实现某种工作流程。

有一些设计模式支持执行类似工作流的线性执行顺序,例如模板方法模式或策略。

要采用模板方法方法,您的Foo类将有一个抽象基,定义执行Do()Save()的顺序,类似于:

public abstract class FooBase
{
    protected abstract void DoSomeThing(); 
    protected abstract void SaveSomething();
    public void DoAndSave()
    {
        //Enforce Execution order
        DoSomeThing();
        SaveSomething();
    }
}
public class Foo : FooBase
{
    protected override void DoSomeThing()
    {
        /*code to calculate results*/
    }
    protected override void SaveSomething()
    {
        /* code to save the results in DB*/
    }
}

这样你的类消费者将只能访问DoAndSave(),他们不会违反你想要的执行顺序。

还有另一种模式处理工作流/状态转换类型的情况。你可以参考命令链和状态模式。

回应你的评论:这遵循了相同的模板思想,您在模板中添加了另一个步骤,假设您想在保存之前验证结果,您可以将模板扩展为:

public abstract class FooBase
{
    protected abstract void DoSomeThing();
    protected abstract void SaveSomething();
    protected abstract bool AreValidResults();
    public void DoAndSave()
    {
        //Enforce Execution order
        DoSomeThing();
        if (AreValidResults())
            SaveSomething();
    }
}

当然,对于更复杂的工作流,我在原始答案的末尾提到了状态模式,您可以对从一个状态到另一个状态的转换条件进行更细粒度的控制。

帮助避免用户错误的一个选项是通过传递一个变量来明确它。通过这样做,它为用户提出一个标志,他们需要在调用savessomething(…)之前获得结果(即DoSomething())。

results = DoSomething(); // returns the results to be saved
SaveSomething(results);

这个怎么样?

interface Result {
     void Save();
     SomeData GetData();
}
class Foo {
     Result DoSomething() { /* ... */ }
}

用法:

myFoo.DoSomething().Save();
//or something like:
var result = myFoo.DoSomething();
if (result.GetData().Importance > threshold) result.Save();

从外部的角度来看,这很有意义。生成Result并提供保存方法(如果需要),而实现则完全不透明。我不必担心将其传递回正确的Foo实例。事实上,我可以将结果传递给对象,而对象甚至不知道创建它的Foo实例(实际上,创建者应该在创建时传递所有必要的信息以保存结果)。结果可能有一个方法告诉我,它是否已经保存,如果需要的话。等等......

这基本上只是SRP的应用,尽管主要是在接口上而不是在实现上。Foo的接口提供了产生结果的方法,Result抽象了操作结果的方法。

根据Levinaris的答案展开(如果我有rep,则加1),您可以在DoSomthing()方法返回的结果对象上使用Save()方法。所以你会得到这样的内容:

var obj = new Foo();
// Get results
var results = obj.DoSomething();
// Check validity, and user acceptance
if(this.AreValidResults(results) && this.UserAcceptsResults(results))
{
    // Save the results
    results.Save();
}
else
{
    // Ditch the results
    results.Dispose();
}

显然,这种方法要求返回的results对象要么是处理保存/处置结果的泛型类型,但也包含泛型结果;或者它需要是特定结果类型可以继承的某种形式的基类。

我喜欢Anas Karkoukli的答案,但另一种选择是状态机。

public class Foo {
    private enum State {
        AwaitingDo,
        AwaitingValidate,
        AwaitingSave,
        Saved
    }
    private State mState = State.AwaitingDo;
    private void Do() {
        // Do something
        mState = State.AwaitingValidate;
    }
    private void Validate() {
        // Do something
        mState = State.AwaitingSave;
    }
    private void Save() {
        // Do something
        mState = State.Saved;
    }
    public void MoveToNextState() {
        switch (mState) {
            case State.AwaitingDo:
                Do();
                break;
            case State.AwaitingValidation:
                Validate();
                break;
            case State.AwaitingSave:
                Save();
                break;
            case State.Saved:
                throw new Exception("Nothing more to do.");
                break;
        }
    }
}

这有点草率,但你明白我的意思。

Anas回答的问题是,所有的函数都是作为一个步骤执行的,这意味着您无法进入对象的中间阶段。状态机迫使开发人员遵循工作流,但是在工作流的每个阶段,他们都可以在进入下一个阶段之前检查对象的属性。

Steve mcconnell的优秀著作Code Complete用了整整一章来讨论这个问题。这是第二版的第14章。

如果语句的顺序很重要,那么对数据执行这种顺序是非常好的实践。所以与其

calculateResults();
saveResults();

(将结果存储在实例变量中)写

Results r = calculateResults();
saveResults(r);

在计算结果之前保存结果是非常困难的。

DoSave方法对我来说似乎不是一个有序的对。您需要对它们排序,只是因为您没有从Do方法返回计算的状态。如果您将Do方法编写为将结果返回给客户端代码的方法,那么您可以重写Save,以便它将结果作为参数接收。

好处:

  1. 你不需要再排序方法,因为Save方法不关心客户端如何获得参数。它只是接收它,就这样。
  2. 你可以更容易地单元测试Do方法,因为方法变得更少耦合。
  3. 如果需要编写复杂的保存逻辑或实现存储库模式,可以将Save方法移动到另一个类。