체인 세터가 왜 전통적인가?


46

Bean에 체인을 구현하면 생성자, 메가 생성자, 팩토리를 오버로드 할 필요가없고 가독성이 향상됩니다. 객체를 불변으로 만들고 싶지 않다면 어떤 단점도 생각할 수 없습니다 .이 경우 어쨌든 setter가 없습니다. 그렇다면 이것이 OOP 규칙이 아닌 이유가 있습니까?

public class DTO {

    private String foo;
    private String bar;

    public String getFoo() {
         return foo;
    }

    public String getBar() {
        return bar;
    }

    public DTO setFoo(String foo) {
        this.foo = foo;
        return this;
    }

    public DTO setBar(String bar) {
        this.bar = bar;
        return this;
    }

}

//...//

DTO dto = new DTO().setFoo("foo").setBar("bar");

32
자바가 세터가 인간에게 가증 한 것이 아닌 유일한 언어 일 수 있기 때문에 ...
— Telastyn

11
반환 값이 의미 적으로 의미가 없기 때문에 잘못된 스타일입니다. 오해의 소지가 있습니다. 유일한 단점은 키 입력을 최소화하는 것입니다.
— usr

58
이 패턴은 전혀 드문 일이 아닙니다. 심지어 이름도 있습니다. 유창한 인터페이스 라고 합니다 .
— Philipp

9
더 읽기 쉬운 결과를 위해 빌더에서이 작성을 추상화 할 수도 있습니다. myCustomDTO = DTOBuilder.defaultDTO().withFoo("foo").withBar("bar").Build();세터가 공허하다는 일반적인 생각과 충돌하지 않기 위해 그렇게 할 것입니다.

9
@Philipp은 기술적으로는 옳지 만 new Foo().setBar('bar').setBaz('baz')매우 유창하다고 느낍니다. 나는 확실히 정확히 같은 방식으로 구현 될 수있는 의미,하지만 난 아주 훨씬 더 같은 것을 읽어 기대Foo().barsThe('bar').withThe('baz').andQuuxes('the quux')
— 웨인 베르너

답변:


50

이것이 왜 OOP 규칙이 아닌 이유가 있습니까?

내 최선의 추측 : CQS를 위반하기 때문에

명령 (객체 상태 변경)과 쿼리 (이 경우 객체 자체를 반환하는 상태의 복사본을 반환)와 동일한 메소드가 혼합되어 있습니다. 반드시 문제는 아니지만 일부 기본 지침을 위반합니다.

예를 들어 C ++에서 std :: stack :: pop ()은 void를 반환하는 명령이고 std :: stack :: top ()은 스택의 최상위 요소에 대한 참조를 반환하는 쿼리입니다. 고전, 두 결합하고 싶지만, 당신은 할 수 없습니다 및 예외 안전합니다. (Java의 할당 연산자가 발생하지 않기 때문에 Java에서는 문제가되지 않습니다).

DTO가 가치 유형 인 경우와 비슷한 결과를 얻을 수 있습니다.

public DTO setFoo(String foo) {
    return new DTO(foo, this.bar);
}

public DTO setBar(String bar) {
    return new DTO(this.foo, bar);
}

또한 체인 반환 값은 상속을 처리 할 때 엄청난 고통입니다. "다채롭게 반복되는 템플릿 패턴"을 참조하십시오.

마지막으로, 기본 생성자가 유효한 상태의 객체를 남겨 두어야하는 문제가 있습니다. 개체를 유효한 상태로 복원하기 위해 많은 명령을 실행 해야하는 경우 무언가 잘못되었습니다.


4
마지막으로 여기에 진짜 함정이 있습니다. 상속은 실제 문제입니다. 체인은 구성에 사용되는 데이터 표현 객체에 대한 기존의 세터가 될 수 있다고 생각합니다.
— Ben

6
"객체에서 상태의 복사본을 반환"-하지 않습니다.
— user253751

2
이 가이드 라인을 따르는 가장 큰 이유는 (반복자와 같은 몇 가지 예외를 제외하고) 코드 가 추론하기가 훨씬 쉽다는 것입니다.
— jpmc26

