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");
                // مالیات‌دهنده بعدی
            }//در حالی که
        }
    }
}

راه حل قبلی شامل وظایف استاندارد برنامه‌نویسی بود:

  1. استخراج داده‌های ذخیره‌شده در فایل‌ها، پایگاه‌های داده و غیره (خطوط ۱۲–۲۱)
  2. تعامل کاربر، خطوط 26 (ورودی) و 29 (نمایش)
  3. استفاده از الگوریتم منطق کسب‌وکار، خط ۲۹

تجربه عملی نشان داده است که جداسازی این فرآیندهای مختلف در کلاس‌های مجزا، قابلیت نگهداری برنامه‌ها را بهبود می‌بخشد. معماری یک برنامه که به این صورت ساختار یافته است به شرح زیر است:

این معماری به عنوان «معماری سه‌لایه» شناخته می‌شود که ترجمه‌ای از اصطلاح انگلیسی است. اصطلاح «سه‌لایه» معمولاً به معماری‌ای اشاره دارد که در آن هر لایه روی یک ماشین مجزا قرار دارد. هنگامی که لایه‌ها روی یک ماشین واحد قرار می‌گیرند، این معماری به یک «معماری سه‌طبقه» تبدیل می‌شود.

  • لایه [metier] شامل قواعد کسب‌وکار برنامه است. برای برنامه محاسبه مالیات ما، این‌ها قواعدی هستند که برای محاسبه بدهی مالیاتی مودی استفاده می‌شوند. این لایه برای کار کردن به داده‌ها نیاز دارد:
  • محدوده‌های مالیاتی، که هر سال تغییر می‌کنند
  • تعداد فرزندان، وضعیت تأهل مودی و حقوق سالانه

در نمودار بالا، داده‌ها می‌توانند از دو منبع تأمین شوند:

  • لایه دسترسی به داده، یا [dao] (DAO = شیء دسترسی به داده)، برای داده‌هایی که از قبل در فایل‌ها یا پایگاه‌های داده ذخیره شده‌اند. این می‌تواند در مورد مقاطع مالیاتی صادق باشد، همانطور که در نسخه قبلی برنامه انجام شده بود.
  • لایه رابط کاربری یا [ui] (UI = رابط کاربری) برای داده‌هایی که توسط کاربر وارد می‌شوند یا به کاربر نمایش داده می‌شوند. این مورد می‌تواند در اینجا برای تعداد فرزندان، وضعیت تأهل و حقوق سالانه مالیات‌دهنده صدق کند.
  • به‌طور کلی، لایه [dao] دسترسی به داده‌های پایدار (فایل‌ها، پایگاه‌های داده) یا داده‌های ناپایدار (شبکه، حسگرها و غیره) را مدیریت می‌کند.
  • از سوی دیگر، لایه [ui] تعاملات با کاربر را، در صورت وجود، مدیریت می‌کند.
  • این سه لایه از طریق استفاده از رابط‌ها مستقل شده‌اند.

ما اپلیکیشن [Impots] را که پیش از این در چندین مورد مطالعه کرده‌ایم، مجدداً بررسی خواهیم کرد تا به آن معماری سه‌لایه بدهیم. برای این کار، ما لایه‌های [ui, metier, dao] را یکی یکی، از لایه [dao] که داده‌های ماندگار را مدیریت می‌کند، بررسی خواهیم کرد.

قبل از انجام این کار، باید رابط‌ها را برای لایه‌های مختلف برنامه [Impots] تعریف کنیم.

6.2. رابط‌های برنامه [Impots]

به یاد داشته باشید که یک رابط مجموعه‌ای از امضاهای متد را تعریف می‌کند. کلاس‌هایی که این رابط را پیاده‌سازی می‌کنند، بدنه این متدها را فراهم می‌کنند.

بیایید به معماری سه‌لایهٔ برنامهٔ خود بازگردیم:

