Skip to content

6. البنى ثلاثية الطبقات

6.1. Introduction

لنعد إلى أحدث إصدار من تطبيق حساب الضرائب:


using System;

namespace Chap3 {
    class Program {
        static void Main() {
            // برنامج تفاعلي لحساب الضرائب
            // يكتب المستخدم ثلاث بيانات على لوحة المفاتيح: متزوج nbEnfants الراتب
            // ثم يعرض البرنامج الضريبة المستحقة
...

            // إنشاء كائن IImpot
            IImpot impot = null;
            try {
                // إنشاء كائن IImpot
                impot = new FileImpot("DataImpotInvalide.txt");
            } catch (FileImpotException e) {
                // عرض خطأ
...
                // إيقاف البرنامج
                Environment.Exit(1);
            }
            // حلقة لا نهائية
            while (true) {
                // يُطلب إدخال معلمات حساب الضريبة
                Console.Write("Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :");
                string paramètres = Console.ReadLine().Trim();
...
                // المعلمات صحيحة - يتم حساب الضريبة
                Console.WriteLine("Impot=" + impot.calculer(marié == "o", nbEnfants, salaire) + " euros");
                // المكلف التالي
            }//while
        }
    }
}

تتضمن الحلول السابقة عمليات معالجة تقليدية في البرمجة:

  1. استرجاع البيانات المخزنة في الملفات وقواعد البيانات، ... الأسطر 12-21
  2. التفاعل مع المستخدم، الأسطر 26 (الإدخالات) و29 (العروض)
  3. استخدام خوارزمية مهنية، السطر 29

أثبتت الممارسة أن عزل هذه المعالجات المختلفة في فئات منفصلة يحسن قابلية صيانة التطبيقات. وتكون بنية التطبيق الذي تم تنظيمه بهذه الطريقة كما يلي:

يُطلق على هذه البنية اسم "بنية ثلاثية الطبقات"، وهي ترجمة من الإنجليزية "three tier architecture". يشير مصطلح "ثلاث طبقات" عادةً إلى بنية يكون فيها كل طبقة على جهاز مختلف. عندما تكون الطبقات على نفس الجهاز، تصبح البنية بنية "ثلاثية الطبقات".

  • الطبقة [metier] هي التي تحتوي على قواعد العمل الخاصة بالتطبيق. بالنسبة لتطبيقنا لحساب الضرائب، هذه هي القواعد التي تسمح بحساب ضريبة دافع الضرائب. تحتاج هذه الطبقة إلى بيانات لتعمل:
  • شرائح الضريبة، وهي بيانات تتغير كل عام
  • عدد الأطفال، والحالة الاجتماعية، والراتب السنوي للمكلف

في المخطط أعلاه، يمكن أن تأتي البيانات من مكانين:

  • طبقة الوصول إلى البيانات أو [dao] (DAO = Data Access Object) للبيانات المسجلة بالفعل في ملفات أو قواعد بيانات. قد يكون هذا هو الحال هنا بالنسبة لشرائح الضرائب كما كان الحال في الإصدار السابق من التطبيق.
  • طبقة واجهة المستخدم أو [ui] (UI = واجهة المستخدم) للبيانات التي يدخلها المستخدم أو التي تُعرض عليه. قد يكون هذا هو الحال هنا بالنسبة لعدد الأطفال والحالة الاجتماعية والراتب السنوي للمكلف
  • بشكل عام، تتولى الطبقة [dao] الوصول إلى البيانات الدائمة (الملفات وقواعد البيانات) أو غير الدائمة (الشبكة وأجهزة الاستشعار، ...).
  • أما الطبقة [ui]، فهي تتولى التفاعلات مع المستخدم إن وجد.
  • أصبحت الطبقات الثلاث مستقلة بفضل استخدام الواجهات.

سنستأنف تطبيق [Impots] الذي تمت دراسته عدة مرات من قبل لمنحه بنية ثلاثية الطبقات. للقيام بذلك، سنقوم بدراسة الطبقات [ui, metier, dao] واحدة تلو الأخرى، بدءًا من الطبقة [dao]، وهي الطبقة التي تتولى البيانات الدائمة.

قبل ذلك، يتعين علينا تحديد واجهات الطبقات المختلفة لتطبيق [Impots].

6.2. واجهات تطبيق [Impots]

تذكر أن الواجهة تحدد مجموعة من توقيعات الطرق. وتقوم الفئات التي تنفذ الواجهة بتزويد هذه الطرق بالمحتوى.

لنعد إلى بنية الطبقات الثلاث لتطبيقنا:

في هذا النوع من البنية، غالبًا ما يكون المستخدم هو من يبادر. فهو يقدم طلبًا في [1] ويتلقى ردًا في [8]. وهذا ما يُسمى بدورة الطلب - الرد. لنأخذ مثال حساب ضريبة أحد دافعي الضرائب. سيتطلب ذلك عدة خطوات:

  1. سيتعين على الطبقة [ui] أن تطلب من المستخدم عدد أطفاله وحالته الاجتماعية وراتبه السنوي. هذه هي العملية [1] المذكورة أعلاه.
  2. وبعد ذلك، ستطلب الطبقة [ui] من الطبقة الوظيفية إجراء حساب الضريبة. ولهذا الغرض، سترسل إليها البيانات التي تلقتها من المستخدم. هذه هي العملية [2].
  3. تحتاج الطبقة [metier] إلى بعض المعلومات لإنجاز عملها: شرائح الضريبة. وستطلب هذه المعلومات من الطبقة [dao] عبر المسار [3, 4, 5, 6]. [3] هي الطلب الأولي و [6] هي الرد على هذا الطلب.
  4. وبعد حصولها على جميع البيانات التي تحتاجها، تقوم الطبقة [metier] بحساب الضريبة.
  5. يمكن للطبقة [metier] الآن الرد على الطلب المقدم من الطبقة [ui] في (ب). هذا هو المسار [7].
  6. ستقوم الطبقة [ui] بتنسيق هذه النتائج ثم عرضها على المستخدم. هذا هو المسار [8].
  7. يمكننا أن نتخيل أن المستخدم يقوم بإجراء محاكاة للضرائب ويرغب في حفظها. سيستخدم المسار [1-8] للقيام بذلك.

نرى في هذا الوصف أن الطبقة تستخدم موارد الطبقة الموجودة على يمينها، وليس أبدًا تلك الموجودة على يسارها. لنفترض وجود طبقتين متجاورتين:

تقوم الطبقة [A] بإرسال طلبات إلى الطبقة [B]. في الحالات الأبسط، يتم تنفيذ الطبقة بواسطة فئة واحدة. يتطور التطبيق بمرور الوقت. وبالتالي، قد تحتوي الطبقة [B] على فئات تنفيذ مختلفة [B1, B2, ...]. إذا كانت الطبقة [B] هي الطبقة [dao]، فقد يكون لهذه الأخيرة تنفيذ أولي [B1] الذي يبحث عن البيانات في ملف. بعد بضع سنوات، قد نرغب في وضع البيانات في قاعدة بيانات. عندها سنقوم بإنشاء فئة تنفيذ ثانية [B2]. إذا كانت الطبقة [A] في التطبيق الأولي تعمل مباشرة مع الفئة [B1]، فسنضطر إلى إعادة كتابة جزء من كود الطبقة [A]. لنفترض على سبيل المثال أننا كتبنا في الطبقة [A] شيئًا مثل ما يلي:

1
2
3
B1 b1=new B1(...);
..
b1.getData(...);
  • السطر 1: يتم إنشاء مثيل للفئة [B1]
  • السطر 3: يتم طلب البيانات من هذه المثيل

إذا افترضنا أن فئة التنفيذ الجديدة [B2] تستخدم طرقًا بنفس توقيع فئة [B1]، فسيكون من الضروري تغيير كل [B1] إلى [B2]. وهذا هو الحالة المثالية، وهي غير مرجحة إلى حد ما إذا لم يتم الانتباه إلى توقيعات الطرق هذه. في الواقع، غالبًا ما لا تحتوي الفئتان [B1] و [B2] على نفس توقيعات الطرق، وبالتالي يجب إعادة كتابة جزء كبير من الطبقة [A] بالكامل.

يمكن تحسين الوضع إذا تم وضع واجهة بين الطبقتين [A] و [B]. وهذا يعني تجميد توقيعات الطرق التي تقدمها الطبقة [B] إلى الطبقة [A] في واجهة. وبذلك يصبح المخطط السابق كما يلي:

لم تعد الطبقة [A] تتواصل مباشرة مع الطبقة [B] بل مع واجهتها [IB]. وبالتالي، في كود الطبقة [A]، لا تظهر فئة التنفيذ [Bi] للطبقة [B] إلا مرة واحدة، عند تنفيذ واجهة [IB]. وبذلك، يتم استخدام الواجهة [IB] وليس فئة التنفيذ الخاصة بها في الكود. يصبح الكود السابق كما يلي:

1
2
3
IB ib=new B1(...);
..
ib.getData(...);
  • السطر 1: يتم إنشاء مثيل [ib] الذي ينفذ واجهة [IB] عن طريق إنشاء مثيل لفئة [B1]
  • السطر 3: يتم طلب البيانات من مثيل [ib]

والآن، إذا استبدلنا التنفيذ [B1] للطبقة [B] بتنفيذ [B2]، وكان هذان التنفيذان يتوافقان مع نفس الواجهة [IB]، فإنه يجب تعديل السطر 1 فقط من الطبقة [A] دون غيره. وهذه ميزة كبيرة تبرر بحد ذاتها الاستخدام المنهجي للواجهات بين طبقتين.

يمكننا الذهاب إلى أبعد من ذلك وجعل الطبقة [A] مستقلة تمامًا عن الطبقة [B]. في الكود أعلاه، يمثل السطر 1 مشكلة لأنه يشير بشكل ثابت إلى الفئة [B1]. سيكون من المثالي أن تتمكن الطبقة [A] من الحصول على تنفيذ للواجهة [IB] دون الحاجة إلى تسمية فئة. سيكون ذلك متسقًا مع مخططنا أعلاه. نرى هنا أن الطبقة [A] تتعامل مع الواجهة [IB] ولا نرى سببًا يجعلها بحاجة إلى معرفة اسم الفئة التي تنفذ هذه الواجهة. هذا التفصيل غير مفيد للطبقة [A].

