TDataSetProxy는 클래식 Delphi 데이터세트 컴포넌트를 위한 래퍼 컴포넌트입니다. 모든 데이터세트를 가짜 데이터세트(인메모리 테이블)로 교체할 수 있습니다. 프록시를 사용하여 데이터 세트에서 비즈니스 클래스를 분리할 수 있습니다. 이러한 분리는 비즈니스 코드를 자동화된 테스트 도구(단위 테스트)에 넣어야 할 때 유용합니다.
영감 . 아이디어는 Martin Fowler가 책 Patterns of Enterprise Application Architecture 에서 정의한 Proxy GoF 패턴 및 Active Record 패턴을 기반으로 합니다.
DataSet Proxy 패턴은 비즈니스 로직 추출 중에 유용합니다. 이는 결합도가 높은 레거시 프로젝트를 개선하는 데 특히 유용할 수 있습니다. 프로덕션 코드가 SQL 데이터 및 SQL 연결에 의존하는 경우 해당 코드에 대한 단위 테스트를 작성하는 것은 정말 어렵습니다.
데이터세트를 프록시로 바꾸면 프로덕션 코드의 SQL 데이터세트와 테스트 프로젝트의 메모리 데이터세트를 모두 촉진할 수 있는 새로운 추상화 수준이 도입됩니다. 프록시에는 기존 데이터 세트와 매우 유사한 인터페이스(메서드 목록)가 있어 쉽게 마이그레이션할 수 있습니다. 가짜 데이터 세트를 사용하면 데이터베이스에 연결하지 않고도 프로덕션 코드를 확인(어설션)할 수 있습니다.
DataSet Proxy는 두 개의 동반 프로젝트(DataSet Generator, Delphi Command Pattern)와 함께 개발자에게 안전한 리팩토링을 통해 단위 테스트를 도입할 수 있는 기회를 제공합니다.
데이터 세트 프록시는 임시 솔루션이며 테스트를 통해 코드를 다룬 후 엔지니어는 고급 리팩토링을 적용할 수 있습니다. 즉, 코드를 분리하거나 코드를 더욱 구성 및 재사용 가능하게 만들 수 있습니다. 이러한 리팩토링 중 하나로서 프록시는 DAO 개체 또는 모델 데이터 구조로 안전하게 대체될 수 있습니다.
코드 및 품질 개선과 함께 개발자는 더 깔끔한 코드를 작성하는 방법이나 테스트 우선 접근 방식을 사용하여 더 잘 작업하는 방법을 배우게 됩니다.
지원 프로젝트:
프로젝트 | GitHub 레포 |
---|---|
데이터세트 생성기 | https://github.com/bogdanpolak/dataset-generator |
프로젝트에는 기본 클래스 TDataSetProxy
의 소스 코드와 두 가지 유형의 프록시 생성기가 포함되어 있습니다.
src/Comp.Generator.DataProxy.pas
TDataSetProxy
에서 상속된 프록시 클래스를 포함하는 유닛tools/generator-app
컴포넌트 TDataProxyGenerator
엔지니어가 프로덕션 코드에서 데이터세트를 종료하기 위한 프록시를 생성하려고 할 때 유용합니다. 이는 두 단계로 구성된 쉬운 작업입니다. (1) 사용 섹션에 구성 요소 단위를 추가하고, (2) 데이터 집합을 사용하여 코드를 찾고 생성기 메서드를 호출합니다.
현재 생산 코드:
aBooksDataSet := fDBConnection.ConstructSQLDataSet(
aOwner, APPSQL_SelectBooks);
dbgridBooks.DataSource.Dataset := aBooksDataSet;
주입된 생성기 코드:
TDataProxyGenerator.SaveToFile( ' ../../src/Proxy.Books ' ,
aBooksDataSet, ' TBookProxy ' );
FireDAC용 생성기 앱 은 주로 데모 목적으로 만들어진 대체 도구입니다. 실제로 이 도구를 사용하는 것은 구성 요소 생성기를 직접 사용하는 것보다 덜 유용할 수 있습니다. Generator App은 코칭 및 훈련 목적으로 사용됩니다. 자세한 내용은 FireDAC용 생성기 앱 - 사용자 가이드를 확인하세요.
type
TBookProxy = class (TDatasetProxy)
private
fISBN :TWideStringField;
fTitle :TWideStringField;
fReleseDate :TDateField;
fPages :TIntegerField;
fPrice :TBCDField;
protected
procedure ConnectFields ; override;
public
property ISBN :TWideStringField read fISBN;
property Title :TWideStringField read fTitle;
property ReleseDate :TDateField read fReleseDate;
property Pages :TIntegerField read fPages;
property Price :TBCDField read fPrice;
end ;
procedure TBookProxy.ConnectFields ;
begin
Assert(fDataSet.Fields.Count = 5 );
fISBN := fDataSet.FieldByName( ' ISBN ' );
fTitle := fDataSet.FieldByName( ' Title ' );
fReleseDate := fDataSet.FieldByName( ' ReleseDate ' );
fPages := fDataSet.FieldByName( ' Pages ' );
fPrice := fDataSet.FieldByName( ' Price ' );
end ;
DataSetProxy 컴포넌트는 클래식 TDataSet 컴포넌트와 거의 동일한 메소드를 갖는 프록시 클래스입니다. 개발자는 프로덕션 코드에 위험도가 낮은 변경 사항만 적용하여 DataSet 구성 요소를 이 프록시로 쉽게 교체할 수 있습니다. 프로덕션 코드 관점에서 볼 때 변경 사항은 작고 그다지 중요하지 않지만 테스트 관점에서는 개발자가 경량 메모리 데이터 세트를 사용하도록 프록시를 재구성할 수 있기 때문에 이는 근본적인 변화입니다.
대부분의 TDataSetProxy
메소드는 TDataSet의 한 번 복제일 뿐입니다. 누락된 항목을 한 번 추가하여 이 방법 세트를 쉽게 확장하거나 새로운 고유 방법을 구축할 수 있습니다. 이 프록시 메소드는 Append
, Edit
, Cancel
, Delete
, Close
, Post
, RecordCount
, First
, Last
, Eof
, Next
, Prior
, EnableControls
, DisableControls
, Locate
, Lookup
, Refresh
등입니다. 문서화 및 이 메소드 사용법은 TDataSet
클래스에 대한 표준 Delphi 문서화와 동일합니다.
나머지 TDataSetProxy
메소드는 프록시 설정 메소드(구성)와 프록시 도우미 메소드(클래식 데이터세트 기능 확장)의 두 그룹으로 나눌 수 있습니다.
procedure TDataModule1.OnCreate (Sender: TObject);
begin
fOrdersProxy := TOrdersProxy.Create(fOwner);
fOrdersDataSource := fOrdersProxy.ConstructDataSource;
end ;
procedure TDataModule1.InitOrders (aYear, aMonth: word);
begin
fOrdersProxy.WithFiredacSQL( FDConnection1,
' SELECT OrderID, CustomerID, OrderDate, Freight ' +
' FROM {id Orders} WHERE OrderDate between ' +
' :StartDate and :EndDate ' ,
[ GetMonthStart(aYear, aMonth),
GetMonthEnd(aYear, aMonth) ],
[ftDate, ftDate])
.Open;
fOrdersInitialized := True;
end ;
procedure TDataModule1.InitOrders (aDataSet: TDataSet);
begin
fOrdersProxy.WithDataSet(aDataSet).Open;
fOrdersInitialized := True;
end ;
TDataSetProxy
컴포넌트의 현재 릴리스에는 예제로 구현된 도우미 메소드가 하나만 포함되어 있습니다. 개발자는 팀 코딩 방식에 따라 이 컬렉션을 확장할 수 있습니다. 확장 프록시 클래스는 상속을 사용하는 것이 좋습니다. 기존 ForEach
도우미 메서드의 샘플 사용법:
function TDataModule.CalculateTotalOrders ( const aCustomerID: string): Currency;
begin
Result := 0 ;
fOrdersProxy.ForEach(procedure
begin
if fOrdersProxy.CustomerID. Value = aCustomerID then
Result := Result + fOrdersProxy.GetTotalOrderValue;
end ;
end ;
Comp.Generator.DataProxy.pas
SaveToFile
SaveToClipboard
Execute
Code
DataSet
GeneratorMode
DataSetAccess
FieldNamingStyle
NameOfUnit
NameOfClass
IndentationText
옵션 | 가치 | 설명 |
---|---|---|
GeneratorMode | ( pgmClass , pgmUnit ) | 클래스 헤더와 구현만 생성하거나 클래스가 포함된 전체 유닛을 생성합니다. |
NameOfUnit | String | 유닛 헤더를 생성하는 데 사용되는 생성된 유닛의 이름 |
NameOfClass | String | 생성된 프록시 클래스의 이름 |
FieldNamingStyle | ( fnsUpperCaseF , fnsLowerCaseF ) | 클래스 필드 이름 지정 방법 결정: 대문자 F 접미사 또는 소문자 사용 |
IndentationText | String | 각 코드 들여쓰기에 텍스트를 사용하며 기본값은 공백 2개입니다. |
DataSetAccess | ( dsaNoAccess , dsaGenComment , dsaFullAccess ) | 내부 프록시 데이터 세트에 대한 액세스를 정의합니다. 전체 액세스 = 읽기 전용 속성이 생성되어 액세스가 가능합니다. 접근 불가 옵션이 기본값이며 권장됩니다. |
폭시 클래스를 생성하려면 Execute 메소드를 사용할 수 있지만 호출하기 전에 모든 옵션과 DataSet
속성을 설정해야 합니다. Execute
호출한 후 생성된 코드는 Code
속성을 통해 액세스할 수 있는 내부 TStringList
에 저장됩니다. 아래 샘플 코드를 참조하세요.
aProxyGenerator:= TDataProxyGenerator.Create(Self);
try
aProxyGenerator.DataSet := fdqEmployees;
aProxyGenerator.NameOfUnit := ' Proxy.Employee ' ;
aProxyGenerator.NameOfClass := ' TEmployeeProxy ' ;
aProxyGenerator.IndentationText := ' ' ;
aProxyGenerator.Execute;
Memo1.Lines := aProxyGenerator.Code;
finally
aProxyGenerator.Free;
end ;
프록시 클래스를 생성하는 훨씬 쉽고 간단한 방법은 생성기 클래스 메서드인 SaveToFile
또는 SaveToClipboard
사용하는 것입니다. 그 이름은 그 기능을 이해하기에 충분히 의미가 있습니다. SaveToFile은 유닛 전체를 생성하여 파일에 쓰고, SaveToClipboard는 클래스만 생성하여 Windows 클립보드에 씁니다. 아래 샘플을 참조하세요.
TDataProxyGenerator.SaveToFile(
' src/Proxy.Employee ' ,
fdqEmployees,
' TEmployeeProxy ' ,
' '
fnsLowerCaseF);
TDataProxyGenerator.SaveToClipboard(
fdqEmployees,
' TEmployeeProxy ' ,
' '
fnsLowerCaseF);
이 프로젝트는 수년간의 여러 팀 경험의 결과입니다. 이 팀은 전통적인 이벤트 기반 Delphi 접근 방식이 생산성이 낮을 뿐만 아니라 개발자, 관리자 및 고객에게 위험하기까지 한다는 사실을 발견했습니다.
Delphi에서 RDBMS(SQL 서버)로 작업하는 것은 매우 생산적이고 단순해 보입니다. 개발자는 Query
구성 요소를 삭제하고, SQL 명령을 입력하고, Active 속성을 설정하고, 모든 DB 인식 컨트롤을 쿼리에 연결하면 완료됩니다. 거의 완료되었지만 실제로는 애플리케이션 제공 준비가 거의 완료되지 않았습니다.
개발자는 이 간단한 시각적 패턴을 사용하여 SQL 서버 데이터를 매우 빠르게 노출하고 수정할 수 있습니다. 실제로 구걸하는 것은 간단해 보이지만 후자는 어려운 일이 됩니다. 시간 내에 엔지니어는 점점 더 많은 데이터 세트와 이벤트를 생성하고 비즈니스 흐름을 조각 모음하고 프레젠테이션, 구성 및 도메인 코드를 혼합합니다. 프로젝트는 점점 더 지저분해지고 결합됩니다. 몇 년이 지나면 관리자와 개발자는 이러한 프로젝트에 대한 통제력을 잃게 됩니다. 계획과 마감일을 정량화할 수 없으며, 고객은 예상치 못한 이상한 버그로 어려움을 겪고 있으며, 간단한 변경에는 많은 시간이 소요됩니다.
클래식 데이터세트를 프록시로 교체하려면 실제로 학습하고 검증하는 데 시간이 필요합니다. 이 접근 방식은 Delphi 개발자에게는 약간 이상해 보일 수 있지만 채택하고 배우기는 쉽습니다. 관리 동기와 수석 엔지니어 코칭 팀을 통해 코드 추출을 더 빠르게 채택하고 데이터 세트를 프록시 기술로 대체할 것입니다.
여기서 정의된 프록시 접근 방식은 EDP(Event Driven 프로그래밍) 방식으로 구축된 클래식 VCL 애플리케이션 전용의 간단하고 안전한 리팩토링 기술입니다. 이 솔루션을 진화 방식으로 사용하면 작지만 비즈니스 코드의 중요한 부분을 추출하고 단위 테스트를 통해 다룰 수 있습니다. 시간이 지나면서 더 나은 안전망(단위 테스트 적용 범위)을 통해 엔지니어는 프록시를 OOP DAO로 교체하고 고급 리팩토링 및 아키텍처 패턴을 사용하여 코드를 더 개선할 수 있습니다.
현대화 프로세스에는 다음 단계가 포함됩니다.
TDataSetProxy를 사용하는 레거시 VCL 프로젝트의 마이그레이션 경로를 보여주는 예를 살펴보세요. 다음 형식으로 정의된 고전적인 방법부터 시작하겠습니다.
procedure TFormMain.LoadBooksToListBox ();
var
aIndex: integer;
aBookmark: TBookmark;
aBook: TBook;
isDatePrecise: boolean;
begin
ListBox1.ItemIndex := - 1 ;
for aIndex := 0 to ListBox1.Items.Count - 1 do
ListBox1.Items.Objects[aIndex].Free;
ListBox1.Clear;
aBookmark := fdqBook.GetBookmark;
try
fdqBook.DisableControls;
try
while not fdqBook.Eof do
begin
aBook := TBook.Create;
ListBox1.AddItem(fdqBook.FieldByName( ' ISBN ' ).AsString + ' - ' +
fdqBook.FieldByName( ' Title ' ).AsString, aBook);
aBook.ISBN := fdqBook.FieldByName( ' ISBN ' ).AsString;
aBook.Authors.AddRange(BuildAuhtorsList(
fdqBook.FieldByName( ' Authors ' ).AsString));
aBook.Title := fdqBook.FieldByName( ' Title ' ).AsString;
aBook.ReleaseDate := ConvertReleaseDate(
fdqBook.FieldByName( ' ReleaseDate ' ).AsString);
aBook.Price := fdqBook.FieldByName( ' Price ' ).AsCurrency;
aBook.PriceCurrency := fdqBook.FieldByName( ' Currency ' ).AsString;
ValidateCurrency(aBook.PriceCurrency);
fdqBook.Next;
end ;
finally
fdqBook.EnableControls;
end
finally
fdqBook.FreeBookmark(aBookmark);
end ;
end ;
알아채다! 위에 제시된 솔루션은 나쁜 습관이지만 안타깝게도 Delphi 개발자가 자주 사용합니다. TDataProxy를 사용하는 목적은 이 상태를 개선하고 시각화에서 비즈니스 로직을 분리하는 것입니다.
이 방법은 fdqBook
TFDQuery를 사용하여 SQL 데이터베이스에서 데이터를 로드하는 것입니다. TBook
클래스의 객체는 각 행에 대해 생성되고 해당 필드는 데이터 세트 값으로 채워지고 유효성이 검사됩니다. TBook
객체는 이를 소유한 TListBox
컨트롤에 저장되기 때문에 이 메서드는 먼저 해당 객체를 해제해야 합니다.
데이터 세트를 프록시 객체로 대체합니다. 또한 기능적인 ForEach
메서드를 사용하여 전통적인 while-not-eof
루프를 변경하여 코드를 현대화하고 있습니다. 동시에 필드 값에 액세스하는 보다 안전한 변형을 도입하고 있습니다. 이 단계를 3개의 개별 단계로 분리할 수 있지만 이 문서에서는 콘텐츠를 간결하게 유지해야 합니다.
procedure TFormMain.LoadBooksToListBox ();
var
aIndex: integer;
aBook: TBook;
begin
ListBox1.ItemIndex := - 1 ;
for aIndex := 0 to ListBox1.Items.Count - 1 do
ListBox1.Items.Objects[aIndex].Free;
ListBox1.Clear;
fProxyBooks.ForEach(
procedure
begin
aBook := TBook.Create;
ListBox1.AddItem(fProxyBooks.ISBN. Value + ' - ' +
fProxyBooks.Title. Value , aBook);
aBook.ISBN := fProxyBooks.ISBN. Value ;
aBook.Authors.AddRange(
BuildAuhtorsList(fProxyBooks.Authors. Value ));
aBook.Title := fProxyBooks.Title. Value ;
aBook.ReleaseDate := ConvertReleaseDate(
fProxyBooks.ReleaseDate. Value );
aBook.Price := fProxyBooks.Price.AsCurrency;
aBook.PriceCurrency := fProxyBooks.Currency. Value ;
ValidateCurrency(aBook.PriceCurrency);
end );
end ;
코드는 더 읽기 쉽고 안전하지만 여전히 형식을 유지합니다. 이제 테스트를 활성화하기 위해 이를 제거하고 모든 종속성에서 분리해야 합니다.
중요한 아키텍처 결정부터 시작해야 합니다. 현재 코드에는 데이터를 저장하는 TBook
과 데이터를 처리하는 TBookProxy
두 개의 유사한 클래스가 있습니다. 이들 클래스 중 어느 클래스가 다른 클래스에 종속되는지 결정하는 것이 중요합니다. TBook
모델 레이어의 일부이며 데이터 액세스 개체에 대해 인식해서는 안 됩니다.
procedure TForm1.LoadBooksToListBox ();
begin
ListBox1.Clear;
fProxyBooks.LoadAndValidate;
fProxyBooks.FillStringsWithBooks(ListBox1.Items);
end ;
마지막으로 양식 방법이 멋지고 명확해 보입니다. 이는 우리가 올바른 방향으로 가고 있다는 좋은 신호입니다. 추출되어 데이터세트 프록시로 이동된 코드는 이전과 거의 비슷합니다.
procedure TBooksProxy.LoadAndValidate ;
var
aBook: TBook;
isDatePrecise: boolean;
begin
fBooksList.Clear;
ForEach(
procedure
begin
aBook := TBook.Create;
fBooksList.Add(aBook);
aBook.ISBN := ISBN. Value ;
aBook.Authors.AddRange(
BuildAuhtorsList(Authors. Value ));
aBook.Title := Title. Value ;
aBook.ReleaseDate := ConvertReleaseDate(
ReleaseDate. Value , isDatePrecise);
aBook.IsPreciseReleaseDate := isDatePrecise;
aBook.Price := Price.AsCurrency;
aBook.PriceCurrency := Currency. Value ;
ValidateCurrency(aBook.PriceCurrency);
end );
end ;
이 코드와 함께 데이터 변환 및 유효성 검사를 담당하는 모든 종속 메서드( BuildAuhtorsList
, ConvertReleaseDate
및 ValidateCurrency
를 이동해야 했습니다.
이 프록시에는 ListBox를 채우는 데 사용되는 책 fBookList
의 내부 컬렉션이 포함되어 있습니다. 그 순간 변경 횟수를 줄이기 위해 이 코드를 데이터세트 프록시 클래스로 옮겼지만 문자 그대로 적절한 클래스로 이동해야 합니다.
procedure TBooksProxy.FillStringsWithBooks (
aStrings: TStrings);
var
aBook: TBook;
begin
aStrings.Clear;
for aBook in fBooksList do
aStrings.AddObject(
aBook.ISBN + ' - ' + aBook.Title, aBook);
end ;
TBookProxy
( Data.Proxy.Book.pas
단위)Data.Mock.Book.pas
단위)의 function CreateMockTableBook