我最近提出了一个有趣的问题,流利的方法应该返回什么?他们应该改变当前对象的状态还是使用新状态创建一个全新的对象?

如果此简短描述不是很直观,这是一个(不幸的)冗长的例子。它是一个计算器。它执行非常重的计算,这就是为什么他通过异步回调返回结果:

public interface ICalculator {
    // because calcualations are too lengthy and run in separate thread
    // these methods do not return values directly, but do a callback
    // defined in IFluentParams
    void Add(); 
    void Mult();
    // ... and so on
}

因此,这是一个流利的接口,可以设置参数和回调:

public interface IFluentParams {
    IFluentParams WithA(int a);
    IFluentParams WithB(int b);
    IFluentParams WithReturnMethod(Action<int> callback);
    ICalculator GetCalculator();
}

我有两个有趣的接口实现选项。我将向他们展示两者,然后我写下我每个人都发现的东西。

因此,首先是一个通常的返回 这个:

public class FluentThisCalc : IFluentParams {
    private int? _a;
    private int? _b;
    private Action<int> _callback;

    public IFluentParams WithA(int a) {
        _a = a;
        return this;
    }

    public IFluentParams WithB(int b) {
        _b = b;
        return this;
    }

    public IFluentParams WithReturnMethod(Action<int> callback) {
        _callback = callback;
        return this;
    }

    public ICalculator GetCalculator() {
        Validate();
        return new Calculator(_a, _b);
    }

    private void Validate() {
        if (!_a.HasValue)
            throw new ArgumentException("a");
        if (!_b.HasValue)
            throw new ArgumentException("bs");
    }
}

第二版更复杂,它返回 新对象 关于状态的每个变化:

public class FluentNewCalc : IFluentParams {
    // internal structure with all data
    private struct Data {
        public int? A;
        public int? B;
        public Action<int> Callback;

        // good - data logic stays with data
        public void Validate() {
            if (!A.HasValue)
                throw new ArgumentException("a");
            if (!B.HasValue)
                throw new ArgumentException("b");
        }
    }

    private Data _data;

    public FluentNewCalc() {
    }

    // used only internally
    private FluentNewCalc(Data data) {
        _data = data;
    }

    public IFluentParams WithA(int a) {
        _data.A = a;
        return new FluentNewCalc(_data);
    }

    public IFluentParams WithB(int b) {
        _data.B = b;
        return new FluentNewCalc(_data);
    }

    public IFluentParams WithReturnMethod(Action<int> callback) {
        _data.Callback = callback;
        return new FluentNewCalc(_data);
    }

    public ICalculator GetCalculator() {
        Validate();
        return new Calculator(_data.A, _data.B);
    }

    private void Validate() {
        _data.Validate();
    }
}

他们如何比较:

pro first(这个) 版本:

  • 更轻松

  • 常用

  • 似乎更有效率

  • 还有什么?

pro第二(新的) 版本:

  • 将数据存储在单独的容器中,允许分开数据逻辑和所有处理

  • 允许我们 容易地 修复部分数据,然后填写其他数据并单独处理。看一看:

        var data = new FluentNewCalc()
            .WithA(1);
    
        Parallel.ForEach(new[] {1, 2, 3, 4, 5, 6, 7, 8}, b => {
            var dt = data
                .WithB(b)
                .WithReturnMethod(res => {/* some tricky actions */});
    
            // now, I have another data object for each value of b, 
            // and they have different callbacks.
            // if I were to do it with first version, I would have to create each 
            // and every data object from scratch
            var calc = dt.GetCalculator();
            calc.Add();
        });
    

在第二版中有什么更好的?

  • 我可以用这样的XXX方法实现:

    public IFluentParams WithXXX(int xxx) {
        var data = _data;
        data.XXX = xxx;
        return new FluentNewCalc(data);
    }
    

    并制作_data Readonly(即不可变),一些聪明人说的很好。

