Java : setter 순서가 중요하지 않은 단계 작성기를 구현하는 방법은 무엇입니까?


10

편집 : 이 질문은 이론적 문제를 설명하고 싶습니다. 필수 매개 변수에 생성자 인수를 사용하거나 API가 잘못 사용되면 런타임 예외가 발생할 수 있음을 알고 있습니다. 그러나 생성자 인수 또는 런타임 검사가 필요 없는 솔루션을 찾고 있습니다.

다음 Car과 같은 인터페이스 가 있다고 상상해보십시오 .

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional
}

의견이 제안 같이,이 Car이 있어야 Engine하고 Transmission있지만은 Stereo선택 사항입니다. 즉 build(), Car인스턴스를 가질 수있는 빌더 는 build()메소드 EngineTransmission둘 다 이미 빌더 인스턴스에 제공된 경우 에만 메소드 를 가져야합니다. 이렇게하면 형식 검사기가 또는 Car없이 인스턴스 를 만들려고 시도하는 코드를 컴파일하지 않습니다 .EngineTransmission

이것은 단계 빌더를 요구 합니다. 일반적으로 다음과 같은 것을 구현합니다.

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional

    public class Builder {
        public BuilderWithEngine engine(Engine engine) {
            return new BuilderWithEngine(engine);
        }
    }

    public class BuilderWithEngine {
        private Engine engine;
        private BuilderWithEngine(Engine engine) {
            this.engine = engine;
        }
        public BuilderWithEngine engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            return new CompleteBuilder(engine, transmission);
        }
    }

    public class CompleteBuilder {
        private Engine engine;
        private Transmission transmission;
        private Stereo stereo = null;
        private CompleteBuilder(Engine engine, Transmission transmission) {
            this.engine = engine;
            this.transmission = transmission;
        }
        public CompleteBuilder engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        public CompleteBuilder stereo(Stereo stereo) {
            this.stereo = stereo;
            return this;
        }
        public Car build() {
            return new Car() {
                @Override
                public Engine getEngine() {
                    return engine;
                }
                @Override
                public Transmission getTransmission() {
                    return transmission;
                }
                @Override
                public Stereo getStereo() {
                    return stereo;
                }
            };
        }
    }
}

다른 빌더 클래스 ( Builder,, ) 체인이 있으며 BuilderWithEngine, CompleteBuilder필요한 세터 메소드를 하나씩 추가하고 마지막 클래스에는 모든 선택적 세터 메소드가 포함됩니다.
이것은이 단계 작성기의 사용자가 작성자가 필수 세터를 사용할 수있는 순서로 제한되어 있음을 의미합니다 . 다음은 가능한 사용법의 예입니다 ( engine(e)처음에는 순서대로 , 그 뒤에는 transmission(t)선택 사항 임 stereo(s)).

new Builder().engine(e).transmission(t).build();
new Builder().engine(e).transmission(t).stereo(s).build();
new Builder().engine(e).engine(e).transmission(t).stereo(s).build();
new Builder().engine(e).transmission(t).engine(e).stereo(s).build();
new Builder().engine(e).transmission(t).stereo(s).engine(e).build();
new Builder().engine(e).transmission(t).transmission(t).stereo(s).build();
new Builder().engine(e).transmission(t).stereo(s).transmission(t).build();
new Builder().engine(e).transmission(t).stereo(s).stereo(s).build();

그러나 특히 빌더에 세터뿐만 아니라 가산기가 있거나 사용자가 빌더의 특정 특성을 사용할 수있는 순서를 제어 할 수없는 경우, 이는 빌더 사용자에게 이상적이지 않은 시나리오가 많이 있습니다.

