긴 매개 변수 목록이있는 생성자를 사용하지 않고 크고 불변의 객체 만들기


96

불변 할 수있는 큰 (3 개 이상의 필드) 개체가 있습니다. 이 경우가 발생할 때마다 긴 매개 변수 목록으로 생성자 혐오감을 만드는 경향이 있습니다.

옳지 않고 사용하기 어렵고 가독성이 떨어집니다.

필드가 목록과 같은 일종의 수집 유형이면 더 나쁩니다. 단순 addSibling(S s)하면 객체 생성이 너무 쉬워 지지만 객체를 변경 가능하게 렌더링합니다.

그런 경우에 너희들은 무엇을 사용합니까?

나는 Scala와 Java를 사용하고 있지만 언어가 객체 지향적이라면 문제는 언어 불가지론 적이라고 생각합니다.

내가 생각할 수있는 솔루션 :

  1. "긴 매개 변수 목록이있는 생성자 혐오"
  2. 빌더 패턴

5
나는 빌더 패턴이 이것에 대한 최고이자 가장 표준적인 솔루션이라고 생각합니다.
Zachary Wright

@Zachary : 당신이 옹호하는 것은 여기 Joshua Bloch가 설명한 것처럼 특별한 형태의 빌더 패턴에서만 작동합니다. drdobbs.com/java/208403883 ? 이 "특수한 형태의 빌더 패턴"(내 답변 참조).
SyntaxT3rr0r

답변:


76

글쎄, 일단 생성되면 읽기 쉽고 불변 객체를 모두 원하십니까?

유창한 인터페이스가 올바르게 완료 되면 도움이 될 것이라고 생각 합니다.

다음과 같이 보일 것입니다 (순수하게 구성된 예제).

final Foo immutable = FooFactory.create()
    .whereRangeConstraintsAre(100,300)
    .withColor(Color.BLUE)
    .withArea(234)
    .withInterspacing(12)
    .build();

대부분의 Java 프로그래머가 유창한 인터페이스를 잘못 이해하고 객체를 빌드하는 데 필요한 방법으로 객체를 오염시키기 때문에 "CORRECTLY DONE" 을 굵게 썼습니다 . 이는 물론 완전히 잘못된 것입니다.

비결은 build () 메서드 만이 실제로 Foo를 생성한다는 것입니다 (따라서 Foo는 변경 불가능할 수 있음).

FooFactory.create () , 여기서 XXX (..)withXXX (..) 모두 "다른 것"을 만듭니다.

다른 것이 FooFactory 일 수 있습니다. 여기에 한 가지 방법이 있습니다 ....

FooFactory는 다음과 같습니다.

// Notice the private FooFactory constructor
private FooFactory() {
}

public static FooFactory create() {
    return new FooFactory();
}

public FooFactory withColor( final Color col ) {
    this.color = color;
    return this;
}

public Foo build() {
    return new FooImpl( color, and, all, the, other, parameters, go, here );
}

11
@ all : "FooImpl"의 "Impl"접미사에 대해 불만을 제기하지 마십시오.이 클래스는 공장 내부에 숨겨져 있으며 유창한 인터페이스를 작성하는 사람 외에는 아무도 볼 수 없습니다. 사용자가 관심을 갖는 것은 그가 "Foo"를 얻는다는 것입니다. "FooImpl" "FooPointlessNitpick"도 호출 할 수 있습니다.;)
SyntaxT3rr0r

5
선제 적이라고 느끼십니까? ;) 당신은 이것에 대해 과거에 nitpicked되었습니다. :)
Greg D

3
나는 그가 언급 년대 일반적인 오류는 사람들이 "withXXX"(등) 방법을 추가하는 것이 믿습니다 Foo 아니라 별도의를하는 것보다, 객체 FooFactory.
Dean Harding

3
여전히 FooImpl8 개의 매개 변수 가있는 생성자가 있습니다. 개선점은 무엇입니까?
Mot

4
나는이 코드를 호출하지 않을 것이며 immutable, 사람들이 팩토리 객체를 재사용하는 것을 두려워 할 것입니다. 내 말은 : 지금은 여성 만 포함 FooFactory people = FooFactory.create().withType("person"); Foo women = people.withGender("female").build(); Foo talls = people.tallerThan("180m").build();되는 곳 talls입니다. 변경 불가능한 API에서는 발생하지 않아야합니다.
Thomas Ahle

