추상 팩토리 패턴
개요
밝은 테마의 버튼을 골랐는데 체크박스만 어두운 테마라면 화면 구성이 어긋난다. 버튼 하나를 만드는 방법보다, 함께 쓰는 부품들을 같은 계열로 고르는 규칙이 필요하다.
추상 팩토리(Abstract Factory)는 관련 제품을 만드는 약속을 한곳에 모은다. 화면은 구체적인 버튼 클래스를 고르는 대신 팩토리에 필요한 부품을 요청한다.
UML
Dialog는 IWidgetFactory에서 버튼과 체크박스를 받는다. LightFactory가 같은 테마의 두 제품을 만들고, 화면은 각 제품의 Draw()만 사용한다.
역사
GoF는 1994년 Design Patterns에서 Abstract Factory를 생성 패턴으로 정리했다. 서로 다른 사용자 인터페이스 계열을 지원하는 도구가 대표적인 적용 상황이다.1
왜 사용하는가?
화면마다 테마를 검사하고 버튼과 체크박스를 따로 고르면 같은 선택 코드가 퍼진다. 팩토리를 처음 조립할 때 정해 두면, 화면은 사용할 부품만 요청하면 된다.
제품 종류를 새로 추가할 때는 팩토리 계약과 각 계열 구현을 함께 고쳐야 한다. 계열을 바꾸는 일이 잦고 그 안의 제품 종류가 비교적 안정적일 때 잘 맞는다.
예제
밝은 테마의 팩토리로 화면 하나를 만든다. 제품은 실제 GUI 대신 이름을 반환해 조합을 보여 준다.
using System;
using System.Collections.Generic;
using System.Linq;
Console.WriteLine(new Dialog(new LightFactory()).Render());
interface IButton { string Draw(); }
interface ICheckbox { string Draw(); }
interface IWidgetFactory
{
IButton Button();
ICheckbox Checkbox();
}
sealed class LightButton : IButton { public string Draw() => "light button"; }
sealed class LightCheckbox : ICheckbox { public string Draw() => "light checkbox"; }
sealed class DarkButton : IButton { public string Draw() => "dark button"; }
sealed class DarkCheckbox : ICheckbox { public string Draw() => "dark checkbox"; }
sealed class LightFactory : IWidgetFactory
{
public IButton Button() => new LightButton();
public ICheckbox Checkbox() => new LightCheckbox();
}
sealed class DarkFactory : IWidgetFactory
{
public IButton Button() => new DarkButton();
public ICheckbox Checkbox() => new DarkCheckbox();
}
sealed class Dialog(IWidgetFactory factory)
{
public string Render() => factory.Button().Draw() + " + " + factory.Checkbox().Draw();
}
light button + light checkboxDialog에는 LightButton을 직접 만드는 코드가 없다. 버튼과 체크박스의 구체적인 선택은 팩토리가 맡는다.
범위 넓힌 예제
이번에는 같은 Dialog에 두 팩토리를 차례로 넘긴다. 화면 코드는 그대로고, 함께 생성되는 제품의 계열만 달라진다.
using System;
using System.Collections.Generic;
using System.Linq;
foreach (IWidgetFactory factory in new IWidgetFactory[] { new LightFactory(), new DarkFactory() })
Console.WriteLine(new Dialog(factory).Render());
interface IButton { string Draw(); }
interface ICheckbox { string Draw(); }
interface IWidgetFactory
{
IButton Button();
ICheckbox Checkbox();
}
sealed class LightButton : IButton { public string Draw() => "light button"; }
sealed class LightCheckbox : ICheckbox { public string Draw() => "light checkbox"; }
sealed class DarkButton : IButton { public string Draw() => "dark button"; }
sealed class DarkCheckbox : ICheckbox { public string Draw() => "dark checkbox"; }
sealed class LightFactory : IWidgetFactory
{
public IButton Button() => new LightButton();
public ICheckbox Checkbox() => new LightCheckbox();
}
sealed class DarkFactory : IWidgetFactory
{
public IButton Button() => new DarkButton();
public ICheckbox Checkbox() => new DarkCheckbox();
}
sealed class Dialog(IWidgetFactory factory)
{
public string Render() => factory.Button().Draw() + " + " + factory.Checkbox().Draw();
}
light button + light checkbox
dark button + dark checkboxDarkFactory도 같은 생성 약속을 구현하므로 Dialog는 두 제품을 같은 방식으로 쓴다. 이 예제의 테마 일관성은 각 팩토리 구현이 지키며, 인터페이스 이름만으로 잘못 섞인 제품을 자동 검출하지는 않는다.
같이 보기
각 예제는 독립된 .NET 10 콘솔 프로그램이다.