내가 생각할 수있는 유일한 해결책은 매우 복잡합니다. 필수 속성의 모든 조합이 설정되었거나 아직 설정되지 않은 경우, 나는 다른 필수 강제 세터를 호출하기 전에 호출 해야하는 것을 알고있는 전용 빌더 클래스를 만들었습니다. build()메소드가 사용 가능해야하는 상태 및 해당 세터 각각이 build()메소드 를 포함하는 한 단계에 가까운 더 완전한 유형의 빌더를 리턴합니다 .
나는 아래의 코드를 추가,하지만 당신은 내가 당신이를 만들 수있는 FSM 만들 유형의 시스템을 사용하고 있다고 말할 수 Builder중 하나로 전환 할 수있다, BuilderWithEngine또는 BuilderWithTransmission둘 다 다음으로 전환 될 수있는 CompleteBuilder, 어떤 구현build()방법. 이러한 빌더 인스턴스에서 선택적 세터를 호출 할 수 있습니다. 여기에 이미지 설명을 입력하십시오

public interface Car {
    public Engine getEngine(); // required
    public Transmission getTransmission(); // required
    public Stereo getStereo(); // optional

    public class Builder extends OptionalBuilder {
        public BuilderWithEngine engine(Engine engine) {
            return new BuilderWithEngine(engine, stereo);
        }
        public BuilderWithTransmission transmission(Transmission transmission) {
            return new BuilderWithTransmission(transmission, stereo);
        }
        @Override
        public Builder stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class OptionalBuilder {
        protected Stereo stereo = null;
        private OptionalBuilder() {}
        public OptionalBuilder stereo(Stereo stereo) {
            this.stereo = stereo;
            return this;
        }
    }

    public class BuilderWithEngine extends OptionalBuilder {
        private Engine engine;
        private BuilderWithEngine(Engine engine, Stereo stereo) {
            this.engine = engine;
            this.stereo = stereo;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            return new CompleteBuilder(engine, transmission, stereo);
        }
        public BuilderWithEngine engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        @Override
        public BuilderWithEngine stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class BuilderWithTransmission extends OptionalBuilder {
        private Transmission transmission;
        private BuilderWithTransmission(Transmission transmission, Stereo stereo) {
            this.transmission = transmission;
            this.stereo = stereo;
        }
        public CompleteBuilder engine(Engine engine) {
            return new CompleteBuilder(engine, transmission, stereo);
        }
        public BuilderWithTransmission transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        @Override
        public BuilderWithTransmission stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
    }

    public class CompleteBuilder extends OptionalBuilder {
        private Engine engine;
        private Transmission transmission;
        private CompleteBuilder(Engine engine, Transmission transmission, Stereo stereo) {
            this.engine = engine;
            this.transmission = transmission;
            this.stereo = stereo;
        }
        public CompleteBuilder engine(Engine engine) {
            this.engine = engine;
            return this;
        }
        public CompleteBuilder transmission(Transmission transmission) {
            this.transmission = transmission;
            return this;
        }
        @Override
        public CompleteBuilder stereo(Stereo stereo) {
            super.stereo(stereo);
            return this;
        }
        public Car build() {
            return new Car() {
                @Override
                public Engine getEngine() {
                    return engine;
                }
                @Override
                public Transmission getTransmission() {
                    return transmission;
                }
                @Override
                public Stereo getStereo() {
                    return stereo;
                }
            };
        }
    }
}

당신이 알 수 있듯이,이 것이 필요한 다른 빌더 클래스의 수로서, 확장 성이 좋지 않습니다 O (2 ^ n은) 여기서 n은 필수 세터의 수입니다.

따라서 내 질문 : 이보다 우아하게 수행 할 수 있습니까?

(Scala도 수용 가능하지만 Java와 작동하는 답변을 찾고 있습니다)


1
IoC 컨테이너를 사용하여 이러한 모든 종속성을 막는 데 방해가되는 것은 무엇입니까? 또한, 당신이 주장한대로 순서가 중요하지 않다면 왜 반환하는 일반적인 setter 메소드를 사용할 수 this없습니까?
Robert Harvey

.engine(e)한 빌더에 대해 두 번 호출한다는 것은 무엇을 의미 합니까?
Erik Eidt

3
모든 조합에 대해 수동으로 클래스를 작성하지 않고 정적으로 확인하려면 매크로 또는 템플릿 메타 프로그래밍과 같은 목 수염 수준의 항목을 사용해야합니다. Java는 그 표현으로는 충분하지 않습니다. 내 지식으로는 다른 언어의 동적으로 검증 된 솔루션보다 그만한 가치가 없을 것입니다.
Karl Bielefeldt

1
Robert : 목표는 타입 체커가 엔진과 변속기가 모두 필수적이라는 사실을 강제하는 것입니다. 당신도 호출 할 수 없습니다 이런 식으로 build()당신이 호출되지 않은 경우 engine(e)transmission(t)전.
derabbink

Erik : 기본 Engine구현 으로 시작 하고 나중에보다 구체적인 구현으로 덮어 쓸 수 있습니다. 그러나 engine(e)세터가 아니라 가산기 라면 이것이 더 의미가 addEngine(e)있습니다. 이는 Car하나 이상의 엔진 / 모터로 하이브리드 자동차를 생산할 수 있는 제작자에게 유용합니다 . 이것은 좋은 예이므로, 왜 당신이 이것을 원하는지에 대한 세부 사항은 간결하게 설명하지 않았습니다.
derabbink

답변:


3

제공 한 메서드 호출에 따라 두 가지 요구 사항이있는 것 같습니다.

