물류 관리 응용 프로그램을 만들고 있다고 가정하자, 프로그램의 첫번째 버전은 트럭에 의한 운송만 처리할 수 있으므로 대부분 많은 양의 코드는 Truck
클래스 안에 존재한다.
잠시 후, 프로그램이 꽤 유명해졌다. 매일 당신은 해운 회사로부터 해상 물류를 프로그램에 통합해 달라는 수십 건의 요청을 받는다.
이미 코드의 나머지 부분들이 기존 클래스에 연결되어 있는 경우, 프로그램에 새로운 클래스를 추가하는 건 쉽지 않다.
좋은 소식이지만, 대부분의 코드가 Truck
클래스에 연결되어 있다. 프로그램에 Ships
클래스를 추가하려면 전체 코드베이스를 변경해야 한다. 또한 이런 방식으로 프로그램에 다른 교통 수단을 추가하기로 했을 때, 모든 변경을 다시 수행해야 할 수 있다.
결과적으로 운송 객체의 클래스에 따라 앱의 동작을 전환하는 조건 분기문으로 가득 찬 불쾌한 코드가 많아질 것이다.
팩토리 메서드 패턴은 연산자를 사용해서 직접적인 객체 생성을 특별한 팩토리 메서드에 대한 호출로 대체할 것을 제안한다. 객체는 물론 new연산자를 통해 생성되지만, 팩토리 메서드 내에서 호출된다. 팩토리 메서드에 의해 반환된 객체는 Product 라고 한다.
서브클래스(RoadLogistics
, SeaLogistics
)는 팩토리 메소드에 의해 반환되는 객체의 클래스를 변경할 수 있다.
언뜻 보기에는 이 변경이 무의미해 보일 수 있다. 하지만 이제 하위 클래스에서 팩토리 메서드를 override 하고 메서드에 의해 생성되는 제품 클래스를 변경할 수 있다.
하지만 약간의 제한이 있다. 서브클래스는 이러한 제품에 공통 기본 클래스 또는 인터페이스가 있는 경우에만 다른 유형의 제품을 반환할 수 있다. 또한 기본 클래스의 팩토리 메서드는 이 인터페이스로 선언된 리턴 유형을 가져야 한다.
모든 제품은 동일한 인터페이스를 따라야 한다.
예를 들어, Truck
및 Ship
클래스 모두 deliver
라는 메서드를 선언하는 Transport
인터페이스를 구현해야 한다. 각 클래스는 이 방법을 다르게 구현한다. 트럭은 육로로 화물을 배달하고 선박은 해상으로 화물을 배달한다. 클래스의 팩토리 메서드 RoadLogistics는 트럭 객체를 반환하지만, SeaLogistics 클래스의 팩토리 메서드는 선박 객체를 반환한다.
모든 제품 클래스가 공통 인터페이스를 구현하는 해당 객체를 손상시키지 않고 클라이언트 코드에 전달할 수 있다.
팩토리 메서드를 사용하는 코드(클라이언트 코드)는 다양한 서브 클래스에서 반환된 실제 제품간의 차이를 모른다. 클라이언트는 모든 제품을 Transport
라고 추상적으로 취급하기 때문이다. 클라이언트 코드는 모든 Transport
객체에 메서드가 있어야 한다는 것을 알고 deliver 메서드가 있다는 것을 알지만, 정확히 어떻게 작동하는지는 클라이언트에게 중요하지 않다.
객체지향 프로그래밍의 특징 중 다형성(Polymorphism)을 활용하는 패턴이라고 생각된다!
Product
는 작성자와 해당 서브 클래스가 생성할 수 있는 모든 객체에 공통적인 인터페이스를 선언한다.Concrete Product
는 Product
인터페이스의 다른 구현이다.Product
객체를 반환하는 팩토리 메서드를 선언한다. 이 메서드의 리턴 타입이 Product
인터페이스와 일치해야 한다. 이 팩토리 메서드를 추상으로 선언해서 모든 서브 클래스가 자체 버전의 메서드를 구현하도록 할 수 있다. 대신, 기본 팩토리 메서드가 일부 기본 product
유형을 리턴할 수 있다.ConcreteCreator
는 기본 팩토리 메서드를 재정의하므로 다른 유형의 제품을 반환한다. 팩토리 메서드는 항상 새 인스턴스를 만들 필요가 없다. 또한 캐시, 객체 풀 또는 다른 소스에서 기존 객체를 반환시킬 수 있다.import XCTest
/// Product 클래스의 새 개체를 반환해야 하는 팩토리 메서드를 선언한다.
protocol Creator {
// 추상 객체 Product를 반환하는 factory 메서드
func factoryMethod() -> Product
// 서브 클래스는 팩토리 메서드를 오버라이딩하고, 다른 유형의 제품을 반환해서 비즈니스 로직을 간접적으로 변경할 수 있다.
func someOperation() -> String
}
// 이 extension을 통해 Creator의 기본 동작을 구현한다. 서브클래스 에서 오버라이딩 될 수 있다.
extension Creator {
func someOperation() -> String {
// Call the factory method to create a Product object.
let product = factoryMethod()
// Now, use the product.
return "Creator: The same creator's code has just worked with " + product.operation()
}
}
// 제품의 유형을 변경하기 위해 팩토리 메서드를 오버라이딩 한다.
class ConcreteCreator1: Creator {
// 비록 구체 프로덕트가 실제로 여기서 반환되더라도 시그니처는 항상 추상 프로덕트를 리턴한다.
// 그래서 Creator는 구체적인 프로덕트 클래스와 독립될 수 있다.
public func factoryMethod() -> Product {
return ConcreteProduct1()
}
}
class ConcreteCreator2: Creator {
public func factoryMethod() -> Product {
return ConcreteProduct2()
}
}
/// Product 프로토콜은 모든 구체적인 제품이 구현해야 하는 작업을 선언한다.
protocol Product {
func operation() -> String
}
/// Concrete Products는 Product 프로토콜의 다양한 구현을 제공한다.
class ConcreteProduct1: Product {
func operation() -> String {
return "{Result of the ConcreteProduct1}"
}
}
class ConcreteProduct2: Product {
func operation() -> String {
return "{Result of the ConcreteProduct2}"
}
}
/// 클라이언트 코드는 기본 프로토콜을 통해서라도 구체적인 작성자의 인스턴스와 함께 작동한다.
class Client {
// ...
static func someClientCode(creator: Creator) {
print("Client: I'm not aware of the creator's class, but it still works.\n"
+ creator.someOperation())
}
// ...
}
/// Let's see how it all works together.
class FactoryMethodConceptual: XCTestCase {
func testFactoryMethodConceptual() {
/// The Application picks a creator's type depending on the
/// configuration or environment.
print("App: Launched with the ConcreteCreator1.")
Client.someClientCode(creator: ConcreteCreator1())
print("\nApp: Launched with the ConcreteCreator2.")
Client.someClientCode(creator: ConcreteCreator2())
}
}