يتيح إطار عمل Spring (http://www.springframework.org) الحصول على هذه النتيجة. تتطور البنية السابقة على النحو التالي:

ستسمح الطبقة العرضية [Spring] لطبقة ما بالحصول، عن طريق التكوين، على مرجع للطبقة الموجودة على يمينها دون الحاجة إلى معرفة اسم فئة تنفيذ الطبقة. سيكون هذا الاسم موجودًا في ملفات التكوين وليس في كود C#. يأخذ كود C# للطبقة [A] الشكل التالي:

1
2
3
IB ib; // تم التهيئة بواسطة Spring
..
ib.getData(...);
  • السطر 1: مثيل [ib] ينفذ واجهة [IB] للطبقة [B]. يتم إنشاء هذا المثيل بواسطة Spring استنادًا إلى المعلومات الموجودة في ملف التكوين. سيتولى Spring إنشاء:
    • المثيل [b] الذي ينفذ الطبقة [B]
    • المثيل [a] الذي ينفذ الطبقة [A]. سيتم تهيئة هذا المثيل. سيتلقى الحقل [ib] أعلاه قيمة المرجع [b] للكائن الذي ينفذ الطبقة [B]
  • السطر 3: يتم طلب البيانات من المثيل [ib]

نرى الآن أن فئة التنفيذ [B1] للطبقة B لا تظهر في أي مكان في كود الطبقة [A]. عندما يتم استبدال التنفيذ [B1] بتنفيذ جديد [B2]، لن يتغير شيء في كود فئة [A]. سنقوم ببساطة بتغيير ملفات تكوين Spring لإنشاء مثيل [B2] بدلاً من [B1].

يوفر الثنائي Spring وواجهات C# تحسينًا حاسمًا في صيانة التطبيقات من خلال جعل طبقاتها منفصلة عن بعضها البعض. هذه هي الحل الذي سنستخدمه لإصدار جديد من التطبيق [Impots].

لنعد إلى بنية الطبقات الثلاث لتطبيقنا:

في الحالات البسيطة، يمكننا البدء من الطبقة [metier] لاكتشاف واجهات التطبيق. لتعمل، تحتاج إلى بيانات:

  • متوفرة بالفعل في الملفات أو قواعد البيانات أو عبر الشبكة. يتم توفيرها بواسطة الطبقة [dao].
  • غير متوفرة بعد. يتم توفيرها في هذه الحالة من خلال الطبقة [ui] التي تحصل عليها من مستخدم التطبيق.

ما هي الواجهة التي يجب أن توفرها الطبقة [dao] للطبقة [metier]؟ ما هي التفاعلات الممكنة بين هاتين الطبقتين؟ يجب أن توفر الطبقة [dao] البيانات التالية للطبقة [metier]:

  • شرائح الضريبة

في تطبيقنا، تستخدم الطبقة [dao] البيانات الموجودة ولكنها لا تنشئ بيانات جديدة. قد يكون تعريف واجهة الطبقة [dao] كما يلي:


using Entites;

namespace Dao {
    public interface IImpotDao {
        // شرائح الضريبة
        TrancheImpot[] TranchesImpot{get;}
    }
}
  • السطر 3: سيتم وضع الطبقة [dao] في مساحة الأسماء [Dao]
  • السطر 6: تحدد واجهة IImpotDao الخاصية TranchesImpot التي ستوفر شرائح الضريبة لطبقة [métier].
  • السطر 1: يستورد مساحة الأسماء التي تم فيها تعريف البنية TrancheImpot:

namespace Entites {
    // شريحة ضريبية
    public struct TrancheImpot {
        public decimal Limite { get; set; }
        public decimal CoeffR { get; set; }
        public decimal CoeffN { get; set; }
    }
}

لنعد إلى بنية الطبقات الثلاث لتطبيقنا:

ما هي الواجهة التي يجب أن تقدمها الطبقة [metier] إلى الطبقة [ui]؟ دعونا نذكر التفاعلات بين هاتين الطبقتين:

  1. تطلب الطبقة [ui] من المستخدم عدد أطفاله وحالته الاجتماعية وراتبه السنوي. هذه هي العملية [1] المذكورة أعلاه.
  2. وبعد ذلك، ستطلب الطبقة [ui] من الطبقة المهنية حساب عدد المقاعد. ولهذا الغرض، سترسل إليها البيانات التي تلقتها من المستخدم. هذه هي العملية [2].

قد يكون تعريف واجهة الطبقة [metier] كما يلي:


namespace Metier {
    interface IImpotMetier {
        int CalculerImpot(bool marié, int nbEnfants, int salaire);
    }
}
  • السطر 1: سنضع كل ما يتعلق بطبقة [metier] في مساحة الأسماء [Metier].
  • السطر 2: لا تحدد واجهة IImpotMetier سوى طريقة واحدة: وهي الطريقة التي تسمح بحساب ضريبة دافع الضرائب بناءً على حالته الاجتماعية وعدد أطفاله وراتبه السنوي.

نحن ندرس أول تطبيق لهذه البنية الطبقية.

6.3. تطبيق نموذجي - الإصدار 4

6.3.1. مشروع Visual Studio

سيكون مشروع Visual Studio كما يلي:

  • [1]: يحتوي المجلد [Entites] على الكائنات المشتركة بين الطبقات [ui, metier, dao]: البنية TrancheImpot، الاستثناء FileImpotException.
  • [2]: يحتوي المجلد [Dao] على فئات وواجهات الطبقة [dao]. سنستخدم تنفيذين للواجهة IImpotDao: الفئة HardwiredImpot التي تمت دراستها في الفقرة 4.10 و FileImpot التي تمت دراستها في الفقرة 5.8.
  • [3]: يحتوي المجلد [Metier] على الفئات والواجهات الخاصة بطبقة [metier]
  • [4]: يحتوي الملف [Ui] على فئات الطبقة [ui]
  • [5]: يحتوي الملف [DataImpot.txt] على شرائح الضريبة المستخدمة من قبل التنفيذ FileImpot للطبقة [dao]. تم تكوين [6] ليتم نسخه تلقائيًا إلى مجلد تشغيل المشروع.

6.3.2. كيانات التطبيق

لنعد إلى بنية الطبقات الثلاث لتطبيقنا:

نسمي entités الفئات المشتركة بين الطبقات. وهذا هو الحال عمومًا بالنسبة للفئات والهياكل التي تغلف بيانات الطبقة [dao]. وعادةً ما تمتد هذه الكيانات حتى الطبقة [ui].

كيانات التطبيق هي التالية:

البنية TrancheImpot


namespace Entites {
    // شريحة ضريبية
    public struct TrancheImpot {
        public decimal Limite { get; set; }
        public decimal CoeffR { get; set; }
        public decimal CoeffN { get; set; }
    }
}

L'exception FileImpotException


using System;

namespace Entites {
    public class FileImpotException : Exception {
        // رموز الأخطاء
        [Flags]
        public enum CodeErreurs { Acces = 1, Ligne = 2, Champ1 = 4, Champ2 = 8, Champ3 = 16 };

        // رمز خطأ
        public CodeErreurs Code { get; set; }

        // الشركات المصنعة
        public FileImpotException() {
        }
        public FileImpotException(string message)
            : base(message) {
        }
        public FileImpotException(string message, Exception e)
            : base(message, e) {
        }
    }
}

ملاحظة: لا تكون الفئة FileImpotException مفيدة إلا إذا تم تنفيذ الطبقة [dao] بواسطة الفئة FileImpot.

6.3.3. الطبقة [dao]

لنتذكر واجهة الطبقة [dao]:


using Entites;

namespace Dao {
    public interface IImpotDao {
        // شرائح الضريبة
        TrancheImpot[] TranchesImpot{get;}
    }
}

سنقوم بتنفيذ هذه الواجهة بطريقتين مختلفتين.

أولاً مع الفئة HardwiredImpot التي تمت دراستها في الفقرة 4.10:


using System;
using Entites;

namespace Dao {
    public class HardwiredImpot : IImpotDao {

        // جداول البيانات اللازمة لحساب الضريبة
        decimal[] limites = { 4962M, 8382M, 14753M, 23888M, 38868M, 47932M, 0M };
        decimal[] coeffR = { 0M, 0.068M, 0.191M, 0.283M, 0.374M, 0.426M, 0.481M };
        decimal[] coeffN = { 0M, 291.09M, 1322.92M, 2668.39M, 4846.98M, 6883.66M, 9505.54M };
        // شرائح الضريبة
        public TrancheImpot[] TranchesImpot { get; private set; }

        // المنشئ
        public HardwiredImpot() {
            // إنشاء جدول شرائح الضريبة
            TranchesImpot = new TrancheImpot[limites.Length];
            // التعبئة
            for (int i = 0; i < TranchesImpot.Length; i++) {
                TranchesImpot[i] = new TrancheImpot { Limite = limites[i], CoeffR = coeffR[i], CoeffN = coeffN[i] };
            }
        }
    }// الفئة
}// مساحة الاسم
  • السطر 5: تنفذ الفئة HardwiredImpot الواجهة IImpotDao
  • السطر 12: تنفيذ الخاصية TranchesImpot للواجهة IImpotDao. هذه الخاصية هي خاصية تلقائية. وهي تنفذ الطريقة get للخاصية TranchesImpot للواجهة IImpotDao. كما تم إعلان طريقة set كطريقة خاصة، وبالتالي داخلية للفئة، حتى يتمكن منشئ الأسطر 15-22 من تهيئة مصفوفة شرائح الضريبة.

سيتم أيضًا تنفيذ الواجهة IImpotDao بواسطة الفئة FileImpot التي تمت دراستها في الفقرة 5.8:


using System;
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using Entites;

namespace Dao {
    class FileImpot : IImpotDao {

        // ملف البيانات
        public string FileName { get; set; }

        // شرائح الضريبة
        public TrancheImpot[] TranchesImpot { get; private set; }

        // المنشئ
        public FileImpot(string fileName) {
            // يتم حفظ اسم الملف
            FileName = fileName;
            // البيانات
            List<TrancheImpot> listTranchesImpot = new List<TrancheImpot>();
            int numLigne = 1;
            // استثناء
            FileImpotException fe = null;
            // قراءة محتوى الملف fileName، سطراً سطراً
            Regex pattern = new Regex(@"s*:\s*");
            // في البداية لا توجد أخطاء
            FileImpotException.CodeErreurs code = 0;
            try {
                using (StreamReader input = new StreamReader(FileName)) {
                    while (!input.EndOfStream && code == 0) {
                        // السطر الحالي
                        string ligne = input.ReadLine().Trim();
                        // يتم تجاهل الأسطر الفارغة
                        if (ligne == "")
                            continue;
                        // السطر مقسم إلى ثلاثة حقول مفصولة بـ:
                        string[] champsLigne = pattern.Split(ligne);
                        // هل لدينا 3 حقول؟
                        if (champsLigne.Length != 3) {
                            code = FileImpotException.CodeErreurs.Ligne;
                        }
                        // تحويلات الحقول الثلاثة
                        decimal limite = 0, coeffR = 0, coeffN = 0;
                        if (code == 0) {
                            if (!Decimal.TryParse(champsLigne[0], out limite))
                                code = FileImpotException.CodeErreurs.Champ1;
                            if (!Decimal.TryParse(champsLigne[1], out coeffR))
                                code |= FileImpotException.CodeErreurs.Champ2;
                            if (!Decimal.TryParse(champsLigne[2], out coeffN))
                                code |= FileImpotException.CodeErreurs.Champ3;
                            ;
                        }
                        // خطأ؟
                        if (code != 0) {
                            // يتم تسجيل الخطأ
                            fe = new FileImpotException(String.Format("Ligne n° {0} incorrecte", numLigne)) { Code = code };
                        } else {
                            // يتم حفظ الشريحة الضريبية الجديدة
                            listTranchesImpot.Add(new TrancheImpot() { Limite = limite, CoeffR = coeffR, CoeffN = coeffN });
                            // السطر التالي
                            numLigne++;
                        }
                    }
                }
            } catch (Exception e) {
                // يتم تسجيل الخطأ
                fe = new FileImpotException(String.Format("Erreur lors de la lecture du fichier {0}", FileName), e) { Code = FileImpotException.CodeErreurs.Acces };
            }
            // هل يجب الإبلاغ عن الخطأ؟
            if (fe != null) {
                // يتم إطلاق الاستثناء
                throw fe;
            } else {
                // يتم عرض القائمة listImpot في الجدول tranchesImpot
                TranchesImpot = listTranchesImpot.ToArray();
            }
        }
    }
}
  • تمت دراسة هذا الرمز بالفعل في الفقرة 5.8.
  • السطر 14: الطريقة TranchesImpot للواجهة IImpotDao
  • السطر 76: تهيئة شرائح الضريبة في منشئ الفئة، انطلاقًا من الملف الذي تلقى المنشئ اسمه في السطر 17.

6.3.4. الطبقة [metier]

لنتذكر واجهة هذه الطبقة:


namespace Metier {
    public interface IImpotMetier {
        int CalculerImpot(bool marié, int nbEnfants, int salaire);
    }
}

التنفيذ ImpotMetier لهذه الواجهة هو كما يلي:


using Entites;
using Dao;

namespace Metier {
    public class ImpotMetier : IImpotMetier {

        // طبقة [dao]
        private IImpotDao Dao { get; set; }

        // شرائح الضريبة
        private TrancheImpot[] tranchesImpot;

        // الشركة المطورة
        public ImpotMetier(IImpotDao dao) {
            // التخزين
            Dao = dao;
            // شرائح الضريبة
            tranchesImpot = dao.TranchesImpot;
        }

        // حساب الضريبة
        public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
            // حساب عدد الحصص
            decimal nbParts;
            if (marié)
                nbParts = (decimal)nbEnfants / 2 + 2;
            else
                nbParts = (decimal)nbEnfants / 2 + 1;
            if (nbEnfants >= 3)
                nbParts += 0.5M;
            // حساب الدخل الخاضع للضريبة و"الناتج العائلي"
            decimal revenu = 0.72M * salaire;
            decimal QF = revenu / nbParts;
            // حساب الضريبة
            tranchesImpot[tranchesImpot.Length - 1].Limite = QF + 1;
            int i = 0;
            while (QF > tranchesImpot[i].Limite)
                i++;
            // إرجاع النتيجة
            return (int)(revenu * tranchesImpot[i].CoeffR - nbParts * tranchesImpot[i].CoeffN);
        }//الحساب
    }//تصنيف

}
  • السطر 5: الفئة [Metier] تنفذ واجهة [IImpotMetier].
  • الأسطر 14-19: يجب أن تتعاون الطبقة [metier] مع الطبقة [dao]. لذلك يجب أن يكون لها مرجع إلى الكائن الذي ينفذ الواجهة IImpotDao. ولهذا السبب يتم تمرير هذا المرجع كمعلمة إلى المنشئ.
  • السطر 16: يتم تخزين المرجع الخاص بالطبقة [dao] في الحقل الخاص بالسطر 8
  • السطر 18: انطلاقًا من هذه الإشارة، يطلب المنشئ جدول شرائح الضريبة ويخزن إشارة إليه في الخاصية الخاصة للسطر 8.
  • الأسطر 22-41: تنفيذ الطريقة CalculerImpot للواجهة IImpotMetier. يستخدم هذا التنفيذ جدول شرائح الضرائب الذي تم تهيئته بواسطة المنشئ.