1
@MatthieuM. 아니. 저는 주로 Java를 사용하며 고급 "유체"API에서 널리 사용됩니다. 다른 언어에도 존재합니다. 본질적 으로 유형을 확장하는 유형에서 유형을 일반 으로 만듭니다 . 그런 다음 abstract T getSelf()일반 유형을 반환 하는 메소드 를 추가하십시오 . 이제 반환하는 대신 this, 당신의 세터에서 당신을 return getSelf()하고있는 재정 클래스는 단순히 자신과 수익률의 일반적인 유형 수 this에서을 getSelf. 이런 식으로 setter는 선언 유형이 아닌 실제 유형을 반환합니다.
— 거미 보리스

1
@MatthieuM. 정말? C ++ 용 CRTP와 비슷한 소리 ...
— 쓸모없는

33
  1. 몇 번의 키 스트로크 저장은 설득력이 없습니다. 좋을지 모르지만 OOP 규칙은 키 입력이 아닌 개념과 구조에 더 관심이 있습니다.

  2. 반환 값은 의미가 없습니다.

  3. 사용자가 반환 값의 의미를 기대할 수 있기 때문에 의미가없는 것보다 반환 값이 잘못 되었습니다. 그들은 그것이 "불변의 세터"라고 기대할 수있다

    public FooHolder {
        public FooHolder withFoo(int foo) {
            /* return a modified COPY of this FooHolder instance */
        }
    }
    

    실제로 세터는 객체를 변경합니다.

  4. 상속에서는 잘 작동하지 않습니다.

    public FooHolder {
        public FooHolder setFoo(int foo) {
            ...
        }
    }
    
    public BarHolder extends FooHolder {
        public FooHolder setBar(int bar) {
            ...
        }
    } 
    

    난 쓸수있다

    new BarHolder().setBar(2).setFoo(1)

    하지만

    new BarHolder().setFoo(1).setBar(2)

저에게는 # 1에서 # 3까지가 중요한 것들입니다. 잘 작성된 코드는 유쾌하게 배열 된 텍스트에 관한 것이 아닙니다. 잘 작성된 코드는 기본 개념, 관계 및 구조에 관한 것입니다. 텍스트는 코드의 진정한 의미를 외적으로 반영한 것입니다.


2
그러나 이러한 주장의 대부분은 유창하고 체인 가능한 인터페이스에 적용됩니다.
— Casey

@Casey, 그렇습니다. 빌더 (즉, 세터 포함)가 체인에서 가장 많이 본 것입니다.
— Paul Draper

10
유창한 인터페이스에 대해 잘 알고 있다면 반환 유형에서 유창한 인터페이스를 의심 할 가능성이 있기 때문에 # 3은 적용되지 않습니다. 또한 "잘 작성된 코드는 유쾌하게 배열 된 텍스트가 아닙니다"라는 의견에 동의하지 않아야합니다. 그것이 전부는 아니지만, 잘 배열 된 텍스트는 프로그램의 독자가 적은 노력으로 텍스트를 이해할 수 있도록하기 때문에 다소 중요합니다.
— Michael Shaw

1
세터 체인은 텍스트를 즐겁게 배열하거나 키 입력을 저장하는 것이 아닙니다. 식별자의 수를 줄이는 것입니다. setter chaining을 사용하면 단일 표현식으로 객체를 생성하고 설정할 수 있습니다. 즉, 변수에 저장할 필요가 없습니다. 즉 이름을 지정해야하는 변수 가 범위의 끝까지 유지됩니다.
— Idan Arye

3
: 점 # 4에 대해서는, 다음과 같이 자바로 해결 될 수있는 일이다 public class Foo<T extends Foo> {...}세터가 복귀와 함께 Foo<T>. 또한 'setter'메소드를 'with'메소드라고 부르는 것이 좋습니다. 과부하가 정상적으로 작동하면 Foo<T> with(Bar b) {...}, 그렇지 않으면 Foo<T> withBar(Bar b).
— YoYo

12

나는 이것이 OOP 규칙이라고 생각하지 않으며, 언어 디자인 및 그 규칙과 관련이 있습니다.