  1. 하나의 (필수) 엔진, 하나의 (필수) 전송 및 하나의 (선택적) 스테레오 만.
  2. 하나 이상의 (필수) 엔진, 하나 이상의 (필수) 변속기 및 하나 이상의 (선택적) 스테레오.

여기에서 첫 번째 문제 는 수업 이 무엇을 원하는지 모른다 는 것입니다. 그 중 일부는 빌드 된 객체의 모양을 알 수 없다는 것입니다.

자동차는 엔진 하나와 변속기 하나만 가질 수 있습니다. 하이브리드 자동차조차 엔진이 하나만 있습니다 (아마도 GasAndElectricEngine)

두 가지 구현을 모두 다룰 것입니다.

public class CarBuilder {

    public CarBuilder(Engine engine, Transmission transmission) {
        // ...
    }

    public CarBuilder setStereo(Stereo stereo) {
        // ...
        return this;
    }
}

public class CarBuilder {

    public CarBuilder(List<Engine> engines, List<Transmission> transmission) {
        // ...
    }

    public CarBuilder addStereo(Stereo stereo) {
        // ...
        return this;
    }
}

엔진과 변속기가 필요한 경우 생성자에 있어야합니다.

어떤 엔진이나 변속기가 필요한지 모르면 아직 설정하지 마십시오. 스택을 너무 멀리 빌더를 작성하고 있다는 표시입니다.


2

null 객체 패턴을 사용하지 않는 이유는 무엇입니까? 이 빌더를 제거하십시오. 작성할 수있는 가장 우아한 코드는 실제로 작성할 필요가없는 코드입니다.

public final class CarImpl implements Car {
    private final Engine engine;
    private final Transmission transmission;
    private final Stereo stereo;

    public CarImpl(Engine engine, Transmission transmission) {
        this(engine, transmission, new DefaultStereo());
    }

    public CarImpl(Engine engine, Transmission transmission, Stereo stereo) {
        this.engine = engine;
        this.transmission = transmission;
        this.stereo = stereo;
    }