60

Scala 2.8에서는 copy케이스 클래스 의 메서드 뿐만 아니라 명명 된 매개 변수와 기본 매개 변수를 사용할 수 있습니다 . 다음은 몇 가지 예제 코드입니다.

case class Person(name: String, age: Int, children: List[Person] = List()) {
  def addChild(p: Person) = copy(children = p :: this.children)
}

val parent = Person(name = "Bob", age = 55)
  .addChild(Person("Lisa", 23))
  .addChild(Person("Peter", 16))

31
스칼라 언어 발명에 +1. 그래, 그것은 평판 시스템의 남용이지만 .. 나는 스칼라를 너무 좋아해서 그렇게해야했다. :)
Malax

1
오, 이런 .. 거의 똑같은 대답을했습니다! 글쎄, 나는 좋은 회사에있다. :-) 나는 전에 당신의 대답을 보지 못한 것이 궁금합니다 ... <shrug>
Daniel C. Sobral

20

음, Scala 2.8에서 이것을 고려하십시오.

case class Person(name: String, 
                  married: Boolean = false, 
                  espouse: Option[String] = None, 
                  children: Set[String] = Set.empty) {
  def marriedTo(whom: String) = this.copy(married = true, espouse = Some(whom))
  def addChild(whom: String) = this.copy(children = children + whom)
}

scala> Person("Joseph").marriedTo("Mary").addChild("Jesus")
res1: Person = Person(Joseph,true,Some(Mary),Set(Jesus))

물론 여기에는 문제가 있습니다. 예를 들어, 만드는 시도 espouse하고 Option[Person], 그리고 서로 결혼 한 두 사람을 받고. 나는 그것을 해결할 방법을 생각할 수 없다. private var그리고 / 또는 private생성자와 공장에 의지하지 않고서는 그것을 해결할 수 없다 .


11

다음은 몇 가지 추가 옵션입니다.

옵션 1

구현 자체를 변경 가능하게 만드 되 변경 가능 및 불변에 노출되는 인터페이스를 분리하십시오. 이것은 Swing 라이브러리 디자인에서 가져온 것입니다.

public interface Foo {
  X getX();
  Y getY();
}

public interface MutableFoo extends Foo {
  void setX(X x);
  void setY(Y y);
}

public class FooImpl implements MutableFoo {...}

public SomeClassThatUsesFoo {
  public Foo makeFoo(...) {
    MutableFoo ret = new MutableFoo...
    ret.setX(...);
    ret.setY(...);
    return ret; // As Foo, not MutableFoo
  }
}

옵션 2

응용 프로그램에 크고 미리 정의 된 불변 개체 (예 : 구성 개체) 집합이 포함 된 경우 Spring 프레임 워크 사용을 고려할 수 있습니다 .


3
옵션 1은 영리하지만 ( 너무 영리 하지는 않음 ) 좋아합니다.
Timothy

4
이전에이 작업을 수행했지만 객체가 여전히 변경 가능하고 변경 방법 만 "숨겨져 있기 때문에"좋은 솔루션이 아니라고 생각합니다. 내가이 주제에 대해 너무 까다로울 수도 있습니다 ...
Malax

옵션 1의 변형은 Immutable 및 Mutable 변형에 대해 별도의 클래스를 갖는 것입니다. 이것은 인터페이스 접근 방식보다 더 나은 안전성을 제공합니다. 아마도 당신은 변경 가능한 인터페이스로 캐스트되어야하는 Immutable이라는 이름의 변경 가능한 객체를 그들에게 줄 때마다 API 소비자에게 거짓말을하고있는 것입니다. 별도의 클래스 접근 방식에는 앞뒤로 변환하는 메서드가 필요합니다. JodaTime API는이 패턴을 사용합니다. DateTime 및 MutableDateTime을 참조하십시오.
toolbear 2011-08-12

6

다양한 종류 의 불변성 이 있음을 기억하면 도움이됩니다 . 귀하의 경우에는 "팝 시클"불변성이 정말 잘 작동 할 것이라고 생각합니다.