Java를 사용하는 것 같습니다. Java에는 setter의 리턴 유형을 무효화하도록 지정 하는 JavaBeans 스펙이 있습니다. 즉 setter의 체인과 충돌합니다. 이 사양은 다양한 도구로 널리 수용되고 구현됩니다.

물론 사양의 일부가 체인이 아닌 이유를 물을 수 있습니다. 나는 대답을 모른다. 아마도이 패턴은 당시에 알려지지 않았거나 인기가 없었습니다.


예, JavaBeans는 내가 염두에 두었던 것입니다.
— Ben

JavaBeans를 사용하는 대부분의 응용 프로그램은 생각하지 않지만 리플렉션을 사용하여 'set ...'이라는 이름의 메서드를 가져 오고 단일 매개 변수를 가지며 void return 인 메서드를 사용할 수 있습니다. 많은 정적 분석 프로그램은 무언가를 반환하는 메소드에서 반환 값을 확인하지 않는 것에 대해 불평하며 이는 매우 성 가실 수 있습니다.

Java에서 메소드를 가져 오는 @MichaelT 반사 호출은 리턴 유형을 지정하지 않습니다. 그런 식으로 return 메소드를 호출하면 메소드 가 리턴 Object유형 Void으로 지정된 경우 싱글 톤 유형 이 리턴 될 수 있습니다 void. 다른 소식에서, "의견을 남기고 투표하지 않는 것"은 "첫 번째 게시물"감사가 실패하더라도 좋은 의견 일 수 있습니다. 나는 이것을 shog를 비난한다.

7

다른 사람들이 말했듯이, 이것은 종종 유창한 인터페이스 라고합니다 .

일반적으로 setter는 응용 프로그램 의 논리 코드에 대한 응답으로 변수 를 전달하는 호출입니다 . DTO 수업이 이에 대한 예입니다. setter가 아무것도 반환하지 않을 때의 기존 코드는 정상적입니다. 다른 답변은 방법을 설명했습니다.

그러나 유창한 인터페이스가 좋은 솔루션 일 수 있는 몇 가지 경우가 있습니다.

  • 상수는 주로 세터에게 전달됩니다.
  • 프로그램 로직은 세터에 전달되는 내용을 변경하지 않습니다.

fluent-nhibernate 와 같은 구성 설정

Id(x => x.Id);
Map(x => x.Name)
   .Length(16)
   .Not.Nullable();
HasMany(x => x.Staff)
   .Inverse()
   .Cascade.All();
HasManyToMany(x => x.Products)
   .Cascade.All()
   .Table("StoreProduct");

특수 TestDataBulderClasses (객체 어머니)를 사용하여 단위 테스트에서 테스트 데이터 설정

members = MemberBuilder.CreateList(4)
    .TheFirst(1).With(b => b.WithFirstName("Rob"))
    .TheNext(2).With(b => b.WithFirstName("Poya"))
    .TheNext(1).With(b => b.WithFirstName("Matt"))
    .BuildList(); // Note the "build" method sets everything else to
                  // senible default values so a test only need to define 
                  // what it care about, even if for example a member 
                  // MUST have MembershipId  set

그러나 유창한 인터페이스를 만드는 것은 매우 어렵 기 때문에 "정적"설정이 많은 경우에만 유용합니다. 또한 유창한 인터페이스는 "정상"클래스와 혼용해서는 안됩니다. 따라서 빌더 패턴이 자주 사용됩니다.


5

나는 하나의 세터를 다른 체인으로 연결하는 것이 관례가 아닌 많은 이유를 생각합니다.이 경우 생성자에서 옵션 객체 또는 매개 변수를 보는 것이 더 일반적이기 때문입니다. C #에는 초기화 구문도 있습니다.

대신에:

DTO dto = new DTO().setFoo("foo").setBar("bar");

하나는 쓸 수 있습니다 :

(JS에서)

var dto = new DTO({foo: "foo", bar: "bar"});

