본문 바로가기

Team Thoth/DevMax

[디자인(설계)] 팩토리 메소드 패턴과 Class 타입을 활용 객체 생성 시 참조 관계 제거

데브맥스 프레임워크를 개발하고 있습니다. 데브맥스에서 사용하는 기술을 틈틈히 정리 및 공유하려 합니다.

(데브맥스 프레임워크에 대한 소개는 다음에 진행하겠습니다.)


이번 글에서는 팩토리 메소드 패턴(부모 클래스에 알려지지 않은 구체 클래스를 생성하는 패턴)과 클래스 타입을 활용해, 여러가지 객체 생성 시 참조 관계를 제거하는 방법을 소개합니다.


배경

1) 메인UI에서 여러가지 서브UI를 동적으로 생성하고 싶다.

2) 메인UI 소스에서 서버UI 소스 참조시 서브UI가 많아질 수록 메인UI 소스가 복잡해 진다.


과제

메인UI에서 서브UI Id로 서브UI 객체를 생성할 수 있어야 한다.

메인UI 소스에서 서브UI 소스를 직접 참조하지 않아야 한다.

서브UI가 늘어나도 메인UI 소스는 변경되지 않아야 한다.


방안

팩토리 메소드 패턴 활용

Class 타입을 이용해 확장성 제공


구현

아키텍처

위 그림과 같이 서브UI들은 ClassTypeFactory에 등록하고, 메인UI는 ClassTypeFactory를 참조해 객체를 생성하는 구조이다.


클래스 타입 지정

  TViewItemClass = class of TControl;
  TViewItemClassInfo = record
    Id: string;
    ViewItemClass: TViewItemClass;

    constructor Create(AId: string; AItemClass: TViewItemClass);
  end;
서브UI(TViewItem)의 클래스 타입(TViewItemClass)을 지정한다.
클래스의 종류는 현재 TControl로 정의 되어 있지만 폼(TForm), 여러분들이 만든 객체(또는 컴포넌트)  등으로 정의해 사용할 수 있다.
클래스 정보 구조체를 선언한다.

Factory 클래스

type
  TViewItemClass = class of TControl;
  TViewItemClassInfo = record
    Id: string;
    ViewItemClass: TViewItemClass;

    constructor Create(AId: string; AItemClass: TViewItemClass);
  end;

  TViewItemFactory = class
  private
    class var FInstance: TViewItemFactory;
  private
    FViewItems: TDictionary;
    function GetList: TArray;
  public
    constructor Create;
    destructor Destroy; override;

    procedure Regist(AId: string; AItemClass: TViewItemClass);
    function GetClass(AId: string): TViewItemClass;
    function CreateControl(AId: string): TControl;

    property List: TArray read GetList;

    class function Instance: TViewItemFactory;
    class procedure ReleaseInstance;
  end;

{ TViewItemFactory }

class function TViewItemFactory.Instance: TViewItemFactory;
begin
  if not Assigned(FInstance) then
    FInstance := Create;
  Result := FInstance;
end;

class procedure TViewItemFactory.ReleaseInstance;
begin
  if Assigned(FInstance) then
    FInstance.Free;
end;

constructor TViewItemFactory.Create;
begin
  FViewItems := TDictionary.Create;
end;

function TViewItemFactory.CreateControl(AId: string): TControl;
var
  ItemClass: TViewItemClass;
begin
  ItemClass := GetClass(AId);
  if not Assigned(ItemClass) then
    Exit(nil);
  Result := ItemClass.Create(nil);
end;

destructor TViewItemFactory.Destroy;
begin
  FViewItems.Free;

  inherited;
end;

function TViewItemFactory.GetClass(AId: string): TViewItemClass;
var
  Info: TViewItemClassInfo;
begin
  Result := nil;
  if FViewItems.TryGetValue(AId, Info) then
    Result := Info.ViewItemClass;
end;

function TViewItemFactory.GetList: TArray;
begin
  Result := FViewItems.Values.ToArray;
end;

procedure TViewItemFactory.Regist(AId: string; AItemClass: TViewItemClass);
begin
  FViewItems.Add(AId, TViewItemClassInfo.Create(AId, AItemClass));
end;

initialization
finalization
  TViewItemFactory.ReleaseInstance;

Factory 클래스가 싱글톤 패턴이 적용되어 약간 복잡하다

주요 메소드는 클래스 타입을 등록하는 Regist 메소드와 클래스 타입을 제공하는 GetClass 메소드이다.


서브UI에서 클래스 등록

type
  TFrame1 = class(TFrame)
    Label1: TLabel;
    Circle1: TCircle;
  private
    { Private declarations }
  public
    { Public declarations }
  end;

implementation

uses
  ClassTypeFactoryType;

{$R *.fmx}

initialization
  TViewItemFactory.Instance.Regist('프레임1', TFrame1);

서브UI에서는 ClassTypeFactory(TViewItemFactory)에 id('프레임1')와 클래스 타입(TFrame1)을 등록한다.


메인UI에서 서브UI 생성

uses
  ClassTypeFactoryType;

{$R *.fmx}

procedure TForm1.Button1Click(Sender: TObject);
var
  Info: TViewItemClassInfo;
  Item: TListBoxItem;
begin
  for Info in TViewItemFactory.Instance.List do
  begin
    Item := TListBoxItem.Create(ListBox1);
    Item.Parent := ListBox1;
    Item.Text := Info.Id;
  end;
end;

procedure TForm1.Button2Click(Sender: TObject);
var
  Id: string;
  ItemClass: TViewItemClass;
begin
  if Assigned(FActiveControl) then
    FActiveControl.Free;

  Id := ListBox1.Selected.Text;
  ItemClass := TViewItemFactory.Instance.GetClass(Id);
  FActiveControl := ItemClass.Create(Self);
  FActiveControl.Parent := Layout1;
  FActiveControl.Align := TAlignLayout.Client;
end;

메인UI에서는 ClassTypeFactory에 등록된 서브UI를 참조할 수 있다.

메인UI는 Id 값으로 서브UI 클래스(TViewItemClass)를 가져와 동적으로 생성할 수 있다.

메인UI 소스에서는 서브UI 소스를 참조하지 않는다.(결합도를 낮출 수 있다.)


샘플코드

다운로드 : 01_ClassTypeFactory.zip

활용 : https://github.com/devgear/DevMax/blob/master/View/DevMax.View.Factory.pas



PS

짧은 시간을 할애해 정보를 자주 올리기 위해 두서없이 글을 작성했습니다. 혹시 이해되지 않는 내용이 있으면 댓글로 질문주시면 앞으로 보강하도록 하겠습니다.