6.3.5. الطبقة [ui]

كانت فئات الحوار مع المستخدم في الإصدارين 2 و 3 متشابهة جدًا. وكانت فئة الإصدار 2 كما يلي:


using System;

namespace Chap2 {
    public class Program {
        static void Main() {
...

            // إنشاء كائن IImpot
            IImpot impot = new HardwiredImpot();

            // حلقة لا نهائية
            while (true) {
...
            }//while
        }
    }
}

وكانت فئة الإصدار 3 كما يلي:


using System;

namespace Chap3 {
    public class Program {
        static void Main() {
...

            // إنشاء كائن IImpot
            IImpot impot = null;
            try {
                // إنشاء كائن IImpot
                impot = new FileImpot("DataImpotInvalide.txt");
            } catch (FileImpotException e) {
                // عرض خطأ
                string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
                Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
                // إيقاف البرنامج
                Environment.Exit(1);
            }
            // حلقة لا نهائية
            while (true) {
...
            }//while
        }
    }
}

الشيء الوحيد الذي تغير هو طريقة إنشاء مثيل الكائن من النوع IImpot الذي يسمح بحساب الضريبة. هذا الكائن يتوافق هنا مع طبقتنا [métier].

بالنسبة لتنفيذ [dao] مع الفئة HardwiredImpot، تكون فئة الحوار كما يلي:


using System;
using Metier;
using Dao;
using Entites;

namespace Ui {
    public class Dialogue2 {
        static void Main() {
...

            // يتم إنشاء الطبقات [metier et dao]
            IImpotMetier metier = new ImpotMetier(new HardwiredImpot());

            // حلقة لا نهائية
            while (true) {
...
                // المعلمات صحيحة - يتم حساب الضريبة
                Console.WriteLine("Impot=" + metier.CalculerImpot(marié == "o", nbEnfants, salaire) + " euros");
                // المكلف التالي
            }//while
        }
    }
}
  • السطر 12: إنشاء مثيلات للطبقات [dao] و [metier]. نذكر أن الطبقة [metier] تحتاج إلى الطبقة [dao].
  • السطر 18: استخدام الطبقة [metier] لحساب الضريبة

بالنسبة لتنفيذ [dao] مع الفئة FileImpot، تكون فئة الحوار كما يلي:


using System;
using Metier;
using Dao;
using Entites;

namespace Ui {
    public class Dialogue {
        static void Main() {
...
            // يتم إنشاء الطبقات [metier et dao]
            IImpotMetier metier = null;
            try {
        // إنشاء طبقة [metier]
                metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
            } catch (FileImpotException e) {
                // عرض الخطأ
                string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
                Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
                // إيقاف البرنامج
                Environment.Exit(1);
            }
            // حلقة لا نهائية
            while (true) {
...
                // المعلمات صحيحة - يتم حساب الضريبة
                Console.WriteLine("Impot=" + metier.CalculerImpot(marié == "o", nbEnfants, salaire) + " euros");
                // المكلف التالي
            }//while
        }
    }
}
  • السطر 11-21: إنشاء مثيلات للطبقات [dao] و [metier]. نظرًا لأن إنشاء مثيل للطبقة [dao] قد يؤدي إلى حدوث استثناء، يتم معالجة هذا الاستثناء
  • السطر 26: استخدام الطبقة [metier] لحساب الضريبة، كما في الإصدار السابق

6.3.6. الخلاصة

أضفت بنية الطبقات واستخدام الواجهات بعض المرونة على تطبيقنا. ويتجلى ذلك بشكل خاص في الطريقة التي تقوم بها الطبقة [ui] بإنشاء مثيلات للطبقات [dao] و [métier]:


        // يتم إنشاء الطبقات [metier et dao]
IImpotMetier metier = new ImpotMetier(new HardwiredImpot());

في إحدى الحالات و:


// يتم إنشاء الطبقات [metier et dao]
            IImpotMetier metier = null;
            try {
        // إنشاء الطبقة [metier]
                metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
            } catch (FileImpotException e) {
                // عرض الخطأ
                string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
                Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
                // إيقاف البرنامج
                Environment.Exit(1);
            }

في الحالة الأخرى. باستثناء معالجة الاستثناء في الحالة 2، فإن إنشاء مثيلات الطبقتين [dao] و [metier] متشابه في كلا التطبيقين. بمجرد إنشاء مثيلات الطبقتين [dao] و [metier]، يكون كود الطبقة [ui] متطابقًا في كلتا الحالتين. ويرجع ذلك إلى أن الطبقة [métier] تتم معالجتها عبر واجهتها IImpotMetier وليس عبر فئة التنفيذ الخاصة بها. إن تغيير الطبقة [metier] أو الطبقة [dao] للتطبيق دون تغيير واجهاتهما سيؤدي دائمًا إلى تغيير الأسطر السابقة فقط في الطبقة [ui].

ومن الأمثلة الأخرى على المرونة التي توفرها هذه البنية تنفيذ الطبقة [métier]:


using Entites;
using Dao;

namespace Metier {
    public class ImpotMetier : IImpotMetier {

        // طبقة [dao]
        private IImpotDao Dao { get; set; }

        // شرائح الضريبة
        private TrancheImpot[] tranchesImpot;

        // الشركة المصنعة
        public ImpotMetier(IImpotDao dao) {
            // التخزين
            Dao = dao;
            // شرائح الضريبة
            tranchesImpot = dao.TranchesImpot;
        }

        // حساب الضريبة
        public int CalculerImpot(bool marié, int nbEnfants, int salaire) {
...
        }//الحساب
    }//الفئة

}

في السطر 14، نرى أن الطبقة [métier] مبنية من مرجع على واجهة الطبقة [dao]. وبالتالي، فإن تغيير تنفيذ هذه الأخيرة لا يؤثر مطلقًا على الطبقة [métier]. ولهذا السبب، تمكن تنفيذنا الوحيد للطبقة [métier] من العمل دون تعديلات مع تنفيذين مختلفين للطبقة [dao].

6.4. تطبيق مثال - الإصدار 5

تستند هذه النسخة الجديدة إلى النسخة السابقة مع إدخال التعديلات التالية:

  • يتم تغليف كل من الطبقتين [métier] و [dao] في DLL واختبارهما باستخدام إطار عمل الاختبارات الفردية NUnit.
  • يتم ضمان تكامل الطبقات بواسطة إطار عمل Spring

في المشاريع الكبيرة، يعمل العديد من المطورين على نفس المشروع. تسهل البنى الطبقية هذا النمط من العمل: نظرًا لأن الطبقات تتواصل فيما بينها عبر واجهات محددة جيدًا، لا يحتاج المطور الذي يعمل على طبقة ما إلى القلق بشأن عمل المطورين الآخرين على الطبقات الأخرى. يكفي فقط أن يلتزم الجميع بالواجهات.