팝 시클 불변성 : 내가 기발하게 한번 쓰기 불변성의 약간의 약화라고 부르는 것입니다. 초기화하는 동안 잠시 동안 변경 가능하다가 영원히 "고정"된 객체 나 필드를 상상할 수 있습니다. 이러한 종류의 불변성은 순환 적으로 서로를 참조하는 불변 객체 또는 디스크에 직렬화되고 역 직렬화시 전체 역 직렬화 프로세스가 완료 될 때까지 "유동적"이어야하는 불변 객체에 특히 유용합니다. 겨울 왕국.

따라서 개체를 초기화 한 다음 더 이상 쓸 수 없음을 나타내는 일종의 "고정"플래그를 설정합니다. 가급적이면 함수 뒤에 변이를 숨기면 함수가 API를 사용하는 클라이언트에게 여전히 순수합니다.


1
반대표? 왜 이것이 좋은 해결책이 아닌지에 대한 의견을 남기고 싶은 사람이 있습니까?
Juliet

+1. clone()새 인스턴스를 파생 하는 데 사용하는 것을 암시하기 때문에 누군가가 이것을 반대 투표 했을 수 있습니다.
finnw

: 그것은 자바에서 스레드 안전을 손상시킬 수 있기 때문에 어쩌면 그것은이었다 java.sun.com/docs/books/jls/third_edition/html/memory.html#17.5을
finnw

단점은 미래의 개발자가 "고정"플래그를 처리 할 때주의해야한다는 것입니다. 나중에 뮤 테이터 메서드가 추가되어 메서드가 고정 해제되었다고 주장하는 것을 잊으면 문제가 발생할 수 있습니다. 마찬가지로 freeze()메서드를 호출해야하지만하지 않는 새 생성자가 작성 되면 상황이 추악해질 수 있습니다.
stalepretzel

5

변경 불가능한 객체가 변경자처럼 보이는 메서드 (예 : addSibling)를 노출하도록 만들 수도 있지만 새 인스턴스를 반환하도록 할 수도 있습니다. 그것이 불변의 Scala 컬렉션이하는 일입니다.

단점은 필요한 것보다 더 많은 인스턴스를 만들 수 있다는 것입니다. 부분적으로 빌드 된 객체를 처리하지 않으려는 경우가 아니면 대부분의 경우 괜찮은 형제가없는 일부 노드와 같은 중간 유효한 구성이있는 경우에만 적용 할 수 있습니다.

예를 들어 아직 목적지가없는 그래프 에지는 유효한 그래프 에지가 아닙니다.


필요한 것보다 더 많은 인스턴스를 생성하는 단점은 그다지 문제가되지 않습니다. 수명이 짧은 개체의 가비지 수집과 마찬가지로 개체 할당은 매우 저렴합니다. 이스케이프 분석이 기본적으로 활성화되면 이러한 종류의 "중간 객체"가 스택 할당 될 가능성이 높으며 문자 그대로 생성하는 데 비용이 들지 않습니다.
gustafc

2
@gustafc : 네. Cliff Click은 한 번 큰 상자 (864 코어, 768GB RAM) 중 하나에서 Rich Hickey의 Clojure Ant Colony 시뮬레이션을 벤치마킹 한 방법에 대해 이야기 한 적이 있습니다. 700 개의 코어에서 각각 100 %로 실행되는 700 개의 병렬 스레드에서 초당 임시 쓰레기 . GC는 땀도 흘리지 않았습니다.
Jörg W Mittag

5

네 가지 가능성을 고려하십시오.

new Immutable(one, fish, two, fish, red, fish, blue, fish); /*1 */

params = new ImmutableParameters(); /*2 */
params.setType("fowl");
new Immutable(params);

factory = new ImmutableFactory(); /*3 */
factory.setType("fish");
factory.getInstance();

Immutable boringImmutable = new Immutable(); /* 4 */
Immutable lessBoring = boringImmutable.setType("vegetable");

나에게 2, 3, 4는 각각 다른 상황에 적응한다. 첫 번째는 OP에서 인용 한 이유로 사랑하기 어렵고 일반적으로 약간의 크립을 겪고 약간의 리팩토링이 필요한 디자인의 증상입니다.