    //...

}

이것이 바로 내가 생각한 것입니다. 세 개의 매개 변수 생성자가 없지만 필수 요소가있는 두 개의 매개 변수 생성자 및 선택 사항 인 스테레오에 대한 세터 만 있습니다.
Encaitar

1
와 같은 간단한 (고려 된) 예제 Car에서 이것은 c'tor 인수의 수가 매우 적으므로 의미가 있습니다. 그러나 약간 복잡한 항목 (> = 4 개의 필수 인수)을 처리하자마자 모든 것이 처리하기 어렵거나 읽기 어려워집니다 ( "엔진이나 변속기가 먼저 왔습니까?"). 그렇기 때문에 빌더를 사용하는 이유는 다음과 같습니다. API를 사용하면 구성중인 항목에 대해보다 명확하게 지정할 수 있습니다.
derabbink

1
@derabbink 왜이 경우 작은 클래스로 수업을 나누지 않겠습니까? 빌더를 사용하면 클래스가 너무 많은 작업을 수행하고 유지 관리 할 수 ​​없게 된 사실을 숨길 수 있습니다.
발견

1
패턴의 광기를 끝내는 것에 대한 조언.
Robert Harvey

@Spotted 일부 클래스에는 많은 데이터가 포함되어 있습니다. 예를 들어, HTTP 요청에 대한 모든 관련 정보를 보유하고 데이터를 CSV 또는 JSON 형식으로 출력하는 액세스 로그 클래스를 생성하려는 경우. 많은 데이터가 있으며 컴파일 시간 동안 일부 필드를 강제로 적용하려면 매우 긴 인수 목록 생성자가있는 빌더 패턴이 필요합니다.
ssgao

1

첫째, 내가 일했던 상점보다 시간이 더 많지 않다면, 어떤 작업 순서를 허용하거나 하나 이상의 라디오를 지정할 수 있다는 사실에 따라 살 가치가 없을 것입니다. 사용자 입력이 아닌 코드에 대해 이야기하고 있으므로 컴파일 시간에 1 초가 아닌 단위 테스트 중에 실패 할 수있는 어설 션이있을 수 있습니다.

그러나 주석에 주어진 바와 같이 엔진과 변속기가 있어야한다는 제약 조건이있는 경우 빌더의 생성자 인 모든 필수 속성을 배치하여이를 강제하십시오.

new Builder(e, t).build();                      // ok
new Builder(e, t).stereo(s).build();            // ok
new Builder(e, t).stereo(s).stereo(s).build();  // exception on second call to stereo as stereo is already set 

선택적인 스테레오 일 뿐이라면, 빌더의 서브 클래스를 사용하여 최종 단계를 수행하는 것이 가능하지만, 그보다 더 많은 것은 테스트가 아닌 컴파일 타임에 오류를 얻는 것의 노력이 가치가 없을 것입니다.


0

필요한 다른 빌더 클래스 수는 O (2 ^ n)입니다. 여기서 n은 필수 설정자 수입니다.

이 질문에 대한 올바른 방향을 이미 추측했습니다.

컴파일 타임 검사를 받으려면 (2^n)유형 이 필요 합니다. 런타임 검사를 받으려면 (2^n)상태 를 저장할 수있는 변수가 필요합니다 . n비트 정수 할 것입니다.


C ++ 지원하므로 비 템플릿 유형 파라미터 (예를 들어, 정수 값) 은 C ++ 클래스 템플릿으로 인스턴스화하는 것이 가능하다 O(2^n)유사한 방식을 사용하여 다른 유형 .

그러나 유형이 아닌 템플릿 매개 변수를 지원하지 않는 언어에서는 유형 시스템을 사용하여 O(2^n)다른 유형 을 인스턴스화 할 수 없습니다 .


다음 기회는 Java 어노테이션 (및 C # 속성)입니다. 이러한 추가 메타 데이터는 주석 프로세서 가 사용될 때 컴파일 타임에 사용자 정의 동작을 트리거하는 데 사용될 수 있습니다 . 그러나 구현하기에는 너무 많은 작업이 필요합니다. 이 기능을 제공하는 프레임 워크를 사용하는 경우이를 사용하십시오. 그렇지 않으면 다음 기회를 확인하십시오.


마지막으로 O(2^n)다른 상태를 런타임에 변수 (문자 적으로 최소 n비트 너비 를 갖는 정수 )로 저장하는 것이 매우 쉽습니다. 이것이 가장 이득이 높은 답변이 런타임시이 검사를 수행하도록 권장하는 이유입니다. 잠재적 이득과 비교할 때 컴파일 타임 검사를 구현하는 데 너무 많은 노력이 필요하기 때문입니다.

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