Подтвердить что ты не робот

Свободные API - вернуть это или новое?

Недавно я подошел к интересному вопросу, какими должны быть быстрые методы? Должны ли они изменить состояние текущего объекта или создать новый с новым состоянием?

В случае, если это краткое описание не очень интуитивно понятно здесь (неудовлетворительно) длинный пример. Это калькулятор. Он выполняет очень тяжелые вычисления и поэтому возвращает результаты через асинхронный обратный вызов:

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 первая (эта) версия:

  • проще и короче

  • обычно используется

  • кажется более энергоемким

  • что еще?

Вторая (новая) версия:

  • хранит данные в отдельном контейнере, позволяет разделить логику данных и всю обработку

  • позволяет легко фиксировать часть данных, а затем заполнять другие данные и обрабатывать их отдельно. Посмотрите:

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

Что может быть лучше во второй версии?

  • Я мог бы реализовать метод WiспасибоXX следующим образом:

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

    и сделайте _data readonly (т.е. неизменяемый), который говорят некоторые умные люди.

Итак, вопрос в том, как вы считаете, что лучше и почему? Постскриптум Я использовал С#, но он вполне может применяться к java.

4b9b3361

Ответ 1

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

Возьмите для типа С# 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

имеет для меня смысл.

Ответ 2

Я склонен предположить, что быстрые методы вернут это. Тем не менее, вы поднимаете хороший момент в отношении изменчивости, которая застала меня во время тестирования. Пример использования вашего примера, я мог бы сделать что-то вроде:

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).

Непрерывные плавные системы сложнее реализовать, требуя более сложного кода. Представляется весьма субъективным в отношении того, превосходит ли пособие по неизменности работу, необходимую для их реализации.

Ответ 3

Если бы это не было для вывода типа, можно было бы "получить лучшее из обоих миров", реализовав не просто неизменяемый класс FluentThing, определенный в API, а другой, изменяемый, FluentThingInternalUseOnly, который поддерживал расширение преобразования в FluentThing. Элементы Fluent на FluentThing будут создавать новый экземпляр FluentThingInternalUseOnly и иметь этот последний тип в качестве возвращаемого типа; члены FluentThingInternalUseOnly будут работать и возвращаться, this.

Это, если сказать FluentThing newThing = oldFluentThing.WithThis(4).WithThat(3).WithOther(57);, метод WithThis построил новый FluentThingInternalUseOnly. Тот же экземпляр будет изменен и возвращен WithThat и WithOther; данные из него затем будут скопированы в новый FluentThing, ссылка на который будет сохранена в newThing.

Основная проблема с этим подходом заключается в том, что если кто-то говорит dim newThing = oldFluentThing.WithThis(3);, то newThing не будет ссылаться на неизменяемый FluentThing, но изменяет FluentThingInternalUseOnly, и эта вещь не будет иметь никакого способа зная, что ссылка на него была сохранена.

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

В противном случае, если задействованные объекты сложны, но операции просты, лучшее, что может быть сделано, - это позволить свободным методам интерфейса возвращать объект, который содержит ссылку на объект, на который он был вызван вместе с информация о том, что должно быть сделано с этим объектом [цепочка беглых методов будет эффективно создавать связанный список] и лениво оцененную ссылку на объект, на который были применены все соответствующие изменения. Если один из них называется newThing = myThing.WithBar(3).WithBoz(9).WithBam(42), на каждом этапе пути будет создан новый объект-оболочка, и первая попытка использовать newThing как вещь должна была бы создать экземпляр Thing с тремя изменениями, примененными к нему, но оригинал myThing был бы нетронутым, и нужно было бы сделать только один новый экземпляр Thing, а не три.

Ответ 4

Я думаю, все будет зависеть от вашего usecase.

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

Тем не менее, многие из моих сборщиков имеют метод copy(), который возвращает новый экземпляр с текущими одинаковыми значениями в те моменты, когда мне нужно поддерживать ваши "Pro second" варианты использования