(C #에서)

DTO dto = new DTO{Foo = "foo", Bar = "bar"};

(자바)

DTO dto = new DTO("foo", "bar");

setFoo그리고 setBar다음 더 이상 초기화 필요하지 않습니다, 나중에 돌연변이에 사용할 수 있습니다.

체인 가능성은 일부 상황에서 유용하지만 줄 바꿈 문자를 줄이기 위해 모든 것을 한 줄로 채우지 않는 것이 중요합니다.

예를 들어

dto.setFoo("foo").setBar("fizz").setFizz("bar").setBuzz("buzz");

무슨 일이 일어나고 있는지 이해하고 이해하기가 어렵습니다. 다음으로 다시 포맷 :

dto.setFoo("foo")
    .setBar("fizz")
    .setFizz("bar")
    .setBuzz("buzz");

이해하기가 훨씬 쉽고 첫 번째 버전의 "실수"를보다 분명하게 만듭니다. 코드를 해당 형식으로 리팩토링하면 다음과 같은 이점이 없습니다.

dto.setFoo("foo");
dto.setBar("bar");
dto.setFizz("fizz");
dto.setBuzz("buzz");

3
1. 가독성 문제에 동의 할 수 없습니다. 각 호출이 더 명확 해지기 전에 인스턴스를 추가하십시오. 2. js와 C #으로 보여준 초기화 패턴은 생성자와 아무 관련이 없습니다 : js에서 한 일은 단일 인수로 전달되며 C #에서 한 것은이면 뒤에 geters와 setter를 호출하는 구문 shugar입니다. C #처럼 geter-setter shugar가 없습니다.
— Ben

1
@Benedictus, OOP 언어가 체인 없이이 문제를 처리하는 다양한 방법을 보여주었습니다. 요점은 동일한 코드를 제공하는 것이 아니라 연쇄를 불필요하게 만드는 대안을 제시하는 것이 었습니다.
— zzzzBov

2
"각 호출 전에 인스턴스를 추가해도 전혀 명확하지는 않습니다."나는 각 호출 전에 인스턴스를 추가하면 더 명확 해 졌다고 주장한 적이 없으며, 상대적으로 동등한 것이라고 말했습니다.
— zzzzBov

1
VB.NET에는 또한 사용 가능한 "With"키워드가있어 컴파일러 임시 참조를 생성하므로 예를 들어 [ /줄 바꿈을 나타내는 데 사용 ] With Foo(1234) / .x = 23 / .y = 47은와 같습니다 Dim temp=Foo(1234) / temp.x = 23 / temp.y = 47. 이후 이러한 구문은 모호성을 생성하지 않는 .x바로 문 "과 함께"주변 [없음, 또는 어떤 멤버가없는 개체가없는 경우에 바인딩하는 것보다 더 의미 다른를 가질 수 그 자체로 x, 다음 .x입니다 의미]. 오라클은 Java와 같은 것을 포함하지 않았지만 그러한 구성은 언어에 매끄럽게 맞습니다.
— supercat

5

이 기술은 실제로 빌더 패턴에서 사용됩니다.

x = ObjectBuilder()
        .foo(5)
        .bar(6);

그러나 일반적으로 모호하기 때문에 피해야합니다. 반환 값이 객체인지 (따라서 다른 설정자를 호출 할 수 있는지) 또는 반환 객체가 방금 할당 된 값인지 (공통 패턴 임) 확실하지 않습니다. 따라서 최소한의 서프라이즈 원칙은 객체 디자인의 기본이 아니라면 사용자가 하나의 솔루션 또는 다른 솔루션을 보길 원한다고 가정해서는 안된다고 제안합니다.


6
차이점은 빌더 패턴을 사용할 때 일반적으로 읽기 전용 (불변) 오브젝트 (빌드하는 클래스에 상관없이)를 구성하는 쓰기 전용 오브젝트 (빌더)를 작성한다는 것입니다. 이와 관련하여, 단일 호출 로 사용될 수 있기 때문에 긴 일련의 메소드 호출을 갖는 것이 바람직하다 .
— Darkhogg

"또는 반환 객체가 방금 할당 된 값이면"방금 전달한 값을 유지하려면 변수에 먼저 넣습니다. 그래도 이전에 할당 된 값을 얻는 것이 유용 할 수 있습니다 (일부 컨테이너 인터페이스가이를 수행한다고 생각합니다).
— JAB February

@JAB 동의합니다. 나는 그 표기법의 팬이 아니지만 그 자리가 있습니다. 마음에 오는 하나는 Obj* x = doSomethingToObjAndReturnIt(new Obj(1, 2, 3)); 나는 그것이 또한 때문에 거울 인기를 얻고 생각 a = b = c = d나는 인기는 잘 설립 확신 아니에요 불구하고. 일부 원자 연산 라이브러리에서 이전 값을 반환하는 것을 언급했습니다. 이야기의 교훈? 내가 소리를내는 것보다 더 혼란 스럽다 =)
— Cort Ammon