所以问题是,您认为哪种方式更好,为什么? PS我使用了C#,但很可能适用于Java。

有帮助吗?

解决方案

当我试图在应用程序设计中回答这样的问题时,我总是考虑一个人在申请中使用我的代码的人会期望什么。

为了确定C# DateTime 类型。它是一个结构,因此不可变。当你要求

var today = DateTime.Now;
var tomorrow = today.AddDays(1);

如果您不知道,您会期待什么 DateTime 不变吗?我不希望今天突然是明天,那会是混乱。

至于您的示例,我想,除非我另外决定,否则仅使用一个计算器实例来处理数字。这是有道理的,对吧?当我编写方程式时,我不会在新行上写每个表达式。我将它们一起写在一起,然后跳到下一行,以分开关注。

所以

var calc = new Calculator(1);
calc.Add(1);
calc.PrintCurrentValue(); // imaginary method for printing of a current value of equation

对我来说很有意义。

其他提示

我倾向于假设流利的方法会返回。但是,您提出了一个很好的观点,即在测试时会吸引我的突变性。有点使用您的示例,我可以做类似的事情:

var calc = new Calculator(0);
var newCalc = calc.Add(1).Add(2).Mult(3);
var result = calc.Add(1);

阅读代码时,我认为许多人会认为结果是 1 正如他们看到的calc + 1。 Add(1).Add(2).Mult(3) 将被应用。

不可变的流利系统难以实施,需要更复杂的代码。对于不可超过的利益是否超过实施它们所需的工作,这似乎是一件高度主观的事情。

如果不是用于类型推理,那么不仅可以实施不变的,可以“两全其美” FluentThing 在API中定义的类,但另一个可变的, FluentThingInternalUseOnly 支持扩大转换为 FluentThing. 。流利的成员 FluentThing 将构建一个新实例 FluentThingInternalUseOnly 并将后一种类型作为其返回类型;成员 FluentThingInternalUseOnly 将继续操作并返回, this.

如果有人说 FluentThing newThing = oldFluentThing.WithThis(4).WithThat(3).WithOther(57);, , 这 WithThis 方法将构建一个新的 FluentThingInternalUseOnly. 。同一实例将被修改并返回 WithThatWithOther;然后将其复制到新的数据 FluentThing 其参考将存储在 newThing.

这种方法的主要问题是,如果有人说 dim newThing = oldFluentThing.WithThis(3);, , 然后 newThing 不会提及不变的 FluentThing, ,但可变 FluentThingInternalUseOnly, ,而且那件事将无法知道对它的参考已被持续存在。

从概念上讲,需要的是一种 FluentThingInternalUseOnly 足够公开,可以将其用作公共功能的返回类型,但不能公开以允许宣布其类型变量的外部代码。不幸的是,我不知道有任何方法,尽管也许有一些技巧 Obsolete() 标签可能是可能的。

否则,如果所采用的对象很复杂,但是操作很简单,那么最好的操作是让流利的接口方法返回一个对象,该对象与该对象有关该对象,并与该对象以及有关什么信息以及有关什么应该对该对象进行[流利的方法将有效地构建一个链接的列表],并懒惰地评估对所有适当更改的对象的引用。如果有人打电话 newThing = myThing.WithBar(3).WithBoz(9).WithBam(42), ,将在每个步骤的每个步骤中创建一个新的包装器对象,也是第一次尝试使用 newThing 作为一个事情,必须构建一个 Thing 实例对其进行了三个更改,但原始 myThing 不会被触摸,只有一个新实例才有必要 Thing 而不是三个。

我想这完全取决于您的用户酶。

在大多数情况下,当我使用构建器时,单线程来操纵可变数据。因此,返回这是首选的,因为没有额外的开销和记忆来返回各地的新实例。

但是,我的许多建筑商都有 copy() 当我需要支持您的“ Pro Second”用例时,返回具有当前值的新实例的方法

许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top