팩토리 생성자는 factory 예약어로 선언합니다. 팩토리 생성자 역시 객체를 생성할 때 호출할 수 있지만, 생성자 호출만으로 객체가 생성되지는 않습니다. 팩토리 생성자에서 적절한 객체를 반환해 줘야 합니다. 결국 팩토리 생성자는 클래스 외부에서는 생성자처럼 이용되지만 실제로는 클래스 타입의 객체를 반환하는 함수입니다.
아래 코드에서 MyClass() 생성자는 factory 예약어가 붙었으므로 오류가 발생합니다. 그 이유는 팩토리 생성자로 선언했으면서 객체를 반환하지 않았기 때문입니다.
// 팩토리 생성자 잘못된 선언 예
class MyClass {
factory MyClass () { // 오류
}
}
factory로 선언한 생성자는 반드시 객체를 반환해 주어야 합니다. 그럼 null객체를 반환하는 다음 코드는 어떨까요?
// 잘못된 객체 반환 예
class MyClass {
factory MyClass() {
return null; // 오류
}
}
이 코드도 오류가 발생합니다. 팩토리 생성자는 반환 타입을 명시할 수 없으며 클래스 타입으로 고정됩니다. 예에서는 클래스 이름이 MyClass이므로 팩토리 생성자의 반환 타입은 MyClass로 고정됩니다. 그리고 MyClass는 널 불허로 선언했으므로 null을 반환할 수 없어서 오류가 발생합니다.
팩토리 생성자 자체로는 객체가 생성되지 않으며 적절한 객체를 준비해서 반환해 줘야 합니다. 따라서 팩토리 생성자가 선언된 클래스에는 객체를 생성하는 다른 생성자를 함께 선언하는 방법을 주로 사용합니다.
// 팩토리 생성자 올바른 예
class MyClass {
MyClass._instance();
factory MyClass() {
return MyClass._instance();
}
}
main() {
var obj = MyClass();
}
정리해 보면 팩토리 생성자는 클래스 외부에서 생성자처럼 이용하지만 실제로 객체를 생성하지는 않고 상황에 맞는 객체를 준비해서 반환하는 역할을 주로 합니다. 이러한 팩토리 생성자는 캐시 알고리즘이나 상속 관계에 따른 다형성을 구현할 때 유용할 수 있습니다. 예를 들어 어떤 클래스의 객체를 여러 개 생성하고 그 객체를 식별자로 구분해서 사용한다고 가정해 보겠습니다. 객체를 생성할 때 식별자에 해당하는 객체가 없으면 새로 생성하고, 있으면 기존의 객체를 그대로 반환할 수 있습니다. 다음 코드는 이러한 캐시 알고리즘을 구현한 예입니다.
// 캐시 알고리즘 구현 예
class Image {
late String url;
static Map<String, Image> _cache = <String, Image>{};
Image._instance(this.url);
factory Image(String url) {
if (_cache[url] == null) { // 전달받은 식별자가 캐시에 없으면
var obj = Image._instance(url); // 해당 식별자로 객체를 새로 생성하고
_cache[url] = obj; // 캐시에 추가
}
return _cache[url]!; // 캐시에서 식별자에 해당하는 객체 반환
}
}
main() {
var image1 = Image('a.jpg');
var image2 = Image('a.jpg');
print('image1 == image2 : ${image1 == image2}'); // image1 == image2 : true
}