فيما سبق، سيحتاج مطور الطبقة [métier] عند اختبار طبقته إلى تنفيذ للطبقة [dao]. وطالما أن هذا التنفيذ لم يكتمل، يمكنه استخدام تنفيذ وهمي للطبقة [dao] طالما أنه يلتزم بالواجهة IImpotDao. وهذا أيضًا أحد مزايا بنية الطبقات: فالتأخير في الطبقة [dao] لا يمنع اختبار الطبقة [métier]. كما أن التنفيذ الوهمي للطبقة [dao] يتميز أيضًا بأنه غالبًا ما يكون أسهل في التنفيذ من الطبقة الحقيقية [dao] التي قد تتطلب تشغيل SGBD، أو وجود اتصالات شبكية، ...

عندما يتم الانتهاء من طبقة [dao] واختبارها، سيتم توفيرها لمطوري طبقة [métier] في شكل DLL بدلاً من كود المصدر. في النهاية، غالبًا ما يتم تسليم التطبيق في شكل ملف .exe قابل للتنفيذ (ملف الطبقة [ui]) ومكتبات فئات .dll (الطبقات الأخرى).

6.4.1. NUnit

كانت الاختبارات التي أجريت حتى الآن لتطبيقاتنا المختلفة تعتمد على الفحص البصري. كنا نتحقق من أن ما يظهر على الشاشة هو ما كان متوقعًا. هذه طريقة غير قابلة للاستخدام عندما يكون هناك العديد من الاختبارات التي يجب إجراؤها. فالإنسان عرضة للتعب وقدرته على التحقق من الاختبارات تتضاءل مع مرور اليوم. لذا يجب أتمتة الاختبارات بحيث لا تتطلب أي تدخل بشري.

يتطور التطبيق بمرور الوقت. مع كل تحديث، يجب التأكد من أن التطبيق لا «يتراجع» في أدائه، c.a.d، وأنه لا يزال يجتاز اختبارات الأداء التي أجريت عليه عند كتابته لأول مرة. وتسمى هذه الاختبارات «اختبارات عدم التراجع». وقد يتطلب أي تطبيق ذو حجم معقول إجراء مئات الاختبارات. في الواقع، يتم اختبار كل طريقة في كل فئة من فئات التطبيق. ويُطلق على هذه الاختبارات اسم "اختبارات الوحدات". وقد تتطلب هذه الاختبارات مشاركة عدد كبير من المطورين إذا لم يتم أتمتتها.

