C ++에는 이것이 단순하게 들리지만 정직하게 수행하는 방법을 알지 못하기 때문에 오랫동안 편안하게 생겼습니다.
C ++에서 팩토리 메소드를 올바르게 구현하려면 어떻게해야합니까?
목표 : 허용 할 수없는 결과와 성능 저하없이 클라이언트가 객체의 생성자 대신 팩토리 메소드를 사용하여 일부 객체를 인스턴스화 할 수 있도록합니다.
"팩토리 메소드 패턴"이란 객체 내부의 정적 팩토리 메소드 또는 다른 클래스에 정의 된 메소드 또는 전역 함수를 의미합니다. 일반적으로 "클래스 X의 일반적인 인스턴스화 방법을 생성자 이외의 다른 곳으로 리디렉션하는 개념".
내가 생각한 몇 가지 가능한 답변을 살펴 보도록하겠습니다.
0) 팩토리를 만들거나 생성자를 만들지 마십시오.
이것은 훌륭하게 들리며 (실제로는 종종 최상의 솔루션 임) 일반적인 치료법은 아닙니다. 우선, 객체 구성이 다른 클래스로의 추출을 정당화하기에 충분히 복잡한 작업 인 경우가 있습니다. 그러나 생성자를 사용하는 간단한 객체의 경우에도 그 사실을 제쳐두기도합니다.
내가 아는 가장 간단한 예는 2-D Vector 클래스입니다. 간단하지만 까다 롭습니다. 직교 좌표와 극좌표 모두에서 구성 할 수 있기를 원합니다. 분명히, 나는 할 수 없습니다 :
struct Vec2 {
Vec2(float x, float y);
Vec2(float angle, float magnitude); // not a valid overload!
// ...
};
내 자연스러운 사고 방식은 다음과 같습니다.
struct Vec2 {
static Vec2 fromLinear(float x, float y);
static Vec2 fromPolar(float angle, float magnitude);
// ...
};
이것은 생성자 대신 정적 팩토리 메소드를 사용하게합니다. 이것은 기본적으로 팩토리 패턴을 어떤 방식 으로든 구현하고 있음을 의미합니다 ( "클래스가 자체 팩토리가 됨"). 이것은 멋지게 보이며 (이 특별한 경우에 적합 할 것입니다) 어떤 경우에는 실패합니다. 포인트 2에서 설명하겠습니다. 계속 읽으십시오.
또 다른 경우 : 관련이없는 도메인의 GUID 또는 GUID 및 비트 필드와 같은 일부 API의 두 가지 불투명 한 typedef로 과부하를 시도하면 유형이 의미 상 완전히 다른 (이론적으로는 유효한 과부하) 유형이지만 실제로는 부호없는 int 또는 void 포인터와 같은 것.
1) 자바 웨이
우리는 동적 할당 객체 만 가지고 있기 때문에 Java는 간단합니다. 공장을 만드는 것은 다음과 같이 사소합니다.
class FooFactory {
public Foo createFooInSomeWay() {
// can be a static method as well,
// if we don't need the factory to provide its own object semantics
// and just serve as a group of methods
return new Foo(some, args);
}
}
C ++에서 이것은 다음과 같이 번역됩니다.
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
};
멋있는? 종종, 실제로. 그러나 이렇게하면 사용자가 동적 할당 만 사용하게됩니다. 정적 할당은 C ++를 복잡하게 만드는 요소이지만 종종 강력하게 만드는 요소이기도합니다. 또한 동적 할당을 허용하지 않는 일부 대상 (키워드 : 포함)이 있다고 생각합니다. 그렇다고 해당 플랫폼의 사용자가 깨끗한 OOP를 작성하는 것을 좋아하지는 않습니다.
어쨌든, 철학은 제쳐두고 : 일반적인 경우에, 나는 공장 사용자들이 동적 할당에 구속되도록 강요하고 싶지 않다.
2) 가치 별 반품
우리는 동적 할당을 원할 때 1) 멋지다는 것을 알고 있습니다. 왜 우리는 정적 할당을 추가하지 않습니까?
class FooFactory {
public:
Foo* createFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooInSomeWay() {
return Foo(some, args);
}
};
뭐? 반품 유형에 의해 과부하가 걸리지 않습니까? 물론 우리는 할 수 없습니다. 이를 반영하기 위해 메소드 이름을 변경해 봅시다. 그리고 예, 나는 이름을 변경해야하기 때문에 현재 언어와 무관 한 팩토리 디자인을 구현할 수 없기 때문에 메소드 이름을 변경 해야하는 필요성을 얼마나 싫어하는지 강조하기 위해 위의 잘못된 코드 예제를 작성했습니다. 이 코드의 모든 사용자는 사양과 구현의 차이점을 기억해야합니다.
class FooFactory {
public:
Foo* createDynamicFooInSomeWay() {
return new Foo(some, args);
}
Foo createFooObjectInSomeWay() {
return Foo(some, args);
}
};
알았어. 저기있다. 메소드 이름을 변경해야하기 때문에 추악합니다. 동일한 코드를 두 번 작성해야하므로 불완전합니다. 그러나 일단 완료되면 작동합니다. 권리?
보통은 그러나 때로는 그렇지 않습니다. Foo를 만들 때 C ++ 표준은 컴파일러 공급 업체가 객체를 내부에서 언제 만들지, 언제 반환 할지를 지정할 수 없을 정도로 자비하기 때문에 실제로 컴파일러를 사용하여 반환 값 최적화를 수행합니다. C ++에서 값별 임시 객체. 따라서 Foo가 복사 비용이 많이 드는 경우이 방법은 위험합니다.
Foo를 전혀 복사 할 수없는 경우 어떻게해야합니까? 글쎄요 ( 복사본 제거가 보장 된 C ++ 17에서 복사 할 수 없음은 위 코드에서 더 이상 문제가되지 않습니다. )
결론 : 객체를 반환하여 팩토리를 만드는 것은 실제로 (앞서 언급 한 2D 벡터와 같은) 일부 솔루션이지만 여전히 생성자를 대체하지는 않습니다.
3) 2 상 구조
누군가가 생각해 낼 수있는 또 다른 것은 객체 할당과 초기화 문제를 분리하는 것입니다. 일반적으로 다음과 같은 코드가 생성됩니다.
class Foo {
public:
Foo() {
// empty or almost empty
}
// ...
};
class FooFactory {
public:
void createFooInSomeWay(Foo& foo, some, args);
};
void clientCode() {
Foo staticFoo;
auto_ptr<Foo> dynamicFoo = new Foo();
FooFactory factory;
factory.createFooInSomeWay(&staticFoo);
factory.createFooInSomeWay(&dynamicFoo.get());
// ...
}
매력처럼 작동한다고 생각할 수도 있습니다. 우리 코드에서 지불하는 유일한 가격은 ...
이 모든 것을 작성하고 이것을 마지막으로 남겨 두었으므로 나는 그것을 싫어해야합니다. :) 왜?
우선 ... 나는 2 단계 구조의 개념을 진심으로 싫어하고 그것을 사용할 때 유죄를 느낍니다. "있는 경우 유효한 상태"라는 어설 션으로 개체를 디자인하면 코드가 더 안전하고 오류가 덜 발생합니다. 그 방법이 마음에 들어요.
그 관습을 버리고 물건의 공장을 만들기위한 목적으로 만 물건의 디자인을 바꾸는 것은 .. 음, 다루기 힘들다.
위의 내용이 많은 사람들을 설득하지는 않는다는 것을 알고 있으므로 좀 더 확실한 주장을하겠습니다. 2 단계 구성을 사용하면 다음을 수행 할 수 없습니다.
const
멤버 변수 초기화 또는 참조- 기본 클래스 생성자와 멤버 객체 생성자에 인수를 전달합니다.
그리고 아마도 내가 지금 생각할 수없는 몇 가지 단점이있을 수 있으며, 위의 글 머리 기호가 이미 저를 설득했기 때문에 특별히 의무감을 느끼지 않습니다.
따라서 공장 구현을위한 훌륭한 일반 솔루션에 가깝지 않습니다.
결론 :
우리는 다음과 같은 객체 인스턴스화 방법을 원합니다.
- 할당에 관계없이 균일 한 인스턴스화를 허용합니다.
- 건설 방법에 다른 의미있는 이름을 부여하십시오 (따라서 인수 오버로드에 의존하지 않음)
- 특히 클라이언트 측에서 현저한 성능 저하, 바람직하게는 상당한 코드 증가를 유발하지 않습니다.
- 다음과 같이 일반적이어야합니다 : 모든 수업에 도입 될 수 있습니다.
내가 언급 한 방법이 이러한 요구 사항을 충족하지 못한다는 것이 입증되었습니다.
힌트가 있습니까? 해결책을 알려주세요.이 언어가 그런 사소한 개념을 제대로 구현할 수 없다고 생각하고 싶지 않습니다.
delete
. 호출자가 포인터의 소유권을 가져 오는 "문서화 된"(소스 코드는 문서 ;-)) 이러한 종류의 메소드는 완벽하게 좋습니다.
unique_ptr<T>
대신을 반환하여 매우 명시 적으로 만들 수도 있습니다 T*
.