TDataSetProxy هو مكون مجمع لمكون مجموعة بيانات دلفي الكلاسيكية. يسمح باستبدال أي مجموعة بيانات بمجموعة بيانات مزيفة (جدول في الذاكرة). يمكن استخدام الوكيل لفصل فئة الأعمال عن مجموعات البيانات، ويكون هذا الفصل مفيدًا عندما يلزم وضع رمز العمل في أداة اختبار آلية (اختبارات الوحدة).
إلهام . تعتمد الفكرة على نمط Proxy GoF ونمط Active Record، اللذين حددهما مارتن فاولر في كتاب أنماط هندسة تطبيقات المؤسسات
يعد نمط وكيل DataSet مفيدًا أثناء استخراج منطق الأعمال. قد يكون هذا مفيدًا بشكل خاص لتحسين المشاريع القديمة والمترابطة بشكل كبير. عندما يعتمد كود الإنتاج على بيانات SQL واتصال SQL، فمن الصعب حقًا كتابة اختبارات الوحدة لمثل هذا الكود.
يؤدي استبدال مجموعة البيانات بالوكلاء إلى تقديم مستوى تجريد جديد يمكن أن يسهل كلا من: مجموعات بيانات SQL في كود الإنتاج ومجموعات بيانات الذاكرة في مشروع الاختبار. يتمتع الوكيل بواجهة مشابهة جدًا (قائمة الأساليب) لمجموعة البيانات الكلاسيكية، مما يساعد في سهولة الترحيل. ستسمح مجموعات البيانات المزيفة بالتحقق من (تأكيد) كود الإنتاج دون الاتصال بقاعدة البيانات.
يمنح DataSet Proxy مع مشروعين مصاحبين (DataSet Generator وDelphi Command Pattern) للمطورين الفرصة لتقديم اختبارات الوحدة باستخدام عمليات إعادة البناء الآمنة.
يعد وكيل مجموعة البيانات حلاً مؤقتًا وبعد تغطية التعليمات البرمجية بالاختبارات، يمكن للمهندسين تطبيق عمليات إعادة هيكلة أكثر تقدمًا: فصل التعليمات البرمجية أو جعلها أكثر قابلية للتركيب وإعادة الاستخدام. كواحد من وكيل إعادة البناء هذا، يمكن استبداله بأمان بكائن DAO أو بهياكل بيانات النموذج.
جنبًا إلى جنب مع مطوري الكود وتحسين الجودة، سيتعلمون كيفية كتابة كود أنظف أو كيفية استخدام نهج الاختبار الأول والعمل بشكل أفضل.
المشروع الداعم:
مشروع | جيثب الريبو |
---|---|
مولد مجموعة البيانات | 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 ' );
يعد Generator App for FireDAC أداة بديلة تم إنشاؤها في الغالب لأغراض العرض التوضيحي. من الناحية العملية، قد يكون استخدام هذه الأداة أقل فائدة من استخدام مولد المكونات مباشرة. تطبيق Generator مخصص لأغراض التدريب والتدريب. لمزيد من المعلومات، راجع: Generator App for 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
.
يمكن تقسيم بقية أساليب 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 | يستخدم النص لكل مسافة بادئة للكود، القيمة الافتراضية هي مسافتين |
DataSetAccess | ( dsaNoAccess ، dsaGenComment ، dsaFullAccess ) | يحدد الوصول إلى مجموعة بيانات الوكيل الداخلية: الوصول الكامل = يتم إنشاء خاصية للقراءة فقط للحصول على حق الوصول. لا يوجد خيار وصول افتراضي وموصى به |
لإنشاء فئة poxy يمكنك استخدام طريقة التنفيذ، ولكن قبل الاتصال بها يجب عليك إعداد كافة الخيارات وخصائص DataSet
. بعد الاتصال، سيتم تخزين التعليمات البرمجية التي تم Execute
في قائمة TStringList
الداخلية التي يمكن الوصول إليها من خلال خاصية Code
. انظر نموذج التعليمات البرمجية أدناه:
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 Clipboard. انظر العينات أدناه:
TDataProxyGenerator.SaveToFile(
' src/Proxy.Employee ' ,
fdqEmployees,
' TEmployeeProxy ' ,
' '
fnsLowerCaseF);
TDataProxyGenerator.SaveToClipboard(
fdqEmployees,
' TEmployeeProxy ' ,
' '
fnsLowerCaseF);
هذا المشروع هو نتيجة لسنوات عديدة وخبرة فرق متعددة. وجدت هذه الفرق أن نهج دلفي الكلاسيكي القائم على الأحداث ليس أقل إنتاجية فحسب، بل إنه خطير أيضًا على المطورين والمديرين والعملاء.
يبدو العمل مع RDBMS (خوادم SQL) في دلفي مثمرًا وبسيطًا للغاية. يقوم المطور بإسقاط مكون Query
، وإدخال أمر SQL، وتعيين الخاصية النشطة، وربط جميع عناصر التحكم المدركة لقاعدة البيانات بالاستعلام، وبذلك تكون قد انتهيت... أوشكت على الانتهاء، تقريبًا ولكن في الواقع بعيدًا عن أن تكون جاهزًا لتسليم التطبيق.
يمكن أن يؤدي استخدام مطور الأنماط المرئية البسيط هذا إلى كشف بيانات خادم SQL وتعديلها بسرعة كبيرة. في الواقع، ما يبدو بسيطًا عند التسول، يصبح أمرًا صعبًا. وبمرور الوقت، يقوم المهندسون بإنشاء المزيد والمزيد من مجموعات البيانات والأحداث، وإلغاء تجزئة تدفق الأعمال ومزج العرض التقديمي والتكوين ورمز المجال. يصبح المشروع أكثر فأكثر فوضويًا ومقترنًا. بعد بضع سنوات، يفقد المديرون والمطورون السيطرة على مثل هذا المشروع: لا يمكن تحديد الخطط والمواعيد النهائية، ويعاني العملاء من أخطاء غير متوقعة وغريبة، وتتطلب التغييرات البسيطة ساعات طويلة من العمل.
يتطلب استبدال مجموعة البيانات الكلاسيكية بالوكيل بعض الوقت للتعلم والتحقق من صحتها أثناء العمل. قد يبدو هذا الأسلوب غريبًا بعض الشيء بالنسبة لمطوري دلفي، ولكن من السهل تبنيه وتعلمه. بفضل تحفيز الإدارة وفريق تدريب كبار المهندسين، سيعتمدون بشكل أسرع استخراج التعليمات البرمجية واستبدال مجموعات البيانات بتقنية الوكلاء.
يعتبر نهج الوكيل المحدد هنا بمثابة تقنية إعادة هيكلة بسيطة وآمنة مخصصة لتطبيق VCL الكلاسيكي المبني بطريقة EDP (البرمجة المستندة إلى الأحداث). باستخدام هذا الحل بطريقة التطور، يمكن استخراج أجزاء صغيرة ولكنها مهمة من كود العمل وتغطيتها باختبارات الوحدة. بعد مرور بعض الوقت، ومع وجود شبكة أمان أفضل (تغطية اختبارات الوحدة)، يمكن للمهندسين تبادل الوكلاء مع OOP DAOs وتحسين التعليمات البرمجية بشكل أكبر باستخدام عمليات إعادة البناء المتقدمة والأنماط المعمارية.
تتضمن عملية التحديث الخطوات التالية:
انظر إلى المثال الذي يوضح مسار الترحيل لمشروع VCL القديم باستخدام TDataSetProxy. سنبدأ بالطريقة الكلاسيكية المحددة في النموذج:
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 ;
يلاحظ! الحل الموضح أعلاه هو ممارسة سيئة، ولكن لسوء الحظ غالبا ما يستخدم من قبل مطوري دلفي. الهدف من استخدام TDataProxy هو تحسين هذه الحالة وفصل منطق العمل عن التصور.
تقوم هذه الطريقة بتحميل البيانات من قاعدة بيانات SQL باستخدام fdqBook
TFDQuery. يتم إنشاء كائن من فئة TBook
لكل صف، ويتم ملء حقوله بقيم مجموعة البيانات والتحقق من صحتها. نظرًا لأنه يتم تخزين كائنات TBook
في عنصر التحكم TListBox
، الذي يمتلكها أيضًا، يجب أن تقوم هذه الطريقة بتحريرها أولاً.
نستبدل مجموعة البيانات بالكائن الوكيل. بالإضافة إلى ذلك، نقوم بتحديث الكود عن طريق تغيير حلقة while-not-eof
الكلاسيكية باستخدام أسلوب ForEach
الوظيفي. وفي الوقت نفسه، نقدم نسخة أكثر أمانًا للوصول إلى قيم الحقول. من الممكن فصل هذه المرحلة إلى 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
.
يحتوي هذا الوكيل على مجموعة داخلية من كتب fBookList
والتي تُستخدم لملء ListBox. في تلك اللحظة، قمنا بنقل هذا الرمز إلى فئة وكيل مجموعة البيانات لتقليل عدد التغييرات، ولكن يجب نقله إلى الفئة المناسبة:
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
)function CreateMockTableBook
في (وحدة Data.Mock.Book.pas
)