در این نوع معماری، اغلب کاربر است که ابتکار عمل را به دست می‌گیرد. آنها در [1] درخواستی ارسال کرده و در [8] پاسخی دریافت می‌کنند. این چرخه درخواست-پاسخ نامیده می‌شود. بیایید مثال محاسبه بدهی مالیاتی یک مؤدی را در نظر بگیریم. این کار به چندین مرحله نیاز دارد:

  1. لایه [ui] باید از کاربر درباره تعداد فرزندان، وضعیت تأهل و حقوق سالانه او سؤال کند. این همان عملیات [1] است که در بالا توضیح داده شد.
  2. پس از انجام این کار، لایه [ui] از لایه کسب‌وکار می‌خواهد که مالیات را محاسبه کند. برای این کار، داده‌هایی را که از کاربر دریافت کرده است، به آن منتقل می‌کند. این عملیات [2] است.
  3. لایه [metier] برای انجام وظیفه خود به اطلاعات خاصی نیاز دارد: رده‌های مالیاتی. این لایه این اطلاعات را از طریق مسیر [3, 4, 5, 6] از لایه [dao] درخواست خواهد کرد. [3] درخواست اولیه و [6] پاسخ به آن درخواست است.
  4. لایه [metier] با در اختیار داشتن تمام داده‌های مورد نیاز، مالیات را محاسبه می‌کند.
  5. لایه [metier] اکنون می‌تواند به درخواست لایه [ui] که در (b) ارسال شده بود، پاسخ دهد. این مسیر [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(...);
  • خط ۱: یک نمونه از کلاس [B1] ایجاد می‌شود
  • خط ۳: از این نمونه درخواست داده می‌شود

اگر فرض کنیم که کلاس پیاده‌سازی جدید [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(...);
  • خط ۱: یک نمونه از `[ib]` که رابط `[IB]` را پیاده‌سازی می‌کند، با نمونه‌سازی کلاس `[B1]` ایجاد می‌شود
  • خط ۳: داده از نمونه [ib] درخواست می‌شود

اکنون، اگر پیاده‌سازی [B1] از لایه [B] را با یک پیاده‌سازی [B2] جایگزین کنیم، و هر دوی این پیاده‌سازی‌ها به همان رابط [IB] پایبند باشند، آنگاه تنها خط ۱ لایه [A] نیاز به اصلاح دارد و هیچ خط دیگری نه. این یک مزیت بزرگ است که به خودی خود، استفاده سیستماتیک از رابط‌ها بین دو لایه را توجیه می‌کند.

می‌توانیم حتی فراتر رفته و لایه [A] را کاملاً از لایه [B] مستقل سازیم. در کد بالا، خط ۱ مشکل‌ساز است زیرا ارجاع به کلاس [B1] را به‌صورت سخت‌کد (hard-code) درج کرده است. به‌طور ایده‌آل، لایه [A] باید بتواند یک پیاده‌سازی از رابط [IB] را بدون نیاز به مشخص کردن نام کلاس استفاده کند. این امر با نمودار ما در بالا سازگار خواهد بود. می‌توانیم ببینیم که لایه [A] با رابط [IB] تعامل دارد و هیچ دلیل آشکاری وجود ندارد که چرا باید نام کلاسی را که این رابط را پیاده‌سازی می‌کند، بداند. این جزئیات برای لایه [A] بی‌فایده است.

چارچوب Spring (http://www.springframework.org) این امکان را فراهم می‌کند. معماری قبلی به شرح زیر تکامل می‌یابد:

لایهٔ عرضی [Spring] به یک لایه این امکان را می‌دهد که از طریق پیکربندی، مرجعی به لایهٔ سمت راست خود دریافت کند، بدون اینکه نیازی به دانستن نام کلاسی که آن لایه را پیاده‌سازی می‌کند، داشته باشد. این نام در فایل‌های پیکربندی قرار خواهد داشت و نه در کد C#. کد C# برای لایه [A] سپس شکل زیر را به خود می‌گیرد:

1
2
3
IB ib; // توسط Spring اولیه شده است
..
ib.getData(...);
  • خط ۱: یک نمونه از [ib] که رابط [IB] لایه [B] را پیاده‌سازی می‌کند. این نمونه توسط Spring بر اساس اطلاعاتی که در یک فایل پیکربندی یافت می‌شود، ایجاد می‌شود. Spring مسئولیت ایجاد موارد زیر را بر عهده خواهد داشت:
    • نمونه [b]، که لایه [B] را پیاده‌سازی می‌کند
    • اِنسان [a] که لایه [A] را پیاده‌سازی می‌کند. این اِنسان مقداردهی اولیه خواهد شد. میدان [ib] در بالا، مرجع [b] شیء پیاده‌ساز لایه [B] را دریافت خواهد کرد
  • خط ۳: داده از نمونه [ib] درخواست می‌شود

اکنون می‌توانیم ببینیم که کلاس پیاده‌سازی [B1] لایه B در هیچ کجای کد لایه [A] ظاهر نمی‌شود. وقتی پیاده‌سازی [B1] با پیاده‌سازی جدید [B2] جایگزین شود، هیچ تغییری در کد کلاس [A] رخ نخواهد داد. ما به سادگی فایل‌های پیکربندی Spring را تغییر می‌دهیم تا به جای [B1]، [B2] را نمونه‌سازی کند.

ترکیب Spring و رابط‌های C# با تضمین پیوستگی تنگاتنگ لایه‌های برنامه، بهبود قابل‌توجهی در نگهداری برنامه به همراه دارد. این راه‌حلی است که برای نسخه جدید برنامه [Impots] به کار خواهیم برد.

بیایید به معماری سه‌لایهٔ برنامهٔ خود بازگردیم:

در موارد ساده، می‌توانیم با لایه [metier] شروع کنیم تا رابط‌های کاربری برنامه را شناسایی کنیم. برای کارکرد، به داده‌ها نیاز دارد:

  • که از قبل در فایل‌ها، پایگاه‌های داده یا از طریق شبکه در دسترس هستند. این داده‌ها توسط لایه [dao] تأمین می‌شوند.
  • هنوز در دسترس نیست. در این صورت، این لایه توسط [ui] تأمین می‌شود که آن را از کاربر برنامه دریافت می‌کند.

لایه [dao] باید چه رابطی را در اختیار لایه [metier] قرار دهد؟ چه تعاملاتی بین این دو لایه ممکن است؟ لایه [dao] باید داده‌های زیر را در اختیار لایه [metier] قرار دهد:

  • محدوده‌های مالیاتی

در برنامهٔ ما، لایهٔ [dao] از داده‌های موجود استفاده می‌کند اما هیچ دادهٔ جدیدی ایجاد نمی‌کند. یک تعریف از رابط برای لایهٔ [dao] می‌تواند به شرح زیر باشد:


using Entites;

namespace Dao {
    public interface IImpotDao {
        // محدوده‌های مالیاتی
        TrancheImpot[] TranchesImpot{get;}
    }
}
  • خط ۳: لایه [dao] در فضای نام [Dao] قرار داده خواهد شد
  • خط ۶: رابط IImpotDao، ویژگی TranchesImpot را تعریف می‌کند که محدوده‌های مالیاتی را برای لایه [métier] فراهم می‌کند.
  • خط ۱: فضای نامی را وارد می‌کند که ساختار 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);
    }
}
  • خط ۱: همه چیز مربوط به لایه [metier] در فضای نام [Metier] قرار می‌گیرد.
  • خط ۲: رابط IImpotMetier تنها یک متد را تعریف می‌کند: متدی که برای محاسبه بدهی مالیاتی مودی بر اساس وضعیت تأهل، تعداد فرزندان و حقوق سالانه او استفاده می‌شود.

ما در حال بررسی پیاده‌سازی اولیه این معماری لایه‌بندی هستیم.

6.3. برنامهٔ نمونه – نسخهٔ ۴

6.3.1. پروژه ویژوال استودیو