وقد تم تطوير أدوات لأتمتة الاختبارات. إحدى هذه الأدوات تسمى NUnit. وهي متاحة على الموقع [http://www.nunit.org]:

وقد استُخدمت النسخة 2.4.6 المذكورة أعلاه في إعداد هذا المستند (مارس 2008). ويضع التثبيت أيقونة [1] على سطح المكتب:

يؤدي النقر المزدوج على أيقونة [1] إلى تشغيل واجهة NUnit [2] الرسومية. وهذه الواجهة لا تساعد في أتمتة الاختبارات، حيث نعود مرة أخرى إلى التحقق البصري: يقوم المختبر بالتحقق من نتائج الاختبارات المعروضة في الواجهة الرسومية. ومع ذلك، يمكن أيضًا تنفيذ الاختبارات بواسطة أدوات الدُفعات وتسجيل نتائجها في ملفات XML. هذه هي الطريقة التي تستخدمها فرق التطوير: يتم تشغيل الاختبارات ليلاً ويحصل المطورون على النتيجة في صباح اليوم التالي.

لنستعرض مبدأ اختبارات NUnit باستخدام مثال. أولاً، لنقم بإنشاء مشروع C# جديد من نوع Console Application:

في [1]، نرى références للمشروع. هذه المراجع هي DLL التي تحتوي على الفئات والواجهات المستخدمة في المشروع. تلك المعروضة في [1] مضمنة بشكل افتراضي في كل مشروع C# جديد. لكي نتمكن من استخدام فئات وواجهات إطار العمل NUnit، علينا إضافة [2] مرجع جديد إلى المشروع.

في علامة التبويب .NET أعلاه، نختار المكون [nunit.framework]. المكونات [nunit.*] أعلاه ليست مكونات موجودة افتراضيًا في بيئة .NET. وقد تم إضافتها إلى هناك من خلال التثبيت السابق لإطار العمل NUnit. بمجرد التحقق من صحة إضافة المرجع، يظهر هذا المرجع [4] في قائمة مراجع المشروع.

قبل إنشاء التطبيق، تكون مجلد [bin/Release] الخاص بالمشروع فارغًا. بعد الإنشاء (F6)، يمكن ملاحظة أن مجلد [bin/Release] لم يعد فارغًا:

في [6]، نلاحظ وجود DLL و [nunit.framework.dll]. إن إضافة المرجع [nunit.framework] هي التي تسببت في نسخ ملف DLL هذا إلى مجلد التشغيل. وهذا المجلد هو في الواقع أحد المجلدات التي سيتم استكشافها بواسطة CLR (Common Language Runtime) .NET للعثور على الفئات والواجهات المشار إليها في المشروع.

لنقم بإنشاء فئة اختبار أولية NUnit. للقيام بذلك، نقوم بحذف الفئة [Program.cs] التي تم إنشاؤها افتراضيًا ثم نضيف فئة جديدة [Nunit1.cs] إلى المشروع. سنحذف أيضًا المراجع غير الضرورية [7].

وستكون فئة الاختبار NUnit1 كما يلي:


using System;
using NUnit.Framework;

namespace NUnit {
    [TestFixture]
    public class NUnit1 {
        public NUnit1() {
            Console.WriteLine("constructeur");
        }
        [SetUp]
        public void avant() {
            Console.WriteLine("Setup");
        }
        [TearDown]
        public void après() {
            Console.WriteLine("TearDown");
        }
        [Test]
        public void t1() {
            Console.WriteLine("test1");
            Assert.AreEqual(1, 1);
        }
        [Test]
        public void t2() {
            Console.WriteLine("test2");
            Assert.AreEqual(1, 2, "1 n'est pas égal à 2");
        }
    }
}
  • السطر 6: يجب أن تكون الفئة NUnit1 عامة. لا يتم إنشاء الكلمة الرئيسية public بشكل افتراضي بواسطة Visual Studio. يجب إضافتها.
  • السطر 5: السمة [TestFixture] هي سمة NUnit. وهي تشير إلى أن الفئة هي فئة اختبار.
  • الأسطر 7-9: المنشئ. يتم استخدامه هنا فقط لكتابة رسالة على الشاشة. نريد أن نرى متى يتم تنفيذه.
  • السطر 10: السمة [SetUp] تحدد طريقة يتم تنفيذها قبل كل اختبار وحدة.
  • السطر 14: السمة [TearDown] تحدد طريقة يتم تنفيذها بعد كل اختبار فردي.
  • السطر 18: يحدد السمة [Test] طريقة اختبار. بالنسبة لكل طريقة تم توضيحها بالسمة [Test]، سيتم تنفيذ الطريقة الموضحة [SetUp] قبل الاختبار، وسيتم تنفيذ الطريقة الموضحة [TearDown] بعد الاختبار.
  • السطر 21: إحدى الطرق [Assert.*] المحددة بواسطة إطار العمل NUnit. توجد الطرق [Assert] التالية:
    • [Assert.AreEqual(expression1, expression2)]: تتحقق من أن قيم التعبيرين متساويتان. يتم قبول العديد من أنواع التعبيرات (int، string، float، double، decimal، ...). إذا لم يكن التعبيران متساويين، يتم إلقاء استثناء.
    • [Assert.AreEqual(réel1, réel2, delta)]: يتحقق من أن عددين حقيقيين متساويان بفارق لا يتجاوز delta، c.a.d abs(réel1-réel2)<=delta. يمكننا على سبيل المثال كتابة [Assert.AreEqual(réel1, réel2, 1E-6)] للتحقق من أن قيمتين متساويتين بفارق لا يتجاوز 10-6.
    • [Assert.AreEqual(expression1, expression2, message)] و [Assert.AreEqual(réel1, réel2, delta, message)] هما متغيرات تسمح بتحديد رسالة الخطأ المرتبطة بالاستثناء الذي يتم إطلاقه عند فشل الطريقة [Assert.AreEqual].
    • [Assert.IsNotNull(object)] و [Assert.IsNotNull(object, message)] : يتحقق من أن object لا يساوي null.
    • [Assert.IsNull(object)] و [Assert.IsNull(object, message)] : يتحقق من أن object يساوي null.
    • [Assert.IsTrue(expression)] و [Assert.IsTrue(expression, message)]: يتحقق من أن التعبير يساوي true.
    • [Assert.IsFalse(expression)] و [Assert.IsFalse(expression, message)] : يتحقق من أن التعبير يساوي false.
    • [Assert.AreSame(object1, object2)] و [Assert.AreSame(object1, object2, message)]: يتحقق من أن المراجع object1 و object2 تشير إلى نفس الكائن.
    • [Assert.AreNotSame(object1, object2)] و [Assert.AreNotSame(object1, object2, message)]: يتحقق من أن المراجع object1 و object2 لا تشيران إلى نفس الكائن.
  • السطر 21: يجب أن تنجح المقارنة
  • السطر 26: يجب أن تفشل المقارنة

لنقوم بتكوين المشروع بحيث ينتج عن إنشائه ملف DLL بدلاً من ملف .exe قابل للتنفيذ:

  • في [1]: خصائص المشروع
  • إلى [2, 3]: كنوع للمشروع، نختار [Class Library] (مكتبة الفئات)
  • في [4]: سيؤدي إنشاء المشروع إلى إنتاج DLL (تجميع) يسمى [Nunit.dll]

لنستخدم الآن NUnit لتنفيذ فئة الاختبار:

  • في [1]: فتح مشروع NUnit
  • في [2, 3]: يتم تحميل DLL bin/Release/Nunit.dll الناتج عن إنشاء مشروع C#
  • إلى [4]: تم تحميل DLL
  • في [5]: شجرة الاختبارات
  • في [6]: يتم تنفيذها
  • إلى [7]: النتائج: نجح t1، وفشل t2
  • في [8]: يشير شريط أحمر إلى الفشل العام لفئة الاختبارات
  • في [9]: رسالة الخطأ المتعلقة بالاختبار الفاشل
  • في [11]: علامات التبويب المختلفة في نافذة النتائج
  • في [12]: علامة التبويب [Console.Out]. نرى فيها أن:
    • لم يتم تنفيذ المنشئ سوى مرة واحدة
    • تم تنفيذ الطريقة [SetUp] قبل كل من الاختبارين
    • تم تنفيذ الطريقة [TearDown] بعد كل من الاختبارين

من الممكن تحديد الطرق المراد اختبارها:

  • في [1]: يُطلب عرض مربع اختيار بجانب كل اختبار
  • في [2]: يتم تحديد الاختبارات المطلوب تنفيذها
  • في [3]: يتم تنفيذها

لتصحيح الأخطاء، يكفي تصحيح مشروع C# وإعادة إنشائه. يكتشف NUnit أن DLL الذي يختبره قد تم تغييره ويقوم بتحميل النسخة الجديدة تلقائيًا. يكفي عندئذ إعادة تشغيل الاختبارات.

لنأخذ الفئة الاختبارية الجديدة التالية:


using System;
using NUnit.Framework;

namespace NUnit {
    [TestFixture]
    public class NUnit2 : AssertionHelper {
        public NUnit2() {
            Console.WriteLine("constructeur");
        }
        [SetUp]
        public void avant() {
            Console.WriteLine("Setup");
        }
        [TearDown]
        public void après() {
            Console.WriteLine("TearDown");
        }
        [Test]
        public void t1() {
            Console.WriteLine("test1");
            Expect(1, EqualTo(1));
        }
        [Test]
        public void t2() {
            Console.WriteLine("test2");
            Expect(1, EqualTo(2), "1 n'est pas égal à 2");
        }
    }
}

اعتبارًا من الإصدار 2.4 من NUnit، أصبحت صيغة جديدة متاحة، وهي تلك الموجودة في السطرين 21 و26. ولهذا، يجب أن تنبثق فئة الاختبار من فئة AssertionHelper (السطر 6).

فيما يلي المطابقة (غير الشاملة) بين الصيغة القديمة والجديدة:

Assert.AreEqual(expression1, expression2,
message)
Expect(expression1,EqualTo(expression2),message)
Assert.AreEqual(réel1, réel2, delta, message)
Expect(expression1,EqualTo(expression2).Within(delta),
essage)
Assert.AreSame(objet1, objet2, message)
Expect(objet1,SameAs(objet2),message)
Assert.AreNotSame(objet1, objet2, message)
Expect(objet1,Not.SameAs(objet2),message)
Assert.IsNull(objet,message)
Expect(objet,Null,message)
Assert.IsNotNull(objet,message)
Expect(objet,Not.Null,message)
Assert.IsTrue(expression,message)
Expect(expression,True,message)
Assert.IsFalse(expression,message)
Expect(expression,False,message)

دعونا نضيف الاختبار التالي إلى الفئة NUnit2:


        [Test]
        public void t3() {
            bool vrai = true, faux = false;
            Expect(vrai, True);
            Expect(faux, False);
            Object obj1 = new Object(), obj2 = null, obj3=obj1;
            Expect(obj1, Not.Null);
            Expect(obj2, Null);
            Expect(obj3, SameAs(obj1));
            double d1 = 4.1, d2 = 6.4, d3 = d1;
            Expect(d1, EqualTo(d3).Within(1e-6));
            Expect(d1, Not.EqualTo(d2));
}

إذا قمنا بإنشاء (F6) DLL الجديد من مشروع C#، يصبح مشروع NUnit كما يلي:

  • في [1]: تم الكشف تلقائيًا عن فئة الاختبار الجديدة [NUnit2]
  • في [2]: يتم تنفيذ الاختبار t3 لـ NUnit2
  • في [3]: تم اجتياز الاختبار t3

لمزيد من المعلومات حول NUnit، يرجى الاطلاع على دليل المساعدة الخاص بـ NUnit:

6.4.2. حل Visual Studio

سنقوم ببناء حل Visual Studio التالي تدريجيًا:

  • في [1]: يتكون الحل ImpotsV5 من ثلاثة مشاريع، واحد لكل طبقة من طبقات التطبيق الثلاث
  • في [2]: المشروع [dao] للطبقة [dao]
  • في [3]: مشروع [metier] للطبقة [metier]
  • إلى [4]: المشروع [ui] من الطبقة [ui]

يمكن بناء الحل ImpotsV5 بالطريقة التالية:

1
234
5
  • في [1]: إنشاء مشروع جديد
  • في [2]: اختيار تطبيق وحدة التحكم
  • في [3]: استدعاء المشروع [dao]
  • في [4]: إنشاء المشروع
  • في [5]: بمجرد إنشاء المشروع، احفظه
  • في [6]: الاحتفاظ باسم [dao] للمشروع
  • في [7]: تحديد مجلد لتسجيل المشروع وحله
  • في [8]: قم بتسمية الحل
  • في [9]: تحديد أن الحل يجب أن يكون له مجلده الخاص
  • في [10]: حفظ المشروع وحله
  • في [11]: المشروع [dao] في حله ImpotsV5
  • في [12]: مجلد الحل ImpotsV5. يحتوي على المجلد [dao] من المجلد [dao].
  • في [13]: محتوى المجلد [dao]
  • في [14]: يتم إضافة مشروع جديد إلى الحل ImpotsV5
  • في [15]: يُسمى المشروع الجديد [metier]
  • في [16]: الحل مع مشروعيه
  • إلى [17]: الحل، بعد إضافة المشروع الثالث [ui]
  • في [18]: مجلد الحل ومجلدات المشاريع الثلاثة
  • عند تنفيذ حل بواسطة (Ctrl+F5)، يتم تنفيذ المشروع النشط. وينطبق الأمر نفسه عند إنشاء (F6) الحل. يظهر اسم المشروع النشط بالخط العريض [19] في الحل.
  • في [20]: لتغيير المشروع النشط في الحل
  • إلى [21]: أصبح المشروع [metier] هو المشروع النشط في الحل

6.4.3. الطبقة [dao]

مراجع المشروع (انظر [1] في المشروع)

يتم إضافة المرجع [nunit.framework] الضروري للاختبارات [NUnit]

الكيانات (انظر [2] في المشروع)

الفئة [TrancheImpot] هي فئة الإصدارات السابقة. تم تغيير اسم الفئة [FileImpotException] من الإصدار السابق إلى [ImpotException] لجعلها أكثر عمومية وعدم ربطها بطبقة [dao] معينة:


using System;

namespace Entites {
    public class ImpotException : Exception {

        // رمز الخطأ
        public int Code { get; set; }

        // الشركات
        public ImpotException() {
        }
        public ImpotException(string message)
            : base(message) {
        }
        public ImpotException(string message, Exception e)
            : base(message, e) {
        }
    }
}

الطبقة [dao] (انظر [3] في المشروع)

واجهة [IImpotDao] هي واجهة الإصدار السابق. وينطبق الأمر نفسه على الفئة [HardwiredImpot]. تتطور الفئة [FileImpot] لتأخذ في الاعتبار تغيير الاستثناء [FileImpotException] إلى [ImpotException]:


...

namespace Dao {
    public class FileImpot : IImpotDao {

        // رموز الأخطاء
        [Flags]
        public enum CodeErreurs { Acces = 1, Ligne = 2, Champ1 = 4, Champ2 = 8, Champ3 = 16 };

...

        // المُنشئ
        public FileImpot(string fileName) {
            // يتم حفظ اسم الملف
            FileName = fileName;
...
            // في البداية لا توجد أخطاء
            CodeErreurs code = 0;
            try {
                using (StreamReader input = new StreamReader(FileName)) {
                    while (!input.EndOfStream && code == 0) {
...
                        // خطأ؟
                        if (code != 0) {
                            // يتم تسجيل الخطأ
                            fe = new ImpotException(String.Format("Ligne n° {0} incorrecte", numLigne)) { Code = (int)code };
                        } else {
...
                        }
                    }
                }
            } catch (Exception e) {
                // يتم تسجيل الخطأ
                fe = new ImpotException(String.Format("Erreur lors de la lecture du fichier {0}", FileName), e) { Code = (int)CodeErreurs.Acces };
            }
            // هل يجب الإبلاغ عن الخطأ؟
...
        }
    }
}
  • السطر 8: تم نقل رموز الأخطاء التي كانت موجودة سابقًا في الفئة [FileImpotException] إلى الفئة [FileImpot]. فهي في الواقع رموز أخطاء خاصة بهذا التنفيذ للواجهة [IImpotDao].
  • السطران 26 و34: لتغليف خطأ، يتم استخدام الفئة [ImpotException] بدلاً من الفئة [FileImpotException].

اختبار [Test1] (انظر [4] في المشروع)

تكتفي الفئة [Test1] بعرض شرائح الضريبة على الشاشة:


using System;
using Dao;
using Entites;

namespace Tests {
    class Test1 {
        static void Main() {

            // يتم إنشاء الطبقة [dao]
            IImpotDao dao = null;
            try {
                // إنشاء الطبقة [dao]
                dao = new FileImpot("DataImpot.txt");
            } catch (ImpotException e) {
                // عرض الخطأ
                string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
                Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
                // إيقاف البرنامج
                Environment.Exit(1);
            }
            // يتم عرض شرائح الضريبة
            TrancheImpot[] tranchesImpot = dao.TranchesImpot;
            foreach (TrancheImpot t in tranchesImpot) {
                Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN);
            }
        }
    }
}
  • السطر 13: يتم تنفيذ الطبقة [dao] بواسطة الفئة [FileImpot]
  • السطر 14: يتم التعامل مع الاستثناء من النوع [ImpotException] الذي قد يحدث.

يتم نسخ الملف [DataImpot.txt] الضروري للاختبارات تلقائيًا إلى مجلد تشغيل المشروع (انظر [5] في المشروع). سيحتوي المشروع [dao] على عدة فئات تحتوي على طريقة [Main]. لذا، يجب تحديد الفئة المراد تنفيذها بشكل صريح عندما يطلب المستخدم تنفيذ المشروع باستخدام Ctrl-F5:

  • في [1]: الوصول إلى خصائص المشروع
  • في [2]: تحديد أنه تطبيق وحدة تحكم
  • في [3]: تحديد الفئة المطلوب تنفيذها

يؤدي تنفيذ الفئة [Test1] السابقة إلى النتائج التالية:

4962:0:0
8382:0,068:291,09
14753:0,191:1322,92
23888:0,283:2668,39
38868:0,374:4846,98
47932:0,426:6883,66
0:0,481:9505,54

الاختبار [Test2] (انظر [4] في المشروع)

تقوم الفئة [Test2] بنفس ما تقوم به الفئة [Test1] من خلال تنفيذ الطبقة [dao] مع الفئة [HardwiredImpot]. يتم استبدال السطر 13 من [Test1] بالسطر التالي:


                dao = new HardwiredImpot();

يتم تعديل المشروع ليقوم الآن بتنفيذ الفئة [Test2]:

نتائج الشاشة هي نفسها كما في السابق.

الاختبار NUnit [NUnit1] (انظر [4] في المشروع)

اختبار الوحدة [NUnit1] هو التالي:


using System;
using Dao;
using Entites;
using NUnit.Framework;

namespace Tests {
    [TestFixture]
    public class NUnit1 : AssertionHelper{
        // طبقة [dao] المراد اختبارها
        private IImpotDao dao;

        // المُنشئ
        public NUnit1() {
            // تهيئة الطبقة [dao]
            dao = new FileImpot("DataImpot.txt");
        }