나는 학자들이 약간의 비판적이라고 생각합니다. 관용구에는 본질적으로 아무런 문제가 없습니다. 어떤 경우에는 명확성을 추가하는 데 매우 유용합니다. 예를 들어 문자열의 모든 줄을 들여 쓰고 그 주위에 구식 상자를 그리는 다음과 같은 텍스트 서식 클래스를 사용하십시오 new BetterText(string).indent(4).box().print();. 이 경우, 나는 gobbledygook를 우회하고 문자열을 가져 와서 들여 넣고 상자에 넣고 출력했습니다. 캐스케이드 (예 :)와 같은 복제 방법 .copy()을 사용하여 모든 후속 작업에서 더 이상 원본을 수정할 수 없도록 할 수도 있습니다.
— tgm1024

2

이것은 답변보다 더 많은 의견이지만 의견을 말할 수는 없습니다.

나는이 질문이 전혀 드문 것으로 보지 않기 때문에이 질문이 나를 놀라게했다고 언급하고 싶었습니다 . 사실, 제 작업 환경 (웹 개발자)은 매우 일반적입니다.

생성 : 예를 들어,이 심포니의 교리는 어떻게 개체가 자동 생성 명령 을 모두 setters , 기본적으로 .

jQuery를이 좀 매우 유사한 방식으로 체인 메서드의 대부분을.


Js는 혐오입니다
— Ben

@ 베네딕토 스 나는 PHP가 더 큰 가증이라고 말합니다. JavaScript는 완벽하게 훌륭한 언어이며 ES6 기능을 포함하여 꽤 좋았습니다 (일부 사람들은 여전히 ​​CoffeeScript 또는 변형을 선호하지만 개인적으로는 CoffeeScript가 변수 범위를 처리하는 방법에 대한 팬이 아닙니다. 파이썬이하는 방식이 아닌 로컬 / 글로벌로 명시 적으로 표시되지 않는 한 로컬 범위에 할당 된 변수를 로컬로 처리하는 것보다 먼저).
— JAB February

@Benedictus와 동의합니다 JAB. 나는 대학 시절에 JS를 가증스러운 스크립트 언어로 보았지만 몇 년 동안 일하고 올바른 사용법을 배우고 나면 실제로 사랑 합니다. . 그리고 ES6에서는 정말 성숙한 언어 가 될 것이라고 생각합니다 . 그러나 내 머리를 돌리게 만드는 것은 ... 내 대답은 JS와 어떤 관련이 있는가? ^^ U
— xDaizu

나는 실제로 js와 2 년을 일했으며 그것이 방법을 배웠다고 말할 수 있습니다. 그럼에도 불구하고 나는이 언어를 단지 실수 일 뿐이라고 본다. 하지만 동의합니다. Php가 훨씬 더 나쁩니다.
— Ben

@ 베네딕토 스 나는 당신의 입장을 이해하고 그것을 존중합니다. 그래도 여전히 마음에 듭니다. 물론, 그것은 특질과 특권을 가지고 있지만 (언어는 그렇지 않습니까?) 꾸준히 올바른 방향으로 나아가고 있습니다. 하지만 .. 난 실제로 그것을 너무 좋아해서 방어해야합니다. 나는 그것을 사랑하거나 미워하는 사람들로부터 아무것도 얻지 못합니다 ... hahaha 또한, 여기 에는 그런 곳이 없습니다. JS에 대한 대답조차 전혀 없었습니다.
— xDaizu
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.