پروژه ویژوال استودیو به شرح زیر خواهد بود:

  • [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' 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] };
            }
        }
    }//کلاس
}// فضای نام
  • خط ۵: کلاس HardwiredImpot رابط IImpotDao را پیاده‌سازی می‌کند
  • خط ۱۲: پیاده‌سازی خاصیت TranchesImpot از رابط IImpotDao. این ویژگی یک ویژگی خودکار است. این ویژگی متد get از ویژگی TranchesImpot از رابط IImpotDao را پیاده‌سازی می‌کند. علاوه بر این، یک متد خصوصی به نام set—که بنابراین داخلی به کلاس است—اعلام شده است تا سازنده در خطوط ۱۵ تا ۲۲ بتواند آرایهٔ بازهٔ مالیاتی را مقداردهی اولیه کند.

رابط 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);
                        //آیا سه فیلد وجود دارد؟
                        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 مورد بحث قرار گرفته است.
  • خط ۱۴: متد TranchesImpot از رابط IImpotDao
  • خط ۷۶: inicialization (ابتدایی‌سازی) بازه‌های مالیاتی در سازنده کلاس، بر اساس فایلی که نام آن در خط ۱۷ به سازنده پاس شده است.

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);
        }//محاسبه
    }//کلاس

}
  • خط ۵: کلاس [Metier] رابط [IImpotMetier] را پیاده‌سازی می‌کند.
  • خطوط ۱۴–۱۹: لایه [metier] باید با لایه [dao] همکاری کند. بنابراین باید مرجعی به شیء پیاده‌ساز رابط IImpotDao در اختیار داشته باشد. به همین دلیل این مرجع به‌عنوان پارامتر به سازنده ارسال می‌شود.
  • خط ۱۶: مرجع لایه [dao] در فیلد خصوصی خط ۸ ذخیره می‌شود
  • خط ۱۸: سازنده با استفاده از این مرجع، جدول رده‌بندی مالیاتی را بازیابی کرده و مرجعی به آن را در ویژگی خصوصی خط ۸ ذخیره می‌کند.
  • خطوط ۲۲–۴۱: پیاده‌سازی متد CalculerImpot از رابط IImpotMetier. این پیاده‌سازی از جدول رده‌های مالیاتی که توسط سازنده اولیه شده است استفاده می‌کند.

6.3.5. لایه [ui]

کلاس‌های رابط کاربری برای نسخه‌های ۲ و ۳ بسیار شبیه به هم بودند. کلاس مربوط به نسخهٔ ۲ به شرح زیر بود:


using System;

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

            // ایجاد شیء IImpot
            IImpot impot = new HardwiredImpot();

            //حلقه بی‌نهایت
            while (true) {
...
            }//حلقه‌ی while
        }
    }
}

و آن برای نسخه ۳:


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) {
...
            }//در حالی که
        }
    }
}

تنها تفاوت در نحوهٔ نمونه‌سازی شیء از نوع 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");
                // مالیات‌دهنده بعدی
            }//در حالی که
        }
    }
}
  • خط ۱۲: ایجاد اشیاء لایه‌های [dao] و [metier]. توجه داشته باشید که لایه [metier] به لایه [dao] نیاز دارد.
  • خط ۱۸: استفاده از لایه [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");
                // مالیات‌دهنده بعدی
            }//در حالی که
        }
    }
}
  • خطوط ۱۱–۲۱: نمونه‌سازی لایه‌های [dao] و [metier]. از آنجا که نمونه‌سازی لایه [dao] ممکن است یک استثنا ایجاد کند، این مورد مدیریت می‌شود
  • خط ۲۶: استفاده از لایه [metier] برای محاسبه مالیات، همانند نسخه قبلی

6.3.6. نتیجه‌گیری

معماری لایه‌ای و استفاده از رابط‌ها انعطاف‌پذیری قابل توجهی به برنامه ما بخشیده است. این امر به‌ویژه در نحوه نمونه‌سازی لایه‌های [dao] و [métier] توسط لایه [ui] مشهود است:


        // ایجاد لایه‌ها [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);
            }

در مورد دوم، به جز مدیریت خطا، ایجاد لایه‌های [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) {
...
        }//محاسبه
    }//کلاس

}

در خط ۱۴، می‌بینیم که لایه [métier] با استفاده از یک مرجع به رابط لایه [dao] ساخته شده است. بنابراین، تغییر پیاده‌سازی مورد دوم هیچ تأثیری بر لایه [métier] ندارد. به همین دلیل، تنها پیاده‌سازی ما از لایه [métier] توانست بدون تغییر در کنار دو پیاده‌سازی مختلف از لایه [dao] کار کند.

6.4. مثال کاربردی – نسخهٔ ۵

این نسخه جدید بر اساس نسخه قبلی است، با تغییرات زیر:

  • لایه‌های [métier] و [dao] هر یک در یک DLL کپسوله شده و با استفاده از چارچوب تست واحد NUnit آزمایش شده‌اند.
  • ادغام لایه‌ها توسط فریم‌ورک Spring انجام می‌شود

در پروژه‌های بزرگ، چندین توسعه‌دهنده روی یک پروژه کار می‌کنند. معماری‌های لایه‌بندی این شیوه کاری را تسهیل می‌کنند: از آنجا که لایه‌ها از طریق رابط‌های کاملاً تعریف‌شده با یکدیگر ارتباط برقرار می‌کنند، توسعه‌دهنده‌ای که روی یک لایه کار می‌کند، نیازی به در نظر گرفتن کار سایر توسعه‌دهندگان روی لایه‌های دیگر ندارد. همه به سادگی باید به رابط‌ها پایبند باشند.

در مثال بالا، هنگامی که توسعه‌دهنده لایه [métier] در حال تست لایه خود است، به یک پیاده‌سازی از لایه [dao] نیاز خواهد داشت. تا زمانی که آن تکمیل شود، می‌توانند از یک پیاده‌سازی موقت لایه [dao] استفاده کنند، به شرطی که با رابط IImpotDao مطابقت داشته باشد. این یک مزیت دیگر معماری لایه‌ای است: تأخیر در لایه [dao] مانع از آزمایش لایه [métier] نمی‌شود. پیاده‌سازی نمونه‌ای لایه [dao] این مزیت را نیز دارد که اغلب پیاده‌سازی آن آسان‌تر از لایه واقعی [dao] است، که ممکن است نیاز به راه‌اندازی یک SGBD، برقراری اتصالات شبکه و غیره داشته باشد.