        // اختبار
        [Test]
        public void ShowTranchesImpot(){
            // يتم عرض شرائح الضريبة
            TrancheImpot[] tranchesImpot = dao.TranchesImpot;
            foreach (TrancheImpot t in tranchesImpot) {
                Console.WriteLine("{0}:{1}:{2}", t.Limite, t.CoeffR, t.CoeffN);
            }
            // بعض الاختبارات
            Expect(tranchesImpot.Length,EqualTo(7));
            Expect(tranchesImpot[2].Limite,EqualTo(14753));
            Expect(tranchesImpot[2].CoeffR, EqualTo(0.191));
            Expect(tranchesImpot[2].CoeffN, EqualTo(1322.92));
        }
    }
}
  • تستمد فئة الاختبار من الفئة [AssertionHelper]، مما يسمح باستخدام الطريقة الثابتة Expect (الأسطر 27-30).
  • السطر 10: مرجع إلى الطبقة [dao]
  • الأسطر 13-16: يقوم المنشئ بإنشاء مثيل للطبقة [dao] باستخدام الفئة [FileImpot]
  • الأسطر 19-20: طريقة الاختبار
  • السطر 22: يتم استرداد جدول شرائح الضرائب من الطبقة [dao]
  • الأسطر 23-25: يتم عرضها كما سبق. لا داعي لهذا العرض في اختبار وحدة حقيقي. هنا، هذا العرض له غرض تعليمي.
  • السطر 27: نتحقق من وجود 7 شرائح ضريبية
  • السطور 28-30: نتحقق من قيم الشريحة الضريبية رقم 2

لتنفيذ هذا الاختبار الفردي، يجب أن يكون المشروع من النوع [Class Library]:

  • في [1]: تم تغيير طبيعة المشروع
  • إلى [2]: سيُسمى DLL الذي تم إنشاؤه بـ [ImpotsV5-dao.dll]
  • في [3]: بعد إنشاء (F6) المشروع، يحتوي المجلد [dao/bin/Release] على DLL [ImpotsV5-dao.dll]

ثم يتم تحميل DLL [ImpotsV5-dao.dll] في إطار العمل NUnit وتنفيذه:

  • في [1]: تم اجتياز الاختبارات بنجاح. نعتبر الآن الطبقة [dao] جاهزة للعمل. تحتوي DLL على جميع فئات المشروع بما في ذلك فئات الاختبار. هذه الفئات غير ضرورية. نقوم بإعادة بناء DLL لاستبعاد فئات الاختبار منها.
  • في [2]: تم استبعاد المجلد [tests] من المشروع
  • في [3]: المشروع الجديد. يتم إعادة إنشاء هذا المشروع بواسطة F6 من أجل إنشاء ملف DLL جديد.

6.4.4. الطبقة [metier]

  • إلى [1]، أصبح المشروع [metier] هو المشروع النشط للحل
  • في [2]: مراجع المشروع
  • في [3]: الطبقة [metier]
  • في [4]: فئات الاختبار
  • في [5]: ملف شرائح الضرائب [DataImpot.txt] الذي تم تكوينه [6] ليتم نسخه تلقائيًا إلى مجلد تشغيل المشروع [7]

مراجع المشروع (انظر [2] في المشروع)

كما هو الحال بالنسبة للمشروع [dao]، يتم إضافة المرجع [nunit.framework] الضروري لاختبارات [NUnit]. تحتاج الطبقة [metier] إلى الطبقة [dao]. لذا، فهي تحتاج إلى مرجع على DLL لهذه الطبقة. يتم ذلك على النحو التالي:

  • في [1]: نضيف مرجعًا جديدًا إلى مراجع مشروع [metier]
  • في [2]: نختار علامة التبويب [Browse]
  • في [3]: يتم تحديد المجلد [dao/bin/Release]
  • في [4]: يتم تحديد DLL [ImpotsV5-dao.dll] التي تم إنشاؤها في المشروع [dao]
  • في [5]: المرجع الجديد

الطبقة [metier] (انظر [3] في المشروع)

واجهة [IImpotMetier] هي واجهة الإصدار السابق. وينطبق الأمر نفسه على الفئة [ImpotMetier].

الاختبار [Test1] (انظر [4] في المشروع)

تقتصر الفئة [Test1] على إجراء بعض حسابات الرواتب:


using System;
using Dao;
using Entites;
using Metier;

namespace Tests {
    class Test1 {
        static void Main() {

            // يتم إنشاء الطبقة [metier]
            IImpotMetier metier = null;
            try {
                // إنشاء طبقة [metier]
                metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
            } catch (ImpotException e) {
                // عرض خطأ
                string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
                Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
                // إيقاف البرنامج
                Environment.Exit(1);
            }
            // يتم حساب بعض الضرائب
            Console.WriteLine(String.Format("Impot(true,2,60000)={0} euros", metier.CalculerImpot(true, 2, 60000)));
            Console.WriteLine(String.Format("Impot(false,3,60000)={0} euros", metier.CalculerImpot(false, 3, 60000)));
            Console.WriteLine(String.Format("Impot(false,3,60000)={0} euros", metier.CalculerImpot(false, 3, 6000)));
            Console.WriteLine(String.Format("Impot(false,3,60000)={0} euros", metier.CalculerImpot(false, 3, 600000)));
        }
    }
}
  • السطر 14: إنشاء الطبقات [metier] و [dao]. يتم تنفيذ الطبقة [dao] باستخدام الفئة [FileImpot]
  • الأسطر 12-21: إدارة استثناء محتمل من النوع [ImpotException]
  • الأسطر 23-26: استدعاءات متكررة للطريقة الوحيدة CalculerImpot للواجهة [IImpotMetier].

تم تكوين المشروع [metier] على النحو التالي:

  • [1]: المشروع من نوع تطبيق وحدة التحكم
  • [2]: الفئة المنفذة هي الفئة [Test1]
  • [3]: سيؤدي إنشاء المشروع إلى إنتاج الملف القابل للتنفيذ [ImpotsV5-metier.exe]

يؤدي تنفيذ المشروع إلى النتائج التالية:

1
2
3
4
Impot(true,2,60000)=4282 euros
Impot(false,3,60000)=4282 euros
Impot(false,3,60000)=0 euros
Impot(false,3,60000)=179275 euros

اختبار [NUnit1] (انظر [4] في المشروع)

تستعرض فئة الاختبارات الفردية [NUnit1] الحسابات الأربعة السابقة وتتحقق من نتائجها:


using Dao;
using Metier;
using NUnit.Framework;

namespace Tests {
    [TestFixture]
    public class NUnit1:AssertionHelper {
        // طبقة [metier] قيد الاختبار
        private IImpotMetier metier;

        // الشركة المصنعة
        public NUnit1() {
            // تهيئة الطبقة [metier]
            metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
        }

        // اختبار
        [Test]
        public void CalculsImpot(){
            // عرض شرائح الضريبة
            Expect(metier.CalculerImpot(true, 2, 60000), EqualTo(4282));
            Expect(metier.CalculerImpot(false, 3, 60000), EqualTo(4282));
            Expect(metier.CalculerImpot(false, 3, 6000), EqualTo(0));
            Expect(metier.CalculerImpot(false, 3, 600000), EqualTo(179275));
        }
    }
}
  • السطر 14: إنشاء الطبقات [metier] و [dao]. يتم تنفيذ الطبقة [dao] باستخدام الفئة [FileImpot]
  • الأسطر 21-24: استدعاءات متكررة للطريقة الوحيدة CalculerImpot للواجهة [IImpotMetier] مع التحقق من النتائج.

تم الآن تكوين المشروع [metier] على النحو التالي:

  • [1]: المشروع من نوع "مكتبة الفئات"
  • [2]: سيؤدي إنشاء المشروع إلى إنتاج DLL [ImpotsV5-metier.dll]

تم إنشاء المشروع (F6). ثم يتم تحميل DLL [ImpotsV5-metier.dll] الذي تم إنشاؤه في NUnit واختباره:

 

فيما سبق، نجحت الاختبارات. نعتبر الآن الطبقة [metier] جاهزة للعمل. تحتوي DLL على جميع فئات المشروع بما في ذلك فئات الاختبار. هذه الفئات غير ضرورية. نقوم بإعادة بناء DLL لاستبعاد فئات الاختبار منها.

  • إلى [1]: تم استبعاد المجلد [tests] من المشروع
  • إلى [2]: المشروع الجديد. يتم إعادة إنشاء هذا المشروع بواسطة F6 لإنشاء DLL جديد.

6.4.5. الطبقة [ui]

  • إلى [1]، أصبح المشروع [ui] هو المشروع النشط للحل
  • في [2]: مراجع المشروع
  • في [3]: الطبقة [ui]
  • في [4]: ملف [DataImpot.txt] الخاص بشرائح الضرائب، تم تكوينه [5] ليتم نسخه تلقائيًا إلى مجلد تشغيل المشروع [6]

مراجع المشروع (انظر [2] في المشروع)

تحتاج الطبقة [ui] إلى الطبقتين [metier] و [dao] لإجراء حسابات الضرائب الخاصة بها. لذا، فهي تحتاج إلى مرجع على DLL لهاتين الطبقتين. يتم اتباع نفس الإجراء كما هو موضح بالنسبة للطبقة [metier]

الفئة الرئيسية [Dialogue.cs] (انظر [3] في المشروع)

الفئة [Dialogue.cs] هي فئة الإصدار السابق.

الاختبارات

تم تكوين المشروع [ui] على النحو التالي:

  • [1]: المشروع من نوع "تطبيق وحدة التحكم"
  • [2]: سيؤدي إنشاء المشروع إلى إنتاج الملف القابل للتنفيذ [ImpotsV5-ui.exe]
  • [3]: الفئة التي سيتم تنفيذها

مثال على التنفيذ (Ctrl+F5) هو التالي:

Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :o 2 60000
Impot=4282 euros

6.4.6. الطبقة [Spring]

لنعد إلى الكود في [Dialogue.cs] الذي ينشئ الطبقتين [dao] و [metier]:


// يتم إنشاء الطبقات [metier et dao]
            IImpotMetier metier = null;
            try {
        // إنشاء طبقة [metier]
                metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
            } catch (ImpotException e) {
                // عرض الخطأ
...
                // إيقاف البرنامج
                Environment.Exit(1);
            }

السطر 5 ينشئ الطبقتين [dao] و [metier] بتسمية فئات التنفيذ للطبقتين بشكل صريح: FileImpot للطبقة [dao]، ImpotMetier للطبقة [metier]. إذا تم تنفيذ إحدى الطبقات باستخدام فئة جديدة، فسيتم تغيير السطر 5. على سبيل المثال:


                metier = new ImpotMetier(new HardwiredImpot());