내가 나열한 것은 (2) '공장'뒤에 상태가 없을 때 좋은 것이고, (3)은 상태가있을 때 선택한 디자인이다. 스레드 및 동기화에 대해 걱정하고 싶지 않을 때 (3)이 아닌 (2)를 사용하고 있으며 많은 개체를 생성하는 동안 값 비싼 설정을 분할하는 것에 대해 걱정할 필요가 없습니다. (3) 반면에 실제 작업이 공장 건설 (SPI에서 설정, 구성 파일 읽기 등)에 들어갈 때 호출됩니다.

마지막으로 다른 사람의 대답은 옵션 (4)에 대해 언급했습니다. 여기서 변경 불가능한 개체가 많고 선호되는 패턴은 오래된 개체에서 뉴스를 가져 오는 것입니다.

나는 '패턴 팬 클럽'의 회원이 아니라는 점에 유의하세요. 물론 어떤 것들은 따라 할만한 가치가 있지만, 사람들이 이름과 재미있는 모자를 주면 도움이되지 않는 삶을 사는 것 같습니다.


6
이것은 빌더 패턴입니다 (옵션 2)
Simon Nickerson

그것은 그의 불변 객체를 방출하는 팩토리 ( '빌더') 객체가 아닐까요?
bmargulies

그 구별은 꽤 의미 론적으로 보입니다. 콩은 어떻게 만들어 지나요? 건축업자와 다른 점은 무엇입니까?
Carl

빌더 (또는 기타)를위한 Java Bean 규칙 패키지를 원하지 않습니다.
Tom Hawtin-tackline

4

또 다른 가능한 옵션은 구성 가능한 필드를 더 적게 갖도록 리팩터링하는 것입니다. 필드 그룹이 (대부분) 서로간에 만 작동하는 경우 자체의 작은 불변 개체로 모으십시오. "작은"객체의 생성자 / 빌더는이 "큰"객체의 생성자 / 빌더처럼 더 관리하기 쉬워야합니다.


1
참고 :이 답변에 대한 마일리지는 문제, 코드 기반 및 개발자 기술에 따라 다를 수 있습니다.
Carl

2

저는 C #을 사용하며 이것이 제 접근 방식입니다. 치다:

class Foo
{
    // private fields only to be written inside a constructor
    private readonly int i;
    private readonly string s;
    private readonly Bar b;

    // public getter properties
    public int I { get { return i; } }
    // etc.
}

옵션 1. 선택적 매개 변수가있는 생성자

public Foo(int i = 0, string s = "bla", Bar b = null)
{
    this.i = i;
    this.s = s;
    this.b = b;
}

예를 들어 new Foo(5, b: new Bar(whatever)). 4.0 이전의 Java 또는 C # 버전에는 해당되지 않습니다. 그러나 모든 솔루션이 언어에 구애받지 않는 방법의 예이므로 여전히 보여줄 가치가 있습니다.

옵션 2. 단일 매개 변수 객체를 취하는 생성자

public Foo(FooParameters parameters)
{
    this.i = parameters.I;
    // etc.
}

class FooParameters
{
    // public properties with automatically generated private backing fields
    public int I { get; set; }
    public string S { get; set; }
    public Bar B { get; set; }

    // All properties are public, so we don't need a full constructor.
    // For convenience, you could include some commonly used initialization
    // patterns as additional constructors.
    public FooParameters() { }
}

사용 예 :

FooParameters fp = new FooParameters();
fp.I = 5;
fp.S = "bla";
fp.B = new Bar();
Foo f = new Foo(fp);`

3.0부터 C #은 개체 이니셜 라이저 구문 (이전 예제와 의미 상 동일)을 사용하여이를 더욱 우아하게 만듭니다.

FooParameters fp = new FooParameters { I = 5, S = "bla", B = new Bar() };
Foo f = new Foo(fp);

옵션 3 :
너무 많은 매개 변수가 필요하지 않도록 클래스를 다시 디자인하십시오. 당신은 그 책임을 여러 클래스로 나눌 수 있습니다. 또는 요청시 생성자가 아닌 특정 메서드에만 매개 변수를 전달합니다. 항상 실행 가능한 것은 아니지만 가능하다면 할 가치가 있습니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.