هنگامی که لایه [dao] تکمیل و آزمایش شد، این لایه به جای کد منبع، به شکل یک DLL در اختیار توسعه‌دهندگان لایه [métier] قرار داده خواهد شد. در نهایت، برنامه اغلب به صورت یک فایل اجرایی .exe (از لایه [ui]) و کتابخانه‌های کلاسی .dll (لایه‌های دیگر) تحویل داده می‌شود.

6.4.1. NUnit

آزمون‌هایی که تاکنون برای برنامه‌های مختلف ما انجام شده، بر بازرسی بصری متکی بوده‌اند. ما بررسی می‌کردیم که آنچه روی صفحه نمایش داده می‌شد با آنچه انتظار می‌رفت مطابقت دارد. این روش زمانی که آزمون‌های متعددی برای انجام دادن وجود دارد، غیرعملی است. انسان‌ها، به هر حال، مستعد خستگی هستند و توانایی آن‌ها در تأیید آزمون‌ها با گذشت روز کاهش می‌یابد. بنابراین، آزمون‌ها باید خودکار شوند و طوری طراحی شوند که به هیچ مداخله انسانی نیاز نداشته باشند.

یک نرم‌افزار در طول زمان تکامل می‌یابد. با هر تغییر، باید بررسی کنیم که نرم‌افزار دچار «پس‌رفت» نشود، c.a.d، و اینکه همچنان تست‌های عملکردی را که هنگام نگارش اولیه انجام شده بودند، پشت سر می‌گذارد. این تست‌ها به عنوان «تست‌های عدم پس‌رفت» شناخته می‌شوند. یک نرم‌افزار نسبتاً بزرگ ممکن است به صدها تست نیاز داشته باشد. در واقع، هر متد در هر کلاس از برنامه تست می‌شود. این‌ها به عنوان تست‌های واحد (unit tests) شناخته می‌شوند. اگر این تست‌ها خودکار نشده باشند، می‌توانند زمان زیادی از توسعه‌دهندگان بگیرند.

ابزارهایی برای خودکارسازی تست توسعه داده شده‌اند. یکی از آن‌ها NUnit نام دارد. این ابزار در وب‌سایت [http://www.nunit.org] در دسترس است:

نسخهٔ 2.4.6 که در بالا نشان داده شده است، برای این سند (مارس ۲۰۰۸) استفاده شده است. نصب، آیکون [1] را روی دسکتاپ قرار می‌دهد:

دوباره‌کلیک روی آیکون [1]، رابط کاربری گرافیکی NUnit [2] را اجرا می‌کند. این کار هیچ کمکی به خودکارسازی تست نمی‌کند، زیرا ما بار دیگر به تأیید بصری محدود می‌شویم: آزمونگر نتایج تست را که در رابط کاربری گرافیکی نمایش داده می‌شود، بررسی می‌کند. با این حال، این تست‌ها را می‌توان با استفاده از ابزارهای دسته‌ای نیز اجرا کرد و نتایج آن‌ها را در فایل‌های XML ذخیره نمود. این روشی است که تیم‌های توسعه از آن استفاده می‌کنند: تست‌ها به صورت شبانه اجرا می‌شوند و توسعه‌دهندگان صبح روز بعد نتایج را در اختیار دارند.

بیایید برای روشن شدن اصل پشت تست‌های NUnit، به یک مثال نگاه کنیم. اول از همه، بیایید یک پروژه جدید C# از نوع Console Application ایجاد کنیم:

در [1]، می‌توانیم références را برای پروژه مشاهده کنیم. این ارجاعات، DLL حاوی کلاس‌ها و رابط‌هایی هستند که توسط پروژه استفاده می‌شوند. آن‌هایی که در [1] فهرست شده‌اند، به‌طور پیش‌فرض در هر پروژه جدید C# گنجانده می‌شوند. برای اینکه بتوانیم از کلاس‌ها و رابط‌های فریم‌ورک NUnit استفاده کنیم، باید یک مرجع جدید به پروژه اضافه کنیم.

در زبانه .NET در بالا، ما کامپوننت [nunit.framework] را انتخاب می‌کنیم. کامپوننت‌های [nunit.*] که در بالا فهرست شده‌اند، به طور پیش‌فرض در محیط .NET گنجانده نشده‌اند. آنها توسط نصب قبلی چارچوب NUnit به آنجا اضافه شده‌اند. پس از اعتبارسنجی مرجع، این مرجع به صورت [4] در لیست مراجع پروژه ظاهر می‌شود.

قبل از تولید برنامه، پوشه [bin/Release] پروژه خالی است. پس از تولید (F6)، مشاهده می‌شود که پوشه [bin/Release] دیگر خالی نیست:

در [6]، می‌توانیم حضور DLL و [nunit.framework.dll] را مشاهده کنیم. این اضافه شدن مرجع [nunit.framework] بود که باعث شد DLL به پوشهٔ runtime کپی شود. در واقع، این یکی از پوشه‌هایی است که توسط CLR (محیط اجرایی زبان مشترک) و 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");
        }
    }
}
  • خط ۶: کلاس NUnit1 باید عمومی (public) باشد. کلمه کلیدی «public» به‌طور پیش‌فرض توسط ویژوال استودیو تولید نمی‌شود. باید اضافه شود.
  • خط ۵: ویژگی [TestFixture] یک ویژگی NUnit است. این نشان می‌دهد که کلاس یک کلاس تست است.
  • خطوط ۷–۹: سازنده. این تنها برای نمایش یک پیام روی صفحه استفاده می‌شود. ما می‌خواهیم ببینیم چه زمانی اجرا می‌شود.
  • خط ۱۰: ویژگی [SetUp] متدی را تعریف می‌کند که قبل از هر آزمون واحد اجرا می‌شود.
  • خط ۱۴: ویژگی [TearDown] متدی را تعریف می‌کند که پس از هر آزمون واحد اجرا می‌شود.
  • خط ۱۸: ویژگی [Test] یک متد تست را تعریف می‌کند. برای هر متدی که با ویژگی [Test] نشانه‌گذاری شده است، متدی که با [SetUp] نشانه‌گذاری شده قبل از تست اجرا می‌شود و متدی که با [TearDown] نشانه‌گذاری شده بعد از تست اجرا می‌شود.
  • خط ۲۱: یکی از متدهای [Assert.*] تعریف‌شده توسط چارچوب NUnit. متدهای زیر از [Assert] در دسترس هستند:
    • [Assert.AreEqual(expression1, expression2)]: بررسی می‌کند که مقادیر دو عبارت برابر هستند. انواع زیادی از عبارت‌ها پذیرفته می‌شوند (int، string، float، double، decimal و غیره). اگر دو عبارت برابر نباشند، یک استثنا پرتاب می‌شود.
    • [Assert.AreEqual(réel1, réel2, delta)]: بررسی می‌کند که دو عدد ممیز شناور تا حد خطای مجاز دلتا برابر هستند، c.a.d abs(floating-point1 - floating-point2) <= delta. به عنوان مثال، می‌توان نوشت [Assert.AreEqual(réel1, réel2, 1E-6)] تا بررسی شود که دو مقدار تا حد 10⁻⁶ برابر هستند.
    • [Assert.AreEqual(expression1, expression2, message)] و [Assert.AreEqual(réel1, réel2, delta, message)] گونه‌هایی هستند که به شما امکان می‌دهند پیام خطایی را که با استثنای پرتاب‌شده هنگام شکست متد [Assert.AreEqual] مرتبط است، مشخص کنید.
    • [Assert.IsNotNull(object)] و [Assert.IsNotNull(object, message)]: بررسی می‌کنند که شیء برابر null نیست.
    • [Assert.IsNull(object)] و [Assert.IsNull(object, message)]: بررسی می‌کنند که `object` برابر با `null` باشد.
    • [Assert.IsTrue(expression)] و [Assert.IsTrue(expression, message)]: بررسی می‌کنند که عبارت درست است.
    • [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 به یک شیء اشاره نمی‌کنند.
  • خط ۲۱: شرط باید برقرار شود
  • خط ۲۶: شرط باید شکست بخورد

بیایید پروژه را طوری پیکربندی کنیم که به جای یک فایل اجرایی .exe، یک فایل DLL تولید کند:

  • در [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

ما به تدریج راه‌حل ویژوال استودیوی زیر را خواهیم ساخت:

  • در [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 };
            }
            // خطایی برای گزارش؟
...
        }
    }
}
  • خط ۸: کدهای خطا که قبلاً در کلاس [FileImpotException] بودند، به کلاس [FileImpot] منتقل شده‌اند. این‌ها در واقع کدهای خطای مخصوص این پیاده‌سازی از رابط [IImpotDao] هستند.
  • خطوط ۲۶ و ۳۴: برای محصورسازی یک خطا، اکنون از کلاس [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);
            }
        }
    }
}
  • خط ۱۳: لایه [dao] توسط کلاس [FileImpot] پیاده‌سازی شده است
  • خط ۱۴: استثنای نوع [ImpotException] که ممکن است رخ دهد، مدیریت می‌شود.