بخلاف هذا التغيير، لن يتغير شيء في التطبيق لأن كل طبقة تتواصل مع الطبقة التالية وفقًا لواجهة. طالما أن هذه الأخيرة لا تتغير، فإن التواصل بين الطبقات لا يتغير أيضًا. يتيح لنا إطار عمل Spring المضي قدمًا قليلاً في استقلالية الطبقات من خلال السماح لنا بتحويل اسم الفئات التي تنفذ الطبقات المختلفة إلى ملف تكوين خارجي. وبالتالي، فإن تغيير تنفيذ إحدى الطبقات يعادل تغيير ملف تكوين. ولا يوجد أي تأثير على كود التطبيق.

فيما سبق، ستطلب الطبقة [ui] من Springإنشاء مثيلات للطبقات [dao] و [1] و [metier] و [2] وفقًا للمعلومات الواردة في ملف التكوين. ستطلب الطبقة [ui] بعد ذلك من Spring [3] مرجعًا للطبقة [metier]:


            // يتم إنشاء الطبقات [metier et dao]
            IImpotMetier metier = null;
            try {
                // سياق Spring
                IApplicationContext ctx = ContextRegistry.GetContext();
                // طلب مرجع على الطبقة [metier]
                metier = (IImpotMetier)ctx.GetObject("metier");
            } catch (Exception e1) {
...
}
  • السطر 5: إنشاء مثيلات للطبقات [dao] و [metier] بواسطة Spring
  • السطر 7: يتم استرداد مرجع على الطبقة [metier]. تجدر الإشارة إلى أن الطبقة [ui] حصلت على هذا المرجع دون ذكر اسم الفئة التي تنفذ الطبقة [metier].

