문제

나는 최근에 흥미로운 질문에 이르렀습니다. Fluent 메소드는 무엇을 반환해야 합니까?현재 객체의 상태를 변경해야 할까요, 아니면 새로운 상태로 새로운 객체를 만들어야 할까요?

이 짧은 설명이 매우 직관적이지 않은 경우에는 (불행히도) 긴 예가 있습니다.계산기입니다.매우 무거운 계산을 수행하므로 비동기 콜백을 통해 결과를 반환합니다.

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

어떻게 비교합니까?

프로 우선(이것) 버전:

  • 더 쉽고 짧게

  • 일반적으로 사용되는

  • 메모리 효율이 더 좋은 것 같습니다.

  • 또 뭐야?

프로세컨드(새로운) 버전:

  • 데이터를 별도의 컨테이너에 저장하고 데이터 로직과 모든 처리를 분리할 수 있습니다.

  • 우리가 할 수 있습니다 용이하게 데이터의 일부를 수정한 다음 다른 데이터를 채워서 별도로 처리합니다.구경하다:

        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를 읽기 전용으로 만듭니다(예:불변) 일부 똑똑한 사람들이 좋다고 말하는 것입니다.

따라서 질문은 어떤 방식이 더 좋다고 생각하며 그 이유는 무엇입니까?추신나는 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.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 세 개보다는.

나는 그것이 모두 당신의 사용 사례에 달려 있다고 생각합니다.

빌더를 사용하는 대부분의 경우 단일 스레드에서 변경 가능한 데이터를 조작하는 것입니다.따라서 모든 곳에서 새 인스턴스를 반환하는 데 드는 추가 오버헤드와 메모리가 없기 때문에 이를 반환하는 것이 좋습니다.

하지만 내 빌더 중 다수는 copy() "Pro second" 사용 사례를 지원해야 할 때 현재 동일한 값을 사용하여 새 인스턴스를 반환하는 메서드

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top