فایل [DataImpot.txt] که برای تست لازم است، به‌طور خودکار به پوشهٔ زمان اجرای پروژه کپی می‌شود (به [5] در پروژه مراجعه کنید). پروژه [dao] شامل چندین کلاس است که هر یک متدی به نام [Main] دارند. بنابراین باید صراحتاً کلاسی را که هنگام درخواست کاربر برای اجرای پروژه با فشردن Ctrl-F5 باید اجرا شود، مشخص کنید:

  • در [1]: به ویژگی‌های پروژه دسترسی پیدا کنید
  • در [2]: مشخص کنید که این یک برنامه کنسول (console application) است
  • در [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]. خط ۱۳ از [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) را امکان‌پذیر می‌سازد.
  • خط ۱۰: اشاره‌ای به لایه [dao]
  • خطوط ۱۳–۱۶: سازنده، لایه [dao] را با کلاس [FileImpot] نمونه سازی می‌کند
  • خطوط ۱۹–۲۰: روش آزمون
  • خط ۲۲: آرایهٔ ردهٔ مالیاتی از لایهٔ [dao] بازیابی می‌شود
  • خطوط ۲۳–۲۵: این موارد مانند قبل نمایش داده می‌شوند. این نمایش در یک تست واحد واقعی ضروری نخواهد بود. در اینجا، هدف آموزشی دارد.
  • خط ۲۷: بررسی می‌کنیم که واقعاً ۷ رده مالیاتی وجود دارد
  • خطوط ۲۸–۳۰: مقادیر مربوط به رده مالیات شماره ۲ را بررسی می‌کنیم

برای اجرای این تست واحد، پروژه باید از نوع [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)));
        }
    }
}
  • خط ۱۴: ایجاد لایه‌های [metier] و [dao]. لایه [dao] با استفاده از کلاس [FileImpot] پیاده‌سازی شده است
  • خطوط ۱۲–۲۱: رسیدگی به یک استثنای احتمالی از نوع [ImpotException]
  • خطوط ۲۳–۲۶: فراخوانی‌های مکرر متد واحد 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));
        }
    }
}
  • خط ۱۴: ایجاد لایه‌های [metier] و [dao]. لایه [dao] با استفاده از کلاس [FileImpot] پیاده‌سازی شده است.
  • خطوط ۲۱–۲۴: فراخوانی‌های مکرر متد واحد 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);
            }