يتوفر إطار عمل Spring في نسختين: Java و .NET. النسخة .NET متاحة على الرابط (مارس 2008) [http://www.springframework.net/]:

  • في [1]: موقع [Spring.net]
  • في [2]: صفحة التنزيلات
  • في [3]: تنزيل Spring 1.1 (مارس 2008)
  • إلى [4]: تنزيل الإصدار .exe ثم تثبيته
  • في [5]: المجلد الذي تم إنشاؤه بواسطة التثبيت
  • في [6]: يحتوي المجلد [bin/net/2.0/release] على ملفات Spring DLL الخاصة بمشاريع Visual Studio 2.0 أو أعلى. Spring هو إطار عمل غني. الجانب من Spring الذي سنستخدمه هنا لإدارة تكامل الطبقات في تطبيق ما يُسمى IoC: Inversion of Control أو DI: Dependence Injection. يوفر Spring مكتبات للوصول إلى قواعد البيانات باستخدام NHibernate، وإنشاء واستغلال خدمات الويب وتطبيقات الويب، ...
  • DLL اللازمة لإدارة تكامل الطبقات في التطبيق هي DLL و [7] و [8].

نقوم بتخزين هذه العناصر الثلاثة DLL في مجلد [lib] ضمن مشروعنا:

  • [1]: يتم وضع الملفات الثلاثة DLL في المجلد [lib] باستخدام مستكشف Windows
  • [2]: في مشروع [ui]، يتم عرض جميع الملفات
  • [3]: أصبح المجلد [ui/lib] مرئيًا الآن. يتم تضمينه في المشروع
  • [4]: المجلد [ui/lib] جزء من المشروع

عملية إنشاء المجلد [lib] ليست ضرورية على الإطلاق. كان من الممكن إنشاء المراجع مباشرة على المجلدات الثلاثة DLL الموجودة في المجلد [bin/net/2.0/release] التابع لـ [Spring.net]. ومع ذلك، فإن إنشاء المجلد [lib] يسمح بتطوير التطبيق على جهاز لا يحتوي على [Spring.net]، مما يجعله أقل اعتمادًا على بيئة التطوير المتاحة.

نضيف إلى المشروع [ui] مراجع حول الملفات الثلاثة الجديدة DLL:

  • [1]: نقوم بإنشاء مراجع حول المكونات الثلاثة DLL الموجودة في المجلد [lib] [2]
  • [3]: الملفات الثلاثة DLL هي جزء من مراجع المشروع

لنعد إلى نظرة عامة على بنية التطبيق:

فيما سبق، ستطلب الطبقة [ui] من Springإنشاء مثيلات للطبقات [dao] و [1] و [metier] و [2] وفقًا للمعلومات الواردة في ملف التكوين. ستطلب الطبقة [ui] بعد ذلك من Spring [3] مرجعًا للطبقة [metier]. وسيترجم ذلك في الطبقة [ui] إلى الكود التالي:


            // يتم إنشاء الطبقات [metier et dao]
            IImpotMetier metier = null;
            try {
                // سياق Spring
                IApplicationContext ctx = ContextRegistry.GetContext();
                // يتم طلب مرجع على الطبقة [metier]
                metier = (IImpotMetier)ctx.GetObject("metier");
            } catch (Exception e1) {
...
}
  • السطر 5: إنشاء مثيلات للطبقات [dao] و [metier] بواسطة Spring
  • السطر 7: يتم استرداد مرجع على الطبقة [metier].

يستخدم السطر [5] أعلاه ملف التكوين [App.config] لمشروع Visual Studio. في مشروع C#، يُستخدم هذا الملف لتكوين التطبيق. وبالتالي، فإن [App.config] ليس مفهومًا خاصًا بـ Spring، بل مفهومًا خاصًا بـ Visual Studio يستخدمه Spring. يستطيع Spring استخدام ملفات تكوين أخرى غير [App.config]. وبالتالي، فإن الحل المقدم هنا ليس الحل الوحيد المتاح.

لنقم بإنشاء الملف [App.config] باستخدام مساعد Visual Studio:

  • إلى [1]: إضافة عنصر جديد إلى المشروع
  • في [2]: حدد "ملف تكوين التطبيق"
  • في [3]: [App.config] هو الاسم الافتراضي لملف التكوين هذا
  • في [4]: تمت إضافة الملف [App.config] إلى المشروع

محتوى الملف [App.config] هو كما يلي:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>
</configuration>

[App.config] هو ملف XML. تتم تكوين المشروع بين العلامات <configuration>. التكوين اللازم لـ Spring هو كما يلي:


<?xml version="1.0" encoding="utf-8" ?>
<configuration>

    <configSections>
        <sectionGroup name="spring">
            <section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
            <section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
        </sectionGroup>
    </configSections>

    <spring>
        <context>
            <resource uri="config://spring/objects" />
        </context>
        <objects xmlns="http://www.springframework.net">
            <object name="dao" type="Dao.FileImpot, ImpotsV5-dao">
                <constructor-arg index="0" value="DataImpot.txt"/>
            </object>
            <object name="metier" type="Metier.ImpotMetier, ImpotsV5-metier">
                <constructor-arg index="0" ref="dao"/>
            </object>
        </objects>
    </spring>
</configuration>
  • الأسطر 11-23: يُسمى القسم المحدد بعلامة <spring> مجموعة أقسام <spring>. يمكن إنشاء أي عدد من مجموعات الأقسام في [App.config].
  • تحتوي مجموعة الأقسام على أقسام: وهذا هو الحال هنا:
    • الأسطر 12-14: القسم <spring/context>
    • الأسطر 15-22: القسم <spring/objects>
  • الأسطر 4-9: المنطقة <configSections> تحدد قائمة معالجات (handlers) مجموعات الأقسام الموجودة في [App.config].
  • الأسطر 5-8: تحدد قائمة معالجات أقسام المجموعة <spring> (name="spring").
  • السطر 6: معالج القسم <context> من المجموعة <spring>:
    • name: اسم القسم المدار
    • type: اسم الفئة التي تدير القسم بالصيغة NomClasse، NomDLL.
    • يتم إدارة قسم <context> من المجموعة <spring> بواسطة الفئة [Spring.Context.Support.ContextHandler] التي يمكن العثور عليها في DLL [Spring.Core.dll]
  • السطر 7: مدير قسم <objects> من المجموعة <spring>

الأسطر 4-9 قياسية في ملف [App.config] مع Spring. يكفي نسخها من مشروع إلى آخر.

  • الأسطر 12-14: تحدد قسم <spring/context>.
  • السطر 13: الغرض من العلامة <resource> هو الإشارة إلى مكان وجود الملف الذي يحدد الفئات التي يجب على Spring إنشاء مثيل لها. يمكن أن تكون هذه الفئات موجودة في [App.config] كما هو الحال هنا، ولكن يمكن أن تكون أيضًا في ملف تكوين آخر. يتم تحديد موقع هذه الفئات في السمة uri للعلامة <resource>:
    • <resource uri="config://spring/objects> يشير إلى أن قائمة الفئات المطلوب إنشاء مثيل لها موجودة في الملف [App.config] (config:)، في القسم //spring/objects، c.a.d. في العلامة <objects> التابعة للعلامة <spring>.
    • <resource uri="file://spring-config.xml"> تشير إلى أن قائمة الفئات المطلوب إنشاء مثيلات لها موجودة في الملف [spring-config.xml]. ويجب وضع هذا الملف في مجلدات التشغيل (bin/Release أو bin/Debug) للمشروع. أسهل طريقة هي وضعه، كما تم مع الملف [DataImpot.txt]، في جذر المشروع مع الخاصية [Copy to output directory=always].

السطور 12-14 هي سطور قياسية في ملف [App.config] مع Spring. يكفي نسخها من مشروع إلى آخر.

  • الأسطر 15-22: تحدد الفئات التي سيتم إنشاء مثيل لها. يتم في هذا الجزء إجراء التكوين الخاص للتطبيق. تحدد العلامة <objects> قسم تعريف الفئات التي سيتم إنشاء مثيل لها.
  • الأسطر 16-18: تحدد الفئة التي سيتم إنشاء مثيل لها لطبقة [dao]
    • السطر 16: كل كائن يتم إنشاء مثيل له بواسطة Spring يخضع لعلامة <object>. تحتوي هذه العلامة على سمة name وهي اسم الكائن الذي تم إنشاء مثيل له. ومن خلال هذه السمة يطلب التطبيق من Spring مرجعًا: "أعطني مرجعًا للكائن المسمى dao". يحدد السمة type الفئة المراد إنشاء مثيل لها بالصيغة NomClasse، NomDLL. وبالتالي، فإن السطر 16 يحدد كائنًا يسمى "dao"، وهو مثيل للفئة "Dao.FileImpot" الموجودة في "DLL" "ImpotsV5-dao.dll". تجدر الإشارة إلى أننا نذكر الاسم الكامل للفئة (بما في ذلك مساحة الأسماء) وأن اللاحقة .dll غير محددة في اسم DLL.

يمكن إنشاء مثيل لفئة بطريقتين باستخدام Spring:

  1. عبر منشئ معين يتم تمرير المعلمات إليه: وهذا ما يتم في الأسطر 16-18.
  2. عبر المنشئ الافتراضي بدون معلمات. يتم عندئذ تهيئة الكائن عبر خصائصه العامة: تحتوي العلامة <object> عندئذ على علامات فرعية <property> لتهيئة هذه الخصائص المختلفة. ليس لدينا مثال على هذه الحالة هنا.
  • (تابع)
    • السطر 16: الفئة التي تم إنشاء مثيل لها هي الفئة FileImpot. تحتوي هذه الفئة على المنشئ التالي:

        public FileImpot(string fileName);

يتم تعريف معلمات المنشئ باستخدام علامات <constructor-arg>.

  • السطر 17: يحدد المعلمة الأولى والوحيدة للمنشئ. السمة index هي رقم معلمة المنشئ، والسمة value هي قيمتها: <constructor-arg index="i" value="valuei"/>
  • الأسطر 19-21: تحدد الفئة المراد إنشاء مثيل لها للطبقة [metier]: الفئة [Metier.ImpotMetier] الموجودة في DLL [ImpotsV5-metier.dll].
    • السطر 19: الفئة التي تم إنشاء مثيل لها هي الفئة ImpotMetier. وتحتوي هذه الفئة على المنشئ التالي:

        public ImpotMetier(IImpotDao dao);
  • (تابع)
    • السطر 20: يحدد المعلمة الأولى والوحيدة للمنشئ. أعلاه، المعلمة dao للمنشئ هي مرجع كائن. في هذه الحالة، في العلامة <constructor-arg>، يتم استخدام السمة ref بدلاً من السمة value التي تم استخدامها للطبقة [dao]: <constructor-arg index="i" ref="refi"/>. في المنشئ أعلاه، يمثل المعلمة dao مثيلًا على الطبقة [dao]. تم تعريف هذا المثيل في الأسطر 16-18 من ملف التكوين. وبالتالي في السطر 20:

                <constructor-arg index="0" ref="dao"/>

ref="dao" يمثل كائن Spring "dao" المحدد في الأسطر 16-18.

باختصار، يقوم الملف [App.config] بما يلي:

  • يُنشئ مثيل الطبقة [dao] باستخدام الفئة FileImpot التي تتلقى كمعلمة DataImpot.txt (السطور 16-18). يُسمى الكائن الناتج "dao"
  • يُنشئ مثيل الطبقة [metier] باستخدام الفئة ImpotMetier التي تتلقى الكائن "dao" السابق كمعلمة (الأسطر 19-21).

لم يتبق لنا سوى استخدام ملف تكوين Spring هذا في الطبقة [ui]. للقيام بذلك، نقوم بنسخ الفئة [Dialogue.cs] إلى [Dialogue2.cs] ونجعل هذه الأخيرة الفئة الرئيسية للمشروع [ui]:

  • إلى [1]: نسخة من [Dialogue.cs]
  • إلى [2]: دمج
  • إلى [3]: نسخة من [Dialogue.cs]
  • في [4]: تمت إعادة تسمية [Dialogue2.cs]
  • إلى [6]: جعلنا [Dialogue2.cs] الفئة الرئيسية لمشروع [ui].

الرمز التالي من [Dialogue.cs]:


            // يتم إنشاء الطبقات [metier et dao]
            IImpotMetier metier = null;
            try {
        // إنشاء الطبقة [metier]
                metier = new ImpotMetier(new FileImpot("DataImpot.txt"));
            } catch (ImpotException e) {
                // عرض خطأ
                string msg = e.InnerException == null ? null : String.Format(", Exception d'origine : {0}", e.InnerException.Message);
                Console.WriteLine("L'erreur suivante s'est produite : [Code={0},Message={1}{2}]", e.Code, e.Message, msg == null ? "" : msg);
                // إيقاف البرنامج
                Environment.Exit(1);
            }
            // حلقة لا نهائية
            while (true) {
...

يصبح كما يلي في [Dialogue2.cs]:


            // يتم إنشاء الطبقات [metier et dao]
            IApplicationContext ctx = null;
            try {
                // سياق Spring
                ctx = ContextRegistry.GetContext();
            } catch (Exception e1) {
                // عرض الخطأ
                Console.WriteLine("Chaîne des exceptions : \n{0}", "".PadLeft(40, '-'));
                Exception e = e1;
                while (e != null) {
                    Console.WriteLine("{0}: {1}", e.GetType().FullName, e.Message);
                    Console.WriteLine("".PadLeft(40, '-'));
                    e = e.InnerException;
                }
                // إيقاف البرنامج
                Environment.Exit(1);
            }
            // يتم طلب مرجع على الطبقة [metier]
            IImpotMetier metier = (IImpotMetier)ctx.GetObject("metier");
            // حلقة لا نهائية
            while (true) {
....................................
  • السطر 2: يتيح IApplicationContext الوصول إلى جميع الكائنات التي تم إنشاء مثيلات لها بواسطة Spring. يُطلق على هذا الكائن اسم «سياق Spring للتطبيق» أو ببساطة «سياق التطبيق». في الوقت الحالي، لم يتم تهيئة هذا السياق بعد. ويقوم بذلك جملة try / catch التالية.
  • السطر 5: يتم قراءة تكوين Spring في [App.config] واستخدامه. بعد هذه العملية، إذا لم تكن هناك استثناءات، يتم إنشاء مثيلات لجميع الكائنات الموجودة في قسم <objects>:
  • كائن Spring "dao" هو مثيل على الطبقة [dao]
  • كائن Spring "metier" هو مثيل على الطبقة [metier]
  • السطر 19: تحتاج الفئة [Dialogue2.cs] إلى مرجع على الطبقة [metier]. يتم طلب هذا المرجع من سياق التطبيق. يتيح الكائن IApplicationContext الوصول إلى كائنات Spring عبر أسمائها (السمة name لعلامة <object> في تكوين Spring). المرجع الذي يتم إرجاعه هو مرجع من النوع العام Object. يتم تحويل نوع المرجع الذي تم إرجاعه إلى النوع الصحيح، وهو هنا نوع واجهة الطبقة [metier]: IImpotMetier.

إذا سارت الأمور على ما يرام، بعد السطر 19، يكون لـ [Dialogue2.cs] مرجع إلى الطبقة [metier]. الكود في السطور 21 وما بعدها هو كود الفئة [Dialogue.cs] التي تمت دراستها سابقًا.

  • السطور 6-17: معالجة الاستثناء الذي يحدث عندما يتعذر إكمال معالجة ملف تكوين Spring. قد يكون هناك أسباب متنوعة لذلك: صيغة غير صحيحة لملف التكوين نفسه أو عدم القدرة على إنشاء مثيل لأحد الكائنات المكونة. في مثالنا، قد تحدث الحالة الأخيرة إذا لم يتم العثور على الملف DataImpot.txt في السطر 17 من [App.config] في مجلد تشغيل المشروع.

الاستثناء الذي يظهر في السطر 6 هو سلسلة من الاستثناءات حيث لكل استثناء خاصيتان:

  • الرسالة: رسالة الخطأ المرتبطة بالاستثناء
  • InnerException: الاستثناء السابق في سلسلة الاستثناءات

تقوم الحلقة في الأسطر 10-14 بعرض جميع الاستثناءات في السلسلة بالشكل التالي: فئة الاستثناء والرسالة المرتبطة به.

عند تنفيذ المشروع [ui] باستخدام ملف تكوين صالح، نحصل على النتائج المعتادة:

Paramètres du calcul de l'Impot au format : Marié (o/n) NbEnfants Salaire ou rien pour arrêter :o 2 60000
Impot=4282 euros

عند تشغيل المشروع [ui] باستخدام ملف [DataImpotInexistant.txt] غير موجود،


            <object name="dao" type="Dao.FileImpot, ImpotsV5-dao">
                <constructor-arg index="0" value="DataImpotInexistant.txt"/>
            </object>

نحصل على النتائج التالية:

Chaîne des exceptions :
----------------------------------------
System.Configuration.ConfigurationErrorsException: Error creating context 'spring.root': Could not find file 'C:\data\2007-2008\c# 2008\poly\Chap4\ImpotsV5\ui\bin\Release\DataImpotInexistant.txt'.
----------------------------------------
Spring.Util.FatalReflectionException: Cannot instantiate Type [Spring.Context.Support.XmlApplicationContext] using ctor [Void .ctor(System.String, Boolean, System.String[])] : 'Exception has been thrown by the target of an invocation.'
----------------------------------------
System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
----------------------------------------
Spring.Objects.Factory.ObjectCreationException: Error creating object with name'dao' defined in 'config [spring/objects]' : Initialization of object failed : Cannot instantiate Type [Dao.FileImpot] using ctor [Void .ctor(System.String)] :'Exception has been thrown by the target of an invocation.'
----------------------------------------
Spring.Util.FatalReflectionException: Cannot instantiate Type [Dao.FileImpot] using ctor [Void .ctor(System.String)] : 'Exception has been thrown by the targetof an invocation.'
----------------------------------------
System.Reflection.TargetInvocationException: Exception has been thrown by the target of an invocation.
----------------------------------------
Entites.ImpotException: Erreur lors de la lecture du fichier DataImpotInexistant.txt
----------------------------------------
System.IO.FileNotFoundException: Could not find file 'C:\data\2007-2008\c# 2008\poly\Chap4\ImpotsV5\ui\bin\Release\DataImpotInexistant.txt'.
  • السطر 17: الاستثناء الأصلي من النوع [FileNotFoundException]
  • السطر 15: الطبقة [dao] تغلف هذا الاستثناء في نوع [Entites.ImpotException]
  • السطر 9: الاستثناء الذي أطلقه Spring لأنه لم ينجح في إنشاء مثيل للكائن المسمى "dao". في عملية إنشاء هذا الكائن، حدث استثناءان آخران من قبل: الاستثناءان في السطرين 11 و13.
  • نظرًا لعدم إمكانية إنشاء الكائن "dao"، لم يتم إنشاء سياق التطبيق. هذا هو معنى الاستثناء في السطر 5. قبل ذلك، حدث استثناء آخر، وهو الاستثناء الموجود في السطر 7.
  • السطر 3: الاستثناء الأعلى مستوى، والأخير في السلسلة: تم الإبلاغ عن خطأ في التكوين.

من كل هذا، نستنتج أن الاستثناء الأعمق، وهو هنا الاستثناء في السطر 17، غالبًا ما يكون الأكثر أهمية. ومع ذلك، تجدر الإشارة إلى أن Spring احتفظ برسالة الخطأ في السطر 17 ليرفعها إلى الاستثناء الأعلى مستوى في السطر 3 من أجل الحصول على السبب الأصلي للخطأ على أعلى مستوى.

يستحق Spring كتابًا خاصًا به. لم نكن هنا سوى لم نلمس الموضوع. يمكننا التعمق فيه من خلال الوثيقة [spring-net-reference.pdf] الموجودة في مجلد تثبيت Spring:

 

يمكنك أيضًا قراءة [http://tahe.developpez.com/dotnet/springioc]، وهو دليل تعليمي لـ Spring مقدم في سياق VB.NET.