메소드 체인을 사용할 때 객체를 재사용하거나 생성합니까?


37

다음과 같은 메소드 체인을 사용할 때 :

var car = new Car().OfBrand(Brand.Ford).OfModel(12345).PaintedIn(Color.Silver).Create();

두 가지 접근 방식이있을 수 있습니다.

  • 다음과 같이 동일한 객체를 재사용하십시오.

    public Car PaintedIn(Color color)
    {
        this.Color = color;
        return this;
    }
  • 다음 Car과 같이 모든 단계에서 새 유형의 객체를 만듭니다 .

    public Car PaintedIn(Color color)
    {
        var car = new Car(this); // Clone the current object.
        car.Color = color; // Assign the values to the clone, not the original object.
        return car;
    }

첫 번째가 잘못 되었거나 개발자가 개인적으로 선택한 것입니까?


나는 그가 첫 번째 접근 방식으로 인해 직관적 / 오해의 소지가있는 코드를 빨리 일으킬 수 있다고 생각합니다. 예:

// Create a car with neither color, nor model.
var mercedes = new Car().OfBrand(Brand.MercedesBenz).PaintedIn(NeutralColor);

// Create several cars based on the neutral car.
var yellowCar = mercedes.PaintedIn(Color.Yellow).Create();
var specificModel = mercedes.OfModel(99).Create();

// Would `specificModel` car be yellow or of neutral color? How would you guess that if
// `yellowCar` were in a separate method called somewhere else in code?

이견있는 사람?


1
무슨 일이야 var car = new Car(Brand.Ford, 12345, Color.Silver);?
James

12
@James 텔레스코픽 생성자, 유창한 패턴은 선택적 매개 변수와 필수 매개 변수를 구분하는 데 도움이 될 수 있습니다 (옵션이 아닌 경우 생성자 인수 인 경우). 그리고 유창한 책은 읽기에 좋습니다.
NimChimpsky