خط ۵ با نام‌گذاری صریح کلاس‌های پیاده‌سازی برای دو لایه، لایه‌های [dao] و [metier] را ایجاد می‌کند: FileImpot برای لایه [dao]، ImpotMetier برای لایه [metier]. اگر یکی از لایه‌ها با استفاده از یک کلاس جدید پیاده‌سازی شود، خط ۵ اصلاح خواهد شد. برای مثال:


                metier = new ImpotMetier(new HardwiredImpot());

به جز این تغییر، هیچ چیز دیگری در برنامه تغییر نخواهد کرد، زیرا هر لایه از طریق یک رابط با لایه بعدی ارتباط برقرار می‌کند. تا زمانی که رابط بدون تغییر باقی بماند، ارتباط بین لایه‌ها نیز بدون تغییر باقی می‌ماند. چارچوب Spring به ما این امکان را می‌دهد که استقلال لایه‌ها را یک قدم فراتر برده و نام کلاس‌های پیاده‌ساز لایه‌های مختلف را در یک فایل پیکربندی خارجی‌سازی کنیم. در این صورت، تغییر پیاده‌سازی یک لایه صرفاً مستلزم تغییر یک فایل پیکربندی است. این کار هیچ تأثیری بر کد برنامه ندارد.

در مثال بالا، لایه [ui] از Spring خواهد خواست تاآشکارسازی نام کلاس‌های پیاده‌ساز لایه‌های مختلف در یک فایل پیکربندی. بنابراین، تغییر پیاده‌سازی یک لایه صرفاً مستلزم تغییر یک فایل پیکربندی است. این امر هیچ تأثیری بر کد برنامه ندارد.در مثال بالا، لایه [ui] از [0] در Spring درخواست می‌کند تا لایه‌های [dao]، [1]، [metier] و [2] را بر اساس اطلاعات موجود در یک فایل پیکربندی، نمونه‌سازی کند. لایه [ui] سپس از Spring [3] درخواست مرجع لایه [metier] را خواهد کرد:


            //لایه‌ها ایجاد می‌شوند [metier et dao]
            IImpotMetier metier = null;
            try {
                // زمینهٔ بهار
                IApplicationContext ctx = ContextRegistry.GetContext();
                // درخواست ارجاع برای لایه [metier]
                metier = (IImpotMetier)ctx.GetObject("metier");
            } catch (Exception e1) {
...
}
  • خط ۵: نمونه‌سازی لایه‌های [dao] و [metier] توسط Spring
  • خط ۷: یک مرجع به لایه [metier] بازیابی می‌شود. توجه داشته باشید که لایه [ui] این مرجع را بدون مشخص کردن نام کلاسی که لایه [metier] را پیاده‌سازی می‌کند، در اختیار داشت.

