هذه المقالة ستبين كيف يمكن أن تستخدم ال Interface في جعل الكود قابل للتطوير بسهولة Extensibility، وسوف نأخذ مثال على ال Repository Pattern.
في هذه السلسلة سوف نوضح ما يلي:
- الفرق بين ال Abstract Class وال Interface
- دور ال Interface في قابلية الصيانة Maintainbility
- عمل ال Repository باستخدام ال Interface (المقالة الحالية)
- ما هو ال Dynamic Factory
مثال عملي يوضح فائدة ال Interface
مثلاُ لدينا تطبيق نريد تشغيله لدى أكثر من جهاز، وكل جهاز سوف يتم تخزين/قراءة البيانات بطريقة مختلفة (سواء في قاعدة بيانات مختلفة أو في مكان مختلف) أي different data store، وبالتالي نريد بناء التطبيق بحيث يعمل مع هذه ال data store / storages المختلفة
الصورة التالية تبين انواع ال data stores التي يمكن أن تستخدم، بدئاً من قواعد البيانات العلائقية وهناك عدة انظمة فيها، ومروراً بقاعدة بيانات ال NO SQL والملفات النصية وبخدمات الويب سواء SOAP أو RESTful وانتهاءً بتخزين البيانات على ال Cloud، وكل هذه انواع من طرق التخزين التي يمكن تستخدم.
البرنامج سوف يقرأ معلومات المستخدمين Persons وهي الاسم الأول والثاني وبضعه معلومات لكل مستخدم، بالإضافة إلى امكانية اضافة مستخدم جديد، حذف مستخدم، تحديث بيانات مستخدم. هذا العمليات تسمى CRUD وسوف نبين هذا الاختصار بعد قليل.
حالياً للتسهيل سوف يكون المطلوب أن يعمل البرنامج على ثلاثة أنواع من ال data sources:
- قاعدة بيانات SQL Server
- ملفات نصية من نوع CSV
- خدمات الويبمن نوع SOAP
بدون استخدام أي تصميم للكود، فيمكن كتابته مباشرة عن طريق جمل ال If وفحص الطريقة المطلوبة بطريقة تشابه ما يلي
public IEnumerable < Person > ReadData(int source) { List < Person > persons = new List < Person > (); if (source == 1) { // Read from SQL Server Database } else if (source == 2) { // Read from CSV File } else if (source == 3) { // Read from SOAP Web Service } return persons; }
بعد أن قمت بعمل كلاس يمثل ال Person واتبعت نصيحة Programming to Abstraction قمت بعمل ارجاع لل IEnumerable، ولكن داخل الدالة قمت بعمل فحص للقيمة التي تقرأها من المستخدم أو من ملف خارجي وتقوم على اساسها بقراءة البيانات من المكان المطلوبة.
الكود السابق به العديد من المشاكل:
- في حال اضفت أي source جديد سوف تحتاج تضيفها في هذه الدالة، وتضيفها ايضاً في بقية الدوال التي تخزن وتحذف وتحدث لأنها سوف يكون بها نفس الشرط وسوف تكون مكررة في مواضع أخرى.
- الدالة سوف تصبح كبيرة وبالتالي لها مسؤوليات كثيرة
الحل المناسب والذي يعد من أسهل طريقة للعمل مع عدة data sources مختلفة هو باستخدام ال pattern المعروف بالاسم Repository Pattern والذي يستخدم ال interface بشكل اساسي.
ال Repository Pattern
وهو من طرق التصميم المعروفة Design Pattern ويستخدم لأضافه طبقة من ال Layer of Abstraction، وهذا تعريفه من الكتاب المعروف Patterns of Enterprise Application Architecture للمؤلف Martin Fowler:
Mediates between the domain and data mapping layers using a collection-like interface for accessing domain objects.
لتبسيط التعريف: وهو طبقة وسيطة بين البرنامج وآلية التخزين، وبالتالي كود التطبيق يتعامل مع هذه الطبقة بدلاً من أن يعتمد على الية التخزين مباشرة والتي يمكن أن تتغير، كما في الصورة التالية سوف تجد أن طبقة التخزين Repository الآن وسيطة بين الكود وبين قاعدة البيانات.
فنحن لا نريد التطبيق أن يتصل مباشرة بالقاعدة أو Data source (لأن في تلك الحالة التطبيق يجب أن يعرف كيف سيتعامل مع نوع ال data source المعين مثلاً عمل SQL Query أو القيام بال Web service Call)، وبدلاً من ذلك سوف نضيف طبقة Layer بين التطبيق وال data source وهذه هي ال Repository، وسوف يتصل التطبيق بها بدون أن يعرف كيف ستتعامل ال repository مع ال data source، بعباره أخرى التطبيق فقط سوف ينادي ال contract المطبق في ال repository وهو لا يهتم بأي Implementation.
عملياً قد تكون هناك layers بين ال Application (وهي هنا UI Layer) وال Repository (مثلاً domain layer أو BLL) وهي تتعامل مع ال Repository ولكن للتبسيط ولتوضيح فائدة ال interface في ال Extensibility سوف تعمل ال Application مباشرة مع ال Repository ونتجاهل بعض ال Layers، والفصل الخامس سوف نأخذ مثالاً على ال Layers.
اذاً: التطبيق سوف يعتمد على ال interface وهو يتوقع أي Implementation موجود أن يطبقها قبل أن يتعامل معه، بغض النظر كونه:
- WCF Service Repository للتعامل مع SOAP service
- أو CSV Service Repository للتعامل مع ال TEXT FILE
- أو SQL Service Repository للتعامل مع RDBMS
أو أي نوع آخر (مثلاً يتعامل مع ال Azure SQL) طالما يطبق نفس ال Interface، فكل شيء سيعمل مباشرة بدون تغيير أي كود
إذا لم تضح لك الصورة بعد، فلا تقلق وسوف تتضح لك كل الأمور مع المثال، لنوضح الآن ماذا نقصد بال CRUD
ماذا نعني بال CRUD
هي مجموعه من الدوال طالما تكون موجودة في أي تطبيق يريد التعامل مع أي جدول في القاعدة أو عموماً مع أي Data Storage، وال CRUD هي اختصار للعمليات الإضافة Create، القراءة Read، التحديث Update، الحذف Delete.
مثال ال Repository باستخدام ال Interface
لنبدأ الآن بالبرمجة، وسوف نقوم بتعريف المستخدم Person
public class Person { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public DateTime BirthDate { get; set; } }
بعد ذلك سوف نقوم بعمل Repository لكي يتعامل مع ال CRUD الخاص بال Person وسوف نعرف ال Contract كما يلي:
public interface IPersonRepository { void AddPerson(Person person); IEnumerable<Person> GetPeople(); Person GetPerson(string firstName); void UpdatePerson(int personId, Person updatedPerson); void DeletePerson(string firstName); void UpdatePeople(IEnumerable<Person> updatedPeopele); }
هنا أنشئنا ال IPersonRepository وفيها دالة للإضافة، ودالة للقراءة سواء لشخص أو مجموعه، وأيضاً دالة للتحديث، وأخيراً دالة لحذف الشخص، وهكذا نقوم بعمل كل ال operations التي نريدها على البيانات.
لاحظ العائد من دالة ارجاع الأشخاص هو IEnumerable وبالتالي كل الكلاسات التي تطبق هذه ال Interface تستطيع ارجاع List، Array أو أي كائن يطبق ال IEnumerable، ونفس الامر في دالة التحديث والتي يتم تمرير مجموعه من الأشخاص (وهنا نطبق ال Program to abstraction).
الآن نريد أن نتعامل مع أكثر من Data Source والبرنامج سوف يجلب البيانات من:
- جلب من ال Service Repository
- جلب من ال CSV Repository
- جلب من SQL Repository
سوف يتم وضع تعريف الكود الخاص بجلب ال People أي دالة واحدة GetPeople فقط في كل كلاس حتى نركز على الفكرة العامة وليس على حول كيف يمكن ادخال أو حذف البيانات من CSV أو Service.
التعامل مع الملفات CSV Repository
سوف نقوم بعمل كلاس جديد وليكن اسمه CVSRepository لكي يتم جلب البيانات من الملف، بالطبع هذا الكلاس سوف يطبق ال IPersonRepository كما يلي:
public class CSVRepository: IPersonRepository { public void AddPerson(Person person) { throw new NotImplementedException(); } public IEnumerable<Person> GetPeople() { List<Person> persons = new List<Person>(); // fill persons from file return persons; } public Person GetPerson(string firstName) { throw new NotImplementedException(); } public void UpdatePerson(int personId, Person updatedPerson) { throw new NotImplementedException(); } public void DeletePerson(string firstName) { throw new NotImplementedException(); } public void UpdatePeople(IEnumerable<Person> updatedPeopele) { throw new NotImplementedException(); } }
سوف نقوم بكتابة كود جلب البيانات من الملف، والملف اسمه data.txt وبنيته بهذا الشكل:
سوف يكون كود استخراج البيانات منها كالتالي (لا داعي للتركيز حول الكيفية وليست هي الطريقة الأفضل):
public IEnumerable<Person> GetPeople() { List<Person> people = new List<Person>(); string[] lines = File.ReadAllLines("data.txt"); foreach (string line in lines) { string[] tokens = line.Split(','); Person person = new Person { Id = int.Parse(tokens[0].Trim()), FirstName = tokens[1].Trim(), LastName = tokens[2].Trim(), BirthDate = DateTime.ParseExact(tokens[3].Trim(), "dd/MM/yyyy", CultureInfo.InvariantCulture) }; people.Add(person); } return people; }
بهذا الشكل سوف يتم تعريف بقية الدوال الموجودة في ال CSVRepository وبالتالي يستطيع هذا الكلاس اضافة، عرض، تحديث، حذف الأشخاص من الملف (يمكنك تطبيقها كتمرين في البرمجة).
التعامل مع قاعدة البيانات SQL Repository
سوف يتم الآن تطبيق الكلاس الذي يتعامل مع قاعدة البيانات ويقوم بنفس المهام ولكن التخزين سوف يكون على القاعدة. وسنقوم بعمل Implement لل Interface والتركيز على دالة GetPeople ويبقى تطبيق البقية اليك كتمرين أيضاً.
ولكتابة الدالة التي تجلب من قاعدة البيانات يمكن أن نستخدم ال ADO.NET ونقوم بفتح الاتصال والاستعلام بجمل SQL وجلب البيانات أو يمكن أن نستخدم أي من ال ORMs الموجودة مثلاً LINQ To SQL أو Entity Framework، وفي هذا المثال سوف نستخدم الطريقة التقليدية.
public IEnumerable<Person> GetPeople() { List<Person> people = new List<Person>(); using (SqlConnection connection = new SqlConnection(ConfigurationManager.ConnectionStrings["PeopleDB"].ToString())) { connection.Open(); using (SqlCommand cmd = new SqlCommand("SELECT * FROM People", connection)) { using (SqlDataReader reader = cmd.ExecuteReader()) { if (reader != null) { while (reader.Read()) { Person person = new Person() { Id = int.Parse(reader["Id"].ToString()), FirstName = reader["FirstName"].ToString(), LastName = reader["LastName"].ToString(), BirthDate = DateTime.Parse(reader["BirthDate"].ToString()) }; people.Add(person); } } } } } return people; }
يتم جلب البيانات من قاعدة البيانات التي تم تحديد ال Connection String لها في ملف ال App.config:
<connectionStrings> <add name="PeopleDB" connectionString="Data Source=.;Initial Catalog=PeopleDB;Integrated Security=True" providerName="System.Data.SqlClient"/> </connectionStrings>
المهم حالياً أن القاعدة PeopleDB موجودة وبها جدول اسمه People والبيانات التي توجد بها:
بقية الدوال الأخرى يمكنك أن تقوم بتطبيقها ايضاً (للحذف والتعديل والاضافة).
التعامل مع الويب سيرفس Service Repository
هناك أنواع كثيرة لتطبيق الويب سيرفس في ال .NET، فمثلاً يمكن أن تكون WCF أو Web API أو حتى ASMX، ولكن الهدف من هذا الموضوع هو توضيح كيف أن ال Interfaces يجعلك تتعامل مع الجميع بطريقة واحدة وليس شرح كيفية عمل ال Web service، لذلك سوف يتم جلب البيانات من List في الذاكرة فقط.
للنظر الأن لتعريف ال Service Repository ولاحظ أنها ارجعت بيانات ثابتة على فرض أنها تأتي من خدمة ويب.
public IEnumerable<Person> GetPeople() { // simulate call to web service then retreive the result return new List<Person> { new Person {Id = 1, FirstName = "Bahi", LastName="Salem", BirthDate=DateTime.Now}, new Person {Id = 2, FirstName = "Omar", LastName="Omar", BirthDate=DateTime.Now}, new Person {Id = 3, FirstName = "Khalid", LastName="Nour", BirthDate=DateTime.Now}, new Person {Id = 4, FirstName = "Ahmed", LastName="Essa", BirthDate=DateTime.Now} }; }
العمل على ال Solution
انتهينا من كتابة ال Repositories الثلاثة، لكن قبل المضي في استخدامها، سوف نقوم بوضع كل منهم على Project منفصل (من نوع Class Library Project) ومن ثم نضيف ال Assembly الخاصة بكل مشروع إلى البرنامج الذي يحتوي على الدالة main.
لمن لم يستخدم ال Class Project من قبل أو يحتاج لعمل مراجعة فننصح بمراجعة الملحق 1 الآن، وبعدها يكمل هنا، حيث به بعض المعلومات المهمة حول المكتبات وكيفية عملها ومتى تحمل للذاكرة.
ال Class Library هو مشروع ولكن ليس له أي مخرج وانما سيخرج ملف DLL تستطيع استخدامه في مشاريع اخرى، ودائماً يفضل أن تقسم المشروع إلى عدة اقسام مثلاً Class Library Project يحتوي على الكود الخاص بالتعامل مع قواعد البيانات، ومشروع Class Library Project آخر يحتوي على الأشياء العامة التي تحتاجها في مواضع أخرى مثلاً ارسال البريد، عمل معالجة للنصوص، وهكذا تستطيع اعادة استخدامها بسهولة من مشروع لأخر بدون الحاجة لإعادة كتابة نفس الكود مرة اخرى أو القيام بعمليات النسخ واللصق. الفصل الخامس سوف نتحدث بمثال عملي عن التقسيم وال Layers.
شكل المشروع بعد عمل عدة مشاريع Class Libraries سوف يكون كالتالي:
- اولاً المشروع Program وهو Console Application والذي فيه الدالة الرئيسية Main ومنه يعمل البرنامج. ولاحظ أنه Startup Project (تستطيع اختيار أي مشروع بالزر الايمن وتحديد أنه Startup Project، وبالطبع يجب أن يكون Console Project أو Web Project والا فلن يعمل إذا كان Class Library Project)
- المشروع Shared ووضعنا فيه ال Interfaceبالإضافة إلى الكلاس Personوهي الاشياء المشتركة أو الكلاسات التي تمثل ال Domain في المشروع.
- مجلد Repositoryويحتوي على 3 مشاريع وهي التي تعمل على ال Data Sources المختلفة، وتم وضعهم داخل المجلد للتنظيم فقط لا أكثر.
كل من هذه المشاريع الثلاثة سوف تحتاج الوصول إلى ال IPersonRepository وال Person حتى تعمل صحيحاً، لذلك سوف نضيف مشروع ال Shared إلى كل من هذه المشاريع الثلاثة والخطوات كالتالي:
لنبدأ بمشروع CSV والبقية ستكون بنفس الخطوات، سوف تجد قائمة ال References وهي المكتبات التي يتعامل معها هذا المشروع، قم بفتحها واختر Add Reference ثم اذهب ال Solution على اليسار واختر Shared (كما في الصورة التي تليها) وهو المشروع الذي نريد أن يوصل لها ال CSVRepository اليه، وحدده ومن ثم اضغط على OK
هنا لتحديد المشروع:
معلومة: تستطيع اضافة المكتبات عموماً (بالأصح ال Assemblies) بعدة طرق منها:
- ال Solution في حال كانت المكتبة في نفس المشروع (كما هو في مثالنا الآن)
- أو من خلال ال Assemblies وهي المتوفرة في .NET حيث في الوضع الافتراضي لا يتم تحميلها كلها في أي مشروع وانما بضعه Assemblies مهمة
- أو تستطيع تحديد مسارها Browse من جهاز
- أو استخدام ال Nuget Package Manager وهو الأسلوب الأفضل خصوصاً في المكتبات الخارجية Third-Party Libraries.
حالياً سوف نضع ال Shared Assembly في المشروعين الباقين وهم SQLRepo وال ServiceRepo بنفس الطريقة اعلاه.
أخيراً سوف نضيف ال Assembly الخاصة بالمشاريع الثلاثة SQLRepo، ServiceRepo، CSVRepo إلى المشروع Program حتى نستطيع استخدامهم جميعاً عند تشغيل البرنامج.
وكما يتبين الآن أن ال Program يستخدم تلك المكتبات
كود الدالة الرئيسية Main
سوف نسأل المستخدم عما يريده أولاً، ومن ثم بناءً على اختيار المستخدم سوف نقوم بطباعة معلومات الأشخاص ونقوم بإنشاء ال Repository المناسب:
class Program { static void Main(string[] args) { Console.Write("Enter your choice: "); int choice = Convert.ToInt32(Console.ReadLine()); if (choice == 1) { IPersonRepository repo = new CSVRepository(); IEnumerable<Person> people = repo.GetPeople(); displayPeople(people); } else if (choice == 2) { IPersonRepository repo = new SQLRepository(); IEnumerable<Person> people = repo.GetPeople(); displayPeople(people); } else if (choice == 3){ IPersonRepository repo = new ServiceRepository(); IEnumerable<Person> people = repo.GetPeople(); displayPeople(people); } Console.ReadKey(); } private static void displayPeople(IEnumerable<Person> people) { foreach (var person in people) { Console.WriteLine(person.Id + " " + person.FirstName + " " + person.LastName); } } }
الآن لو جربت البرنامج سوف تجد أن الخيار يطبع البيانات التي توجد على ال CSV بينما 2 يطبع على ال SQL و3 يطبع التي توجد على ال Service. وهكذا توحدت ال API ولكن ال Implementation يختلف على حسب نوع ال Repository. طبعاً سوف يكون هذا الأمر مطبق على جميع الدوال وليس فقط GetPeople.
وبهذا الشكل فإن المشروع يتعامل مع هذه ال Repositories الثلاثة، وكل ذلك من خلال ال Interface، رائع!
حذف الكود المكرر باستخدام ال Factory Method
لاحظ وجود الكود المكرر داخل جمل ال IF في الكود السابق، والكود متشابه فقط الإختلاف في سطر عمل ال Implementation وهو شيء واحد فقط يتغير في الثلاثة دوال، لذلك كما قلنا سابقاً هناك تصميم غير جيد، ويجب التحسين Refactoring، وسوف نقوم بعمل Refactor لهذه الأكواد وبالتالي يتم مسح التكرار، وسوف نستخدم Factory Method للقيام بذلك.
سوف نضيف كلاس Repository Factory وفيه دالة ترجع نوع ال IPersonRepsotiry وتأخذ قيمة نوع ال repo المطلوب وباستخدام جملة if أو switch يتم ارجاع ال Implementationالمطلوب، وفي حال أرسلت نوع خاطئ سوف يتم عمل throwلل exception كما في الشكل التالي:
للاستزادة عن ال Static Factory Method يمكن قراءة هذه المقالة وهي بالجافا ولكن المبدأ نفسه هل مللت من دالة البناء Constructor؟
public class RepositoryFactory { public static IPersonRepository GetRepository(string type) { IPersonRepository repo = null; switch (type) { case "Service": repo = new ServiceRepository(); break; case "SQL": repo = new SQLRepository(); break; case "CSV": repo = new CSVRepository(); break; default: throw new ArgumentException("Invalid Repository Type"); } return repo; } }
الآن الدالة الرئيسية Main سوف تكون بهذا الشكل:
class Program { static void Main(string[] args) { Console.Write("Enter your choice: "); int choice = Convert.ToInt32(Console.ReadLine()); if (choice== 1) FetchData("CSV"); else if (choice == 2) FetchData("SQL"); else if (choice == 3) FetchData("Service"); Console.ReadKey(); } private static void FetchData(string type) { IPersonRepository repo = RepositoryFactory.GetRepository(type); IEnumerable<Person> people = repo.GetPeople(); displayPeople(people); } private static void displayPeople(IEnumerable<Person> people) { foreach (var person in people) { Console.WriteLine(person.Id + " " + person.FirstName + " " + person.LastName); } } }
كل شيء يعمل كما كان بالضبط، ولكن بكود أفضل وبدون تكرار. رائع!
الدالة FetchData لا تهتم بنوع ال type الذي يرجعه ال Factory سواء كان csv أو sql فهذا الكود يعمل بنفس الطريقة (لأنه يعمل باستخدام ال contract).
أيضاً تستطيع إضافة أي repo جديد بسهولة، فقط سوف تغير في ال factory وتضيف النوع الجديد الذي ترغب فيه واعادة ترجمة المشروع. وهكذا بال interface تحقق فكرة سهولة ال Extensibility.
فقط هناك عيب بسيط، ماذا لو كنا نريد أن يعمل البرنامج على Repository واحد فقط، ومن خلال تغيير في ملف خارجي Configuration يتم تحديد نوع ال Repository، مثلاً في جهاز العميل الأول سوف يعمل البرنامج باستخدام ال CSV File ويخزن ويستعرض البيانات. بينما نفس البرنامج يعمل باستخدام ال SQL Database فقط بتغيير اسم ال Repository في ملف ال Configuration بدون تغيير أي جزئية في الكود وإعادة ترجمة المشروع؟
بمعنى أن تكون الدالة الرئيسية بهذا الشكل
class Program { static void Main(string[] args) { FetchData(); Console.ReadKey(); } private static void FetchData() { IPersonRepository repo = RepositoryFactory.GetRepository(); IEnumerable<Person> people = repo.GetPeople(); displayPeople(people); } private static void displayPeople(IEnumerable<Person> people) { foreach (var person in people) { Console.WriteLine(person.Id + " " + person.FirstName + " " + person.LastName); } } }
وعلى حسب القيمة التي توجد في ال Configuration File يتم استخدام ال Repository المناسب.
بالإمكان عمل ذلك عن طريق اضافة فكرة ال Dynamic Loading وبالتالي لا تحتاج لعمل hard coded لل Implementations في ال Factory Method، وسيتم تحميل ال Implementation تلقائياً وقت التشغيل إذا أردت على حسب ما يتم تحديده في الملف.
خلاصة
- تعلمنا مفهوم ال Repository pattern وما هي ال CRUD
- تعلمنا كيف تقوم بعمل custom interface وتقوم بتطبيقه في عدة أماكن
- لإنشاء ال Repository
- يمكن استخدام ال Factory لأنشاء ال Repositoryبطريقة ال hard coded أي Compile time
- أو من خلال ال Dynamic loading لل Repository في وقت التشغيلكما سيأتي الفصل المقبل.
يعطيك العافيه, سؤال اخي هل الكود البرنامج مرفوع علئ ال Github ؟
[…] post عمل ال Repository Pattern باستخدام ال Interface appeared first on […]