8
@NimChimpsky 좋은 구식 (C #의 경우) 속성과 필 요한 필드가있는 생성자에 영향을 미쳤습니다. Fluent API를 폭파하는 것이 아니라 큰 팬이지만 종종 과도하게 사용됩니다.
Chris S

8
@ChrisS setter에 의존한다면 (나는 자바 출신) 객체를 변경 가능하게 만들어야하며, 원하지 않을 수도 있습니다. 또한 유창하게 사용할 때 더 나은 지능을 얻습니다. 생각이 적을수록 아이디어는 거의 당신을 위해 물건을 구성합니다.
NimChimpsky

1
@NimChimpsky yeh Java의 유창한 발전을 볼 수 있습니다
Chris S

답변:


41

나는 유창한 api 를 자신의 "builder"클래스에 만들고자하는 객체와 분리시켰다. 이렇게하면 클라이언트가 유창한 API를 사용하지 않으려는 경우 여전히 수동으로 사용할 수 있으며 도메인 개체를 오염시키지 않습니다 (단일 책임 원칙 준수). 이 경우 다음이 생성됩니다.

  • Car 이것은 도메인 객체입니다
  • CarBuilder 유창한 API를 보유한

사용법은 다음과 같습니다.

var car = CarBuilder.BuildCar()
    .OfBrand(Brand.Ford)
    .OfModel(12345)
    .PaintedIn(Color.Silver)
    .Build();

CarBuilder클래스는 (내가 여기에 C # 명명 규칙을 사용하고 있습니다)과 같을 것이다 :

public class CarBuilder {

    private Car _car;

    /// Constructor
    public CarBuilder() {
        _car = new Car();
        SetDefaults();
    }

    private void SetDefaults() {
        this.OfBrand(Brand.Ford);
          // you can continue the chaining for 
          // other default values
    }

    /// Starts an instance of the car builder to 
    /// build a new car with default values.
    public static CarBuilder BuildCar() {
        return new CarBuilder();
    }

    /// Sets the brand
    public CarBuilder OfBrand(Brand brand) {
        _car.SetBrand(brand);
        return this;
    }

    // continue with OfModel(...), PaintedIn(...), and so on...
    // that returns "this" to allow method chaining

    /// Returns the built car
    public Car Build() {
        return _car;
    }

}

이 클래스는 스레드로부터 안전하지 않습니다 (각 스레드마다 자체 CarBuilder 인스턴스가 필요함). 또한 유창한 API가 정말 멋진 개념이지만 간단한 도메인 객체를 만드는 데는 너무 많은 노력이 필요합니다.

이 거래는 훨씬 더 추상적 인 API를 만들고 더 복잡한 설정 및 실행을하는 경우 더 유용합니다. 이것이 단위 테스트 및 DI 프레임 워크에서 훌륭하게 작동하는 이유입니다. 지속성, 날짜 처리 및 모의 객체와 함께 Wikipedia Fluent Interface 기사의 Java 섹션에서 다른 예제를 볼 수 있습니다 .


편집하다:

의견에서 언급했듯이; Builder 클래스를 정적 ​​내부 클래스 (Car 내부)로 만들고 Car를 변경할 수 없게 만들 수 있습니다. Car를 불변으로 만드는이 예는 약간 어리석은 것 같습니다. 그러나 더 복잡한 시스템에서는 빌드 된 객체의 내용을 절대 변경하지 않으려는 경우 할 수 있습니다.

아래는 정적 내부 클래스를 수행하는 방법과 그것이 생성하는 불변의 객체 생성을 처리하는 방법의 한 예입니다.

// the class that represents the immutable object
public class ImmutableWriter {

    // immutable variables
    private int _times; private string _write;

    // the "complex" constructor
    public ImmutableWriter(int times, string write) {
        _times = times;
        _write = write;
    }

    public void Perform() {
        for (int i = 0; i < _times; i++) Console.Write(_write + " ");
    }

    // static inner builder of the immutable object
    protected static class ImmutableWriterBuilder {

        // the variables needed to construct the immutable object
        private int _ii = 0; private string _is = String.Empty;

        public void Times(int i) { _ii = i; }

        public void Write(string s) { _is = s; }

        // The stuff is all built here
        public ImmutableWriter Build() {
            return new ImmutableWriter(_ii, _is);
        }

    }

    // factory method to get the builder
    public static ImmutableWriterBuilder GetBuilder() {
        return new ImmutableWriterBuilder();
    }
}

사용법은 다음과 같습니다.

var writer = ImmutableWriter
                .GetBuilder()
                .Write("peanut butter jelly time")
                .Times(2)
                .Build();

writer.Perform();
// console writes: peanut butter jelly time peanut butter jelly time 

편집 2 : 의견에서 Pete 는 복잡한 도메인 객체로 단위 테스트를 작성하는 맥락에서 람다 함수 가있는 빌더를 사용 하는 방법에 대한 블로그 게시물을 만들었습니다 . 빌더를 좀 더 표현력있게 만드는 것은 흥미로운 대안입니다.

의 경우 CarBuilder당신 대신이 방법이 필요합니다 :

public static Car Build(Action<CarBuilder> buildAction = null) {
    var carBuilder = new CarBuilder();
    if (buildAction != null) buildAction(carBuilder);
    return carBuilder._car;
}

이것으로 사용할 수 있습니다 :

Car c = CarBuilder
    .Build(car => 
        car.OfBrand(Brand.Ford)
           .OfModel(12345)
           .PaintedIn(Color.Silver);

3
이 조쉬 블로흐의 효과적인 자바 설명되어 @Baqueta
님 침 스키

6
@Baqueta는 java dev, imho에 대한 읽기가 필요했습니다.
NimChimpsky

3
IMHO의 가장 큰 장점은이 패턴 (적절하게 수정 된 경우)을 사용하여 완료되지 않은 오브젝트의 인스턴스가 빌더를 빠져 나가지 못하게 할 수 있다는 것입니다. 예를 들어 정의되지 않은 색상의 자동차가 없는지 확인할 수 있습니다.
scarfridge

1
흠 ... 나는 항상 빌드 패턴의 최종 메소드 build()(또는 Build())를 호출 했지만 빌드 한 유형의 이름 ( Car()예 :)이 아닙니다 . 또한 Car정말로 불변의 객체 (예를 들어, 모든 필드가 readonly) 인 경우 빌더조차도 객체를 변경할 수 없으므로 Build()메소드가 새 인스턴스를 생성해야합니다. 이를 수행하는 한 가지 방법 Car은 빌더를 인수로 사용하는 단일 생성자를 갖는 것입니다. 그런 다음 Build()방법을 사용할 수 있습니다 return new Car(this);.
Daniel Pryden

1
람다를 기반으로 빌더를 작성하는 다른 접근법에 대해 블로그를 작성했습니다. 게시물에 약간의 편집이 필요합니다. 내 상황은 대부분 단위 테스트 범위 내에서 이루어졌지만 적용 가능한 경우 다른 영역에도 적용될 수 있습니다. 여기에서 찾을 수 있습니다 : petesdotnet.blogspot.com/2012/05/…
Pete

9

조건에 따라서.

당신의 자동차는 엔터티 입니까 아니면 값 객체 입니까? 자동차가 엔터티 인 경우 객체 아이덴티티가 중요하므로 동일한 참조를 반환해야합니다. 객체가 값 객체 인 경우 변경할 수 없어야합니다. 즉, 매번 새 인스턴스를 반환하는 유일한 방법입니다.

후자의 예는 값 객체 인 .NET의 DateTime 클래스입니다.

var date1 = new DateTime(2012,1,1);
var date2 = date1.AddDays(1);
// date2 now refers to Jan 2., while date1 remains unchanged at Jan 1.

그러나 모델이 엔티티라면 빌더 클래스를 사용하여 객체를 빌드하는 것에 대한 Spoike의 답변이 마음에 듭니다. 다시 말해, 그 예는 Car가 가치 객체 인 경우에만 IMHO가 의미가 있습니다.


1
'Entity'대 'Value'질문의 경우 +1입니다. 클래스가 변경 가능 유형인지 변경 불가능한 유형인지 (이 객체를 변경해야합니까?) 질문이며, 디자인에 영향을 주지만 전적으로 귀하에게 달려 있습니다. 메소드가 새 객체를 반환하지 않으면 메소드 체인이 변경 가능한 유형에서 작동한다고 일반적으로 기대하지 않습니다.
Casey Kuball

6

별도의 정적 내부 빌더를 작성하십시오.

필수 매개 변수에 일반 생성자 인수를 사용하십시오. 옵션에 대한 유창한 API.

색상을 설정할 때 NewCarInColour 메서드 또는 이와 유사한 이름을 변경하지 않는 한 새 객체를 만들지 마십시오.

필자는 필요에 따라 브랜드를 지정하고 나머지 옵션은 선택 사항입니다 (이것은 자바이지만 귀하는 자바 스크립트처럼 보이지만 약간의 니트 선택과 상호 교환 가능하다는 것을 확신합니다).

Car yellowMercedes = new Car.Builder(Brand.MercedesBenz).PaintedIn(Color.Yellow).create();

Car specificYellowModel =new Car.Builder(Brand.MercedesBenz).WithModel(99).PaintedIn(Color.Yellow).create();

4

가장 중요한 것은 선택한 결정이 무엇이든 메소드 이름 및 / 또는 주석에 명확하게 명시되어 있다는 것입니다.

표준이 없으며 때로는 메서드가 새 객체를 반환하거나 (대부분의 String 메서드가 그렇게 함) 체인 연결 또는 메모리 효율성을 위해이 객체를 반환합니다.

한 번은 3D Vector 객체를 설계했으며 모든 수학 연산마다 두 가지 방법을 모두 구현했습니다. 즉시 스케일 방법 :

Vector3D scaleLocal(float factor){
    this.x *= factor; 
    this.y *= factor; 
    this.z *= factor; 
    return this;
}

Vector3D scale(float factor){
    Vector3D that = new Vector3D(this); // clone this vector
    return that.scaleLocal(factor);
}

3
+1. 아주 좋은 지적입니다. 왜 이것이 다운 보트를 얻었는지 알 수 없습니다. 그러나 선택한 이름은 명확하지 않습니다. 나는 그것들을 scale(뮤 테이터)와 scaledBy(제너레이터) 라고 부릅니다 .
back2dos

좋은 지적, 이름이 더 명확했을 수 있습니다. 명명은 라이브러리에서 사용한 다른 수학 클래스의 규칙을 따랐습니다. 그 효과는 혼동을 피하기 위해 메소드의 javadoc 주석에도 언급되었습니다.
XGouchet

3

혼란 스러울 것 같은 몇 가지 문제가 있습니다 ... 질문의 첫 번째 줄 :

var car = new Car().OfBrand(Brand.Ford).OfModel(12345).PaintedIn(Color.Silver).Create();

당신은 생성자 (신규)와 create 메소드를 호출하고 있습니다 ... create () 메소드는 거의 항상 정적 메소드 또는 빌더 메소드 일 것입니다. 컴파일러는 경고 또는 오류로 잡아야합니다. 이 구문이 잘못되었거나 끔찍한 이름이 있습니다. 그러나 나중에 두 가지를 모두 사용하지 않으므로 살펴 보겠습니다.

// Create a car with neither color, nor model.
var mercedes = new Car().OfBrand(Brand.MercedesBenz).PaintedIn(NeutralColor);

// Create several cars based on the neutral car.
var yellowCar = mercedes.PaintedIn(Color.Yellow).Create();
var specificModel = mercedes.OfModel(99).Create();

다시 새로운 생성자가 아닌 create를 사용하십시오. 문제는, 당신이 대신 copy () 메소드를 찾고 있다고 생각합니다. 그래서 그것이 사실이고 이름이 좋지 않은 경우, 한 가지만 살펴 봅시다 ... 메르세데스를 호출합니다 .Paintedin (Color.Yellow) .Copy () '복사되기 전에-나에게 정상적인 논리 흐름. 따라서 사본을 먼저 넣으십시오.

var yellowCar = mercedes.Copy().PaintedIn(Color.Yellow)

저에게 당신이 사본을 그리고 노란 차를 만들고 있다는 것을 쉽게 알 수 있습니다.


new와 Create () 사이의 불협화음을 지적하기 위해 +1;
Joshua Drake

1

첫 번째 접근 방식에는 언급 한 단점이 있지만 문서에서 명확하게 밝히면 반 유능한 코더에는 문제가 없어야합니다. 내가 개인적으로 사용한 모든 메소드 체인 코드는 이런 식으로 작동했습니다.

두 번째 접근법은 분명히 더 많은 작업이 필요하다는 단점이 있습니다. 또한 반환하는 사본이 얕거나 딥 카피를 수행할지 여부를 결정해야합니다.이 방법은 클래스마다 다르거 나 방법마다 다르므로 불일치가 발생하거나 최상의 동작에 영향을 줄 수 있습니다. 이것이 문자열과 같은 불변 개체에 대한 유일한 옵션이라는 점은 주목할 가치가 있습니다.

당신이 무엇을 하든지, 같은 수업 내에서 섞거나 어울리지 마십시오!


1

차라리 "확장 방법"메커니즘처럼 생각하고 싶습니다.

public Car PaintedIn(this Car car, Color color)
{
    car.Color = color;
    return car;
}

0

이것은 위의 방법에 대한 변형입니다. 차이점은 Car 클래스에 Builder의 메소드 이름과 일치하는 정적 메소드가 있으므로 명시 적으로 Builder를 작성할 필요가 없다는 것입니다.

Car car = Car.builder().ofBrand(Brand.Ford).ofColor("Green")...

체인 빌더 호출에서 사용하는 것과 동일한 메소드 이름을 사용할 수 있습니다.

Car car = Car.ofBrand(Brand.Ford).ofColor("Green")...

또한 클래스에는 현재 인스턴스의 모든 값으로 채워진 빌더를 리턴하는 .copy () 메소드가 있으므로 테마에 변형을 작성할 수 있습니다.

Car red = car.copy().paintedIn("Red").build();

마지막으로, 빌더의 .build () 메소드는 모든 필수 값이 제공되었는지 확인하고 누락 된 값이 있으면 발생합니다. 빌더의 생성자에 일부 값을 요구하고 나머지는 선택적이되도록하는 것이 바람직 할 수 있습니다. 이 경우 다른 답변의 패턴 중 하나를 원할 것입니다.

public enum Brand {
    Ford, Chrysler, GM, Honda, Toyota, Mercedes, BMW, Lexis, Tesla;
}

public class Car {
    private final Brand brand;
    private final int model;
    private final String color;

    public Car(Brand brand, int model, String color) {
        this.brand = brand;
        this.model = model;
        this.color = color;
    }

    public Brand getBrand() {
        return brand;
    }

    public int getModel() {
        return model;
    }

    public String getColor() {
        return color;
    }

    @Override public String toString() {
        return brand + " " + model + " " + color;
    }

    public Builder copy() {
        Builder builder = new Builder();
        builder.brand = brand;
        builder.model = model;
        builder.color = color;
        return builder;
    }

    public static Builder ofBrand(Brand brand) {
        Builder builder = new Builder();
        builder.brand = brand;
        return builder;
    }

    public static Builder ofModel(int model) {
        Builder builder = new Builder();
        builder.model = model;
        return builder;
    }

    public static Builder paintedIn(String color) {
        Builder builder = new Builder();
        builder.color = color;
        return builder;
    }

    public static class Builder {
        private Brand brand = null;
        private Integer model = null;
        private String color = null;

        public Builder ofBrand(Brand brand) {
            this.brand = brand;
            return this;
        }

        public Builder ofModel(int model) {
            this.model = model;
            return this;
        }

        public Builder paintedIn(String color) {
            this.color = color;
            return this;
        }

        public Car build() {
            if (brand == null) throw new IllegalArgumentException("no brand");
            if (model == null) throw new IllegalArgumentException("no model");
            if (color == null) throw new IllegalArgumentException("no color");
            return new Car(brand, model, color);
        }
    }
}
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.