流畅的API - 返回这个还是新的?

时间:2013-11-07 16:39:33

标签: c# java oop design-patterns fluent-interface

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

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

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( this )版本:

  • 更轻松,更短

  • 常用的

  • 似乎更具记忆效率

  • 还有什么?

Pro second( new )版本:

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

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

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

在第二版中哪些更好?

  • 我可以像这样实现WithXXX方法:

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

    并使_data readonly(即不可变),一些聪明的人说是好的。

所以问题是,您认为哪种方式更好,为什么? 附:我使用了c#,但很适用于java。

4 个答案:

答案 0 :(得分:6)

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

考虑到实例,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

对我来说非常有意义。

答案 1 :(得分:3)

我倾向于假设流利的方法会返回这个。但是,你提出了一个关于可变性的好点,这个问题在测试时引起了我的注意。使用你的例子,我可以做类似的事情:

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

在阅读代码时,我认为很多人会认为结果是1,因为他们会看到calc + 1.由于可变流畅系统的原因,答案会因{{1}而不同将被应用。

不可变的流畅系统虽然难以实现,但需要更复杂的代码。关于不变性利益是否超过实施它们所需的工作似乎是一个非常主观的事情。

答案 2 :(得分:3)

如果不是类型推断,可以通过实现API中定义的不可变FluentThing类来实现“两全其美”,而另一个可变的,FluentThingInternalUseOnly支持扩展转换为FluentThingFluentThing上的Fluent成员将构造一个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()标签的技巧可能。

否则,如果被操作的对象很复杂但操作很简单,那么最好的操作就是让fluent接口方法返回一个对象,该对象保存对其被调用的对象的引用。关于应该对该对象做什么的信息[链接流畅的方法将有效地构建链表]和对已经应用了所有适当更改的对象的延迟评估的引用。如果一个调用newThing = myThing.WithBar(3).WithBoz(9).WithBam(42),则会在每个步骤中创建一个新的包装器对象,并且第一次尝试使用newThing作为一个东西必须构造一个Thing实例,其中包含三个应用了更改,但原始myThing将不受影响,只需要创建Thing的一个新实例而不是三个。

答案 3 :(得分:2)

我想这完全取决于你的用例。

大多数情况下,当我使用Builder时,它是一个单线程来操纵可变数据。因此,返回此选项是首选,因为没有额外的开销和内存在任何地方返回新实例。

但是,我的许多构建器都有一个copy()方法,它返回一个新实例,当我需要支持“Pro第二”用例时,它返回当前相同的值