چارچوب Spring در دو نسخه موجود است: Java و .NET. نسخه .NET در آدرس URL (مارس ۲۰۰۸) [http://www.springframework.net/] در دسترس است:

  • در [1]: وب‌سایت [Spring.net]
  • در [2]: صفحه دانلود
  • در [3]: دانلود Spring 1.1 (مارس ۲۰۰۸)
  • به [4]: نسخه .exe را دانلود کرده و سپس آن را نصب کنید
  • در [5]: پوشه‌ای که توسط نصب ایجاد شده است
  • در [6]: پوشه [bin/net/2.0/release] حاوی فایل‌های Spring DLL برای پروژه‌های Visual Studio نسخه 2.0 یا بالاتر است. اسپرینگ یک چارچوب غنی از امکانات است. جنبه‌ای از اسپرینگ که در اینجا برای مدیریت یکپارچه‌سازی لایه‌ها در یک برنامه استفاده خواهیم کرد، IoC: معکوس‌سازی کنترل، یا DI: تزریق وابستگی نامیده می‌شود. اسپرینگ کتابخانه‌هایی را برای دسترسی به پایگاه داده از طریق NHibernate و همچنین برای تولید و راه‌اندازی سرویس‌های وب، برنامه‌های وب و غیره فراهم می‌کند.
  • فایل‌های DLL مورد نیاز برای مدیریت یکپارچه‌سازی لایه‌ها در یک برنامه عبارتند از DLL، [7] و [8].

ما این سه فایل DLL را در یک پوشه [lib] درون پروژه خود ذخیره می‌کنیم:

  • [1]: سه فایل DLL با استفاده از Windows Explorer در پوشه [lib] قرار داده شده‌اند
  • [2]: در پروژه [ui]، ما همه فایل‌ها را نمایش می‌دهیم
  • [3]: پوشه [ui/lib] اکنون قابل مشاهده است. این پوشه در پروژه گنجانده شده است
  • [4]: پوشه [ui/lib] بخشی از پروژه است

ایجاد پوشه [lib] به هیچ وجه ضروری نیست. می‌توانست ارجاعات مستقیماً روی سه فایل DLL در پوشه [bin/net/2.0/release] از [Spring.net] ایجاد شوند. با این حال، ایجاد پوشه [lib] این امکان را فراهم می‌کند که برنامه روی دستگاهی که [Spring.net] را ندارد توسعه یابد و بدین ترتیب وابستگی آن به محیط توسعه موجود را کاهش می‌دهد.

ما ارجاعات به سه فایل جدید DLL را به پروژه [ui] اضافه می‌کنیم:

  • [1]: ما ارجاع‌هایی به سه فایل DLL در پوشه [lib] ایجاد می‌کنیم [2]
  • [3]: سه فایل DLL بخشی از مراجع پروژه هستند

بیایید به نمای کلی معماری برنامه بازگردیم:

در بالا، لایه [ui] از Spring خواهد خواست تاآشکارسازی لایه‌های [dao]، [1]، [metier] و [2] بر اساس اطلاعات موجود در یک فایل پیکربندی. لایه [ui] سپس از Spring [3] درخواست مرجع لایه [metier] را خواهد کرد. این موضوع در لایه [ui] با کد زیر منعکس خواهد شد:


            //لایه‌ها [metier et dao] ایجاد شده‌اند
            IImpotMetier metier = null;
            try {
                // زمینه بهار
                IApplicationContext ctx = ContextRegistry.GetContext();
                // یک مرجع برای لایه [metier] درخواست شده است
                metier = (IImpotMetier)ctx.GetObject("metier");
            } catch (Exception e1) {
...
}
  • خط ۵: نمونه‌سازی لایه‌های [dao] و [metier] توسط Spring
  • خط ۷: یک مرجع به لایه [metier] بازیابی می‌شود.

خط [5] بالا از فایل پیکربندی [App.config] پروژه Visual Studio استفاده می‌کند. در یک پروژهٔ C#، این فایل برای پیکربندی برنامه استفاده می‌شود. بنابراین [App.config] یک مفهوم Spring نیست، بلکه مفهومی از Visual Studio است که Spring از آن بهره می‌برد. Spring می‌تواند از فایل‌های پیکربندی دیگری غیر از [App.config] استفاده کند. بنابراین راه‌حل ارائه‌شده در اینجا تنها راه‌حل موجود نیست.

بیایید فایل [App.config] را با استفاده از جادوگر ویژوال استودیو ایجاد کنیم:

  • در [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>
  • خطوط ۱۱–۲۳: بخشی که با تگ <spring> محدود شده است، گروه بخش <spring> نامیده می‌شود. شما می‌توانید به هر تعداد که مایلید در [App.config] گروه بخش ایجاد کنید.
  • یک گروه بخش شامل بخش‌ها است: در اینجا نیز همین‌طور است:
    • خطوط ۱۲–۱۴: بخش <spring/context>
    • خطوط ۱۵–۲۲: بخش <spring/objects>
  • خطوط ۴–۹: منطقه <configSections> فهرست رسیدگی‌کننده‌ها را برای گروه‌های بخشی موجود در [App.config] تعریف می‌کند.
  • خطوط ۵–۸: لیست هانلدرها را برای بخش‌های گروه <spring> (name="spring") تعریف می‌کنند.
  • خط ۶: هاندر برای بخش <context> در گروه <spring>:
    • name: نام بخشی که مدیریت می‌شود
    • type: نام کلاسی که این بخش را در قالب NomClasse، NomDLL مدیریت می‌کند.
    • بخش <context> از گروه <spring> توسط کلاس [Spring.Context.Support.ContextHandler] مدیریت می‌شود که می‌توان آن را در DLL و [Spring.Core.dll] یافت.
  • خط ۷: هاندر بخش <objects> از گروه <spring>

خطوط ۴–۹ در یک فایل [App.config] با Spring استاندارد هستند. آنها به سادگی از یک پروژه به پروژه دیگر کپی می‌شوند.

  • خطوط ۱۲–۱۴: بخش <spring/context> را تعریف می‌کنند.
  • خط ۱۳: تگ <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] قرار دهیم.

خطوط ۱۲ تا ۱۴ در یک فایل [App.config] استاندارد که از Spring استفاده می‌کند، یکسان هستند. شما به سادگی آنها را از یک پروژه به پروژه دیگر کپی می‌کنید.

  • خطوط ۱۵–۲۲: کلاس‌هایی را که باید نمونه‌سازی شوند، تعریف می‌کنند. اینجاست که پیکربندی خاص یک برنامه انجام می‌شود. تگ <objects> بخش تعریف کلاس‌های نمونه‌سازی‌شده را جدا می‌کند.
  • خطوط ۱۶–۱۸: کلاسی را که باید برای لایه [dao] نمونه‌سازی شود، تعریف می‌کنند
    • خط ۱۶: هر ابجکتی که توسط Spring ایجاد می‌شود، در داخل تگ <object> قرار می‌گیرد. این تگ دارای یک ویژگی 'name' است که نام ابجکت ایجاد شده می‌باشد. این ویژگی است که برنامه از Spring درخواست یک مرجع می‌کند: «یک مرجع به ابجکتی به نام 'dao' به من بده». ویژگی type کلاس مورد نظر برای نمونه سازی را به صورت NomClasse, NomDLL تعریف می‌کند. بنابراین، خط ۱۶ یک شیء به نام «dao» را تعریف می‌کند که نمونه‌ای از کلاس «Dao.FileImpot» است و در پکیج «DLL» از «ImpotsV5-dao.dll» قرار دارد. توجه داشته باشید که نام کامل کلاس (شامل فضای نام) ارائه شده است و پسوند .dll در نام DLL مشخص نشده است.

یک کلاس را می‌توان با Spring به دو روش نمونه سازی کرد:

  1. از طریق یک سازندهٔ خاص که پارامترها به آن ارسال می‌شوند: این کاری است که در خطوط ۱۶–۱۸ انجام می‌شود.
  2. از طریق سازندهٔ پیش‌فرض بدون پارامتر. سپس شیء از طریق ویژگی‌های عمومی خود مقداردهی اولیه می‌شود: بنابراین تگ <object> شامل زیرتگ‌های <property> برای مقداردهی این ویژگی‌های مختلف است. در اینجا مثالی از این حالت نداریم.
  • (ادامه)
    • خط 16: کلاس نمونهٔ ایجادشده، کلاس FileImpot است. این کلاس دارای سازندهٔ زیر است:

        public FileImpot(string fileName);

پارامترهای سازنده با استفاده از تگ‌های <constructor-arg> تعریف می‌شوند.

  • خط ۱۷: اولین و تنها پارامتر کانستراکتور را تعریف می‌کند. ویژگی index شماره پارامتر کانستراکتور و ویژگی value مقدار آن است: <constructor-arg index="i" value="valuei"/>
  • خطوط ۱۹–۲۱: کلاسی را که باید برای لایه [metier] نمونه‌سازی شود، تعریف می‌کنند: کلاس [Metier.ImpotMetier]، که در داخل DLL [ImpotsV5-metier.dll] قرار دارد.
    • خط ۱۹: کلاس نمونه‌سازی‌شده، کلاس ImpotMetier است. این کلاس دارای سازندهٔ زیر است:

        public ImpotMetier(IImpotDao dao);
  • (ادامه)
    • خط ۲۰: اولین و تنها پارامتر سازنده را تعریف می‌کند. در مثال بالا، پارامتر سازنده dao یک مرجع شیء است. در این مورد، در داخل تگ <constructor-arg>، از ویژگی ref به جای ویژگی value که برای لایه [dao] استفاده شده بود، استفاده می‌شود: <constructor-arg index="i" ref="refi"/>. در کَنستراکتور بالا، پارامتر dao نماد یک نمونه روی لایه [dao] است. این نمونه توسط خطوط ۱۶ تا ۱۸ فایل پیکربندی تعریف شده است. بنابراین، در خط ۲۰:

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

ref="dao" نمایندهٔ شیء Spring با نام "dao" است که در خطوط 16–18 تعریف شده است.

برای خلاصه، فایل [App.config]:

  • لایه [dao] را با استفاده از کلاس FileImpot که DataImpot.txt را به‌عنوان پارامتر می‌پذیرد (خطوط 16–18) نمونه‌سازی می‌کند. شیء حاصل «dao» نامیده می‌شود.
  • لایه [metier] را با استفاده از کلاس ImpotMetier که شی «dao» قبلی را به‌عنوان پارامتر می‌پذیرد، ایجاد می‌کند (خطوط ۱۹–۲۱).

تنها کاری که باقی می‌ماند این است که از این فایل پیکربندی 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 {
                // زمینهٔ اسپرینگ
                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) {
....................................
  • خط ۲: IApplicationContext دسترسی به تمامی اشیایی را که توسط Spring ایجاد شده‌اند فراهم می‌کند. این شیء به عنوان زمینه Spring برنامه، یا به طور ساده‌تر، زمینه برنامه، شناخته می‌شود. در این مرحله، این زمینه هنوز инициалиزه نشده است. این کار توسط بلاک try/catch زیر انجام می‌شود.
  • خط ۵: پیکربندی Spring در [App.config] خوانده و پردازش می‌شود. پس از این عملیات، مشروط بر اینکه هیچ استثنایی رخ نداده باشد، همهٔ اشیاء در بخش <objects> ایجاد شده‌اند:
  • شیء «dao» اسپرینگ یک نمونه در لایه [dao] است
  • شیء «metier» از Spring یک نمونه در لایه [metier] است.
  • خط ۱۹: کلاس [Dialogue2.cs] به یک مرجع به لایه [metier] نیاز دارد. این مرجع از زمینهٔ برنامه درخواست می‌شود. شیء IApplicationContext دسترسی به اشیاء Spring را از طریق نام‌های آن‌ها فراهم می‌کند (ویژگی 'name' تگ <object> در پیکربندی Spring). مرجع بازگردانده‌شده، مرجعی به نوع عمومی Object است. سپس باید مرجع بازگشتی را به نوع صحیح تبدیل کنیم، که در این مورد نوع رابط لایه [metier] است: IImpotMetier.

اگر همه چیز به درستی پیش رفته باشد، پس از خط ۱۹، [Dialogue2.cs] حاوی مرجعی به لایه [metier] خواهد بود. کد از خط ۲۱ به بعد مربوط به کلاس [Dialogue.cs] است که قبلاً آن را بررسی کرده‌ایم.

  • خطوط ۶–۱۷: رسیدگی به استثنایی که زمانی رخ می‌دهد که فایل پیکربندی Spring نتواند به‌طور کامل پردازش شود. دلایل مختلفی برای این امر وجود دارد: نحو نادرست در خود فایل پیکربندی یا ناتوانی در ایجاد یکی از اشیاء پیکربندی‌شده. در مثال ما، سناریوی دوم زمانی رخ می‌دهد که فایل DataImpot.txt، که در خط 17 از [App.config] به آن ارجاع شده است، در پوشهٔ زمان اجرای پروژه پیدا نشود.

استثنایی که در خط ۶ پرتاب شده، یک زنجیره از استثناها است که در آن هر استثنا دو ویژگی دارد:

  • پیام: پیام خطای مرتبط با استثنا
  • InnerException: استثنای قبلی در زنجیرهٔ استثناها

حلقه در خطوط ۱۰–۱۴ تمام استثناها را در زنجیره به شکل زیر نمایش می‌دهد: کلاس استثنا و پیام مرتبط.

وقتی پروژه [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]
  • خط ۱۵: لایه [dao] این استثنا را در نوع [Entites.ImpotException] محصور می‌کند
  • خط ۹: استثنایی که توسط Spring پرتاب شده است زیرا نتوانسته است شی‌ای به نام «dao» را نمونه سازی کند. در طول فرآیند ایجاد این شی، دو استثنای دیگر قبلاً رخ داده بودند: آن‌هایی که در خطوط ۱۱ و ۱۳ هستند.
  • از آنجا که شیء «dao» ایجاد نشد، زمینهٔ برنامه (application context) نیز ایجاد نشد. این معنای استثنای خط ۵ است. پیش از این، یک استثنای دیگر، همان مورد خط ۷، رخ داده بود.
  • خط ۳: استثنای سطح بالا، آخرین مورد در زنجیره: یک خطای پیکربندی گزارش شده است.

از همه اینها می‌توان نتیجه گرفت که عمیق‌ترین استثنا - در این مورد، استثنای خط 17 - اغلب مهم‌ترین است. با این حال، باید توجه داشت که Spring پیام خطای خط 17 را حفظ کرده و آن را به استثنای سطح بالا در خط 3 منتقل کرده است تا علت اصلی خطا را در بالاترین سطح شناسایی کند.

اسپرینگ به تنهایی شایسته یک کتاب کامل است. ما در اینجا تنها به سطحی از این موضوع پرداخته‌ایم. شما می‌توانید با استفاده از سند [spring-net-reference.pdf]، که در پوشه نصب اسپرینگ یافت می‌شود، آن را با جزئیات بیشتری بررسی کنید:

 

شما همچنین ممکن است بخواهید [http://tahe.developpez.com/dotnet/springioc] را بخوانید، یک آموزش Spring که در زمینه VB.NET ارائه شده است.