محددات الأكواد Code Contracts (جديد دوت نت 4)


الكاتب الأخ : عبد العظيم بخاري

جاءت الدوت نت 4 بكلاسات جديدة ضمن namespace تدعى System.Diagnostics.Contracts .
فمع هذه الكلاسات التي تقدم امكانية البرمجة والتصميم باستعمال المحددات يمكنك تعريف شروط ومحددات مسبقة ولاحقة على الأكواد اضافة إلى محددات ثابتة invariants والتي يتم فحصها اثناء وقت التشغيل والتي تختلف عن المحددات والشروط السابقة بأنها تعالج وتحلل بشكل ثابت.

التصميم باستخدام المحددات والعقود هو فكرة مقتبسة عن اللغة البرمجية Eiffel وقد وضعتها مايكروسوفت في الإصدار الأحدث من الدوت نت 4 ضمن الnamespace التي ذكرناها System.Diagonstics.Contract والتي يمكن استعمالها في جميع لغات .NET.
تمكنك هذه الكلاسات من تعريف شروط ومحددات مسبقة ولاحقة ومحددات ثابتة invariants داخل ميثود ما .

تمثل المحددات السابقة عادة متطلبات الباراميترز اما المحددات اللاحقة فتمثل البيانات المرجعة returned data , فيما يتعلق بالمحددات الثابتة فهي التي تعرف متطلبات المتغيرات ضمن الميثود نفسها .
يمكن لمعلومات المحددات أن تترجم في كودات الdebug والrelease ويمكن ايضاً تعريف اسمبلي خاصة بالمحددات ويمكن عمل فحوصات للمحددات بشكل ستاتيكي دون الحاجة لتشغيل التطبيق .
يمكن ايضاً تعريف محددات للinterfaces والتي تفيد في تحديد كيفية عمل implementation للانترفيس.
يمكن لأدوات المحددات اعادة كتابة الأسمبلي لتدخل فحوصات المحددات داخل الكودات في وقت التشغيل runtime أو اثناء وقت الترجمة compile time وتضيف معلومات المحددات إلى توثيقات XML المنشئة.

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

وضع خيار Perform Runtime Contract Checking لFull سوف يعرف الرمز CONTRACTS_FULL ولأن عديد من دوال المحددات مذيلة بالattribute المسمى [Conditional( “ CONTRACTS_FULL “ )] فجميع فحوصات وقت التشغيل سوف تتم بهذا الخيار .

ملاحظة 1 : حتى تعمل مع محددات الأكواد يمكنك استعمال الكلاسات المتوفرة في .NET 4 ضمن System.Diagnostics.Contracts لكن ليس هناك اي اداة موجودة في فيجوال ستديو 2010 مخصصة لهذا الغرض لذلك يجب أن تحمل اضافة للفيجوال ستديو من Microsoft DevLabs على الرابط الاتي :
http://msdn.microsoft.com/en-us/devlabs/dd491992.
طبعاً تحتاج ل Visual Studio Team System في حال اردت عمل static analysis بهذه الأداة اما فيما يتعلق بruntime analysis فاصدار Visual Studio Standard edition يكفي .

ملاحظة 2 : لاحظ أن الصورة السابقة بها منطقة بالوسط فيها خيارات Static Checking . هذه الخيارات متوفرة فقط في اصدار Visual Studio 2010 Premium فما فوق . هذه الخيارات تعمل على تفعيل فحص وجود اخطاء المحددات اثناء وقت التصميم للبرنامج دون الحاجة للانتظار لوقت التشغيل حتى تظهر تلك الأخطاء .

ملاحظة 3 : يجب أن تضيف الرفرنس Microsoft.Contracts للمشروع حتى تعمل معك هذه الأداة.

تعرف محددات الأكواد عن طريق كلاس Contract . جميع شروط المحددات سواء كانت سابقة أو لاحقة يجب وضعها في بداية الميثود ويمكنك ايضاً اضافة global event handler للContractFiled Event والتي تستدعى عندما لا يتم تطبيق شروط المحدد اثناء وقت التشغيل . فاستدعاء SetHandled() مع باراميتر ContractFiledEventArgs سوف يوقف النمط الإفتراضي الذي يظهر عادة عند حدوث استثناء exception .

رمز برمجي:
Contract.ContractFailed += (sender, e) =>
{
Console.WriteLine(e.Message);
e.SetHandled();
};

بعد كل هذه المقدمة عن محددات الأكودات دعونا ندرس كيفية تطبيقها بامثلة مفيدة .

الشروط المسبقة PreConditions

تفحص الشروط السابقة الباراميترز التي تمرر لميثود ما . الميثود Requires() و الميثود

رمز برمجي:
Requires<TException>()

هما من الشروط السابقة التي يمكن تعريفها مع كلاس Contract .
يجب أن تعطي الميثود الأولى Requires قيمة منطقية Boolean ويمكنك اعطاءها باراميتر ثاني اختياري يحتوي على رسالة الخطأ التي تظهر في حال عدم تحقق الشرط .

يتطلب المثال التالي أن يكون الباراميتر min اقل أو يساوي الباراميتر max :

رمز برمجي:
//انظر كودات Program.cs اخر الدرس
static void MinMax(int min, int max)
{
Contract.Requires(min <= max);
//...
}

المحدد التالي يرمي ArgumentNullException في حال كان الباراميتر o هو null , يجب أن تعلم أنه لن يتم رمي الإستثناء في حال كان هناك event handler قام بوضع معالجة ل ContracFailed event . ايضاً اذا تم وضع اعداد التصريح عن فشل المحدد فإنه يتم عمل Trace.Assert() من اجل ايقاف البرنامج بدلاً من رمي استثناء Exception .

رمز برمجي:
static void Preconditions(object o)
{
Contract.Requires<ArgumentNullException>(o != null,
"Preconditions, o may not be null");
//...

الميثود Requires<TException>() ليست مذيلة بالattribute المدعو [Conditional(“CONTRACTS_FULL“)] , وايضاً لا تملك أي شرط على رمز DEBUG لذلك يتم هذا الفحص في وقت التشغيل في كل الأحوال .
هذه الميثود ايضاً ترمي استثناء معرف في حال لم يتحقق الشرط .

كنا عادة نفحص الarguments بجمل if الشرطية وبعدها يتم رمي استثناء Exception في حال عدم تحقق الشرط .
اما مع محددات الأكواد ليس هناك حاجة لاعادة كتابة التحقق ; فقط ضع سطر واحد .

رمز برمجي:
static void PrecondtionsWithUsuallyCode(object o)
{
if (o == null) throw new ArgumentNullException("o");
Contract.EndContractBlock();

تعرف الميثود EndContravtBlock() أن الكودات السابقة يجب أن تعالج كمحدد . اما في حال تم استخدام جمل محددات اخرى فهذه الميثود ليست ضرورية .

يقدم الكلاس Contract الميثود Exists() والميثود ForAll() من اجل فحص الCollections المستخدمة كباراميتر للميثود .
تفحص الميثود ForAll() كل عنصر في الCollection في حال تحقق الشرط ففي المثال التالي نفحص اذا كان كل عنصر في الcollection لديه قيمة اقل من 100 .

فيما يتعلق بالميثود Exists() فهي تفحص اذا كان هناك عنصر واحد على الأقل في الcollection يحقق الشرط.

رمز برمجي:
static void ArrayTest(int[] data)
{
Contract.Requires(Contract.ForAll(data, i => i < 100));

يوجد للميثود Exists() والميثود ForAll() اشكال اخرى من الoverloads تمكنك من تمرير رقمي integer وهما fromInclusive و toEnclusive بدلاً من IEnumerable<T> .

يمكن ايضاً تمرير نطاق من الأرقام (باستثناء toExclusive) إلى الdelegate المدعو Predicate<int> المعرف في الباراميتر الثالث.
يمكنك استعمال Exists() و ForAll() مع الشروط المسبقة واللاحقة ومع الشروط الثابتة Invariants.

الشروط اللاحقة PostConditions

تقدم الشروط اللاحقة ضمانات حول البيانات المشتركة والقيم المرجعة return values ويجب كتابة هذه الشروط في بداية الميثود وليس في نهايتها .
من الشروط اللاحقة الميثود Ensures() والميثود
EnsuresOnThrow<TException>()
يحدد المثال التالي أن المتغير shreadAmount يجب أن يكون اقل من 5000 في نهاية الميثود مع العلم أن هذه القيمة يمكن تغييرها في الوسط.

رمز برمجي:
//انظر كودات Program.cs اخر الدرس
private static int sharedAmount = 4000;
    static void Postcondition()
    {
      Contract.Ensures(sharedAmount < 5000);
      sharedAmount = 9000;
      Console.WriteLine("change sharedAmount invariant {0}", sharedAmount);
      sharedAmount = 3000;
      Console.WriteLine("before returning change it to a valid value {0}", sharedAmount);
    }

تضمن الميثود EnsuresOnThrow<TException>() أن الكمية المشتركة sharedAmount تحقق الشرط في حال تم رمي استثناء محدد.
حتى نضمن القيمة المرجعة return value يمكننا استعمال القيمة الخاصة
Result<T>
مع محدد Ensures() ولأننا هنا نملك قيمة int فيمكننا وضعها للميثود Result() لأن T هو نوع generic .
بعد كل هذا سوف يضمن لنا المحدد Ensures() أن القيمة المرجعة return value هي اقل من 5000.

رمز برمجي:
static int ReturnValue()
    {
      Contract.Ensures(Contract.Result<int>() < 5000);
      return 3;
    }

يمكنك ايضاً مقارنة قيمة ما مع القيمة القديمة لها وهذا يتم مع الميثود

رمز برمجي:
OldValue<T>()

والتي ترجع القيمة الأصلية لمدخلات الميثود من المتغيرات الممررة لها.
يتأكد المثال التالي أن المحدد يعرف أن الناتج المرجع

رمز برمجي:
(Contract.Result<int>())

هو اكبرمن القيمة القديمة القادمة من الباراميتر x

رمز برمجي:
(Contract.OldValue<int>(x))

كودات المثال :

رمز برمجي:
static int ReturnLargerThanInput(int x)
{
Contract.Ensures(Contract.Result<int>() > Contract.OldValue<int>(x));
return x + 3;
}

في حال ارجعت الميثود قيم باستخدام المعيار out بدلاً من جملة return العادية فيمكن تعريف الشروط باستخدام الميثود ValueAtReturned() .

يوجد في المثال التالي محدد يعرف أن المتغير x يجب أن يكون أكبر من 25 واصغر من 50 عندما يتم ارجاعه .
وايضاً يجب أن يكون باقي قسمة المتغير y على 5 هو صفر عن الإرجاع .

رمز برمجي:
static void OutParameters(out int x, out int y)
{
Contract.Ensures(Contract.ValueAtReturn<int>(out x) > 25 &&
Contract.ValueAtReturn<int>(out x) < 50);
Contract.Ensures(Contract.ValueAtReturn<int>(out y) % 5 == 0);
x = 30;
y = 10;
}

المحددات الثابتة Invariants
هذه المحددات تكون للمتغيرات اثناء تنفيذ الميثود . فكما تعلمنا سابقاً فالميثود Contract.Requires() تعرف الشروط السابقة والمتطلبات على القيم المدخلة للميثود (الباراميترز) وايضا رأينا كيف أن الميثود Contract.Ensures تعرف الشروط اللاحقة أي الشروط التي تكون على نهاية الميثود عندما يتم ارجاع قيم . هناك ايضاً ميثود Contract.Invariant() والذي يعرف الشروط التي يجب أن تحقق خلال زمن تنفيذ الميثود (وسطها).

رمز برمجي:
//انظر كودات Program.cs اخر الدرس
static void Invariant(ref int x)
{
Contract.Invariant(x > 5);
x = 3;
Console.WriteLine("invariant value: {0}", x);
x = 9;
}

المحددات على الinterfaces

كما نعلم أنه في الinterfaces يمكننا تعريف عدة methods و properties و والتي يجب على الكلاس الذي يشتق هذا الإنترفيس أن يقوم بعمل implement لها كلها .

لكنك مع تعريف الإنترفيس فلا يمكنك تحديد الطريقة التي يجب للانترفيس أن يتم عمل implementation له . اما الان اصبح هذا ممكناً مع محددات الأكواد code contracts .

بالنظر للانترفيس التالي IStudent الذي يعرف عدة properties ك FirstName و LastName و Age ويعرف ايضاً الميثود ChangeName() . كل هذه الأمور نعرفها لكن الجديد هو الattribute الذي يظهر قبل الإنترفيس وهو Contract() .
هذا الattribute يطبق على الإنترفيس IStudent ويعرف أن كلاس StudentContract هو المستخدم لعمل محددات أكواد لهذا الإنترفيس :

رمز برمجي:
//ملف IStudent.cs 
using System.Diagnostics.Contracts;[ContractClass(typeof(StudentContract))]
  public interface IStudent
  {
    string FirstName { get; set; }
    string LastName { get; set; }
    int Age { get; set; }
    void ChangeName(string firstName, string lastName);
  }

يقوم الكلاس StudentContract بعمل implement للانترفيس IStudent ويعرف محددات الاكواد لجميع الأعضاء .
PureAttribute تعني أن الميثود أو الproperty قد لا تغير حالة الinstance للكلاس . هذا يتم تعريفه داخل get للproperties لكن يمكن ايضاً تعريفه مع جميع الmethods التي لا يسمح بتغير الحالة لها state.

الget لكل من FirstName و LastName تعرف ايضاً أن الناتج يجب أن يكون string باستخدام Contract.Result() اما الget لل Age فتعرف أن الشرط اللاحق يجب أن يتأكد أن القيمة هي بين 0 و 120 .
الset لل FirstName و LastName تطلب أن تكون القيمة الممررة ليست null اما فيما يخص الset لل Age فتعرف أن الشرط السابق يجب أن يكون بين 0 و 120 .

رمز برمجي:
//ملف StudentContract.cs 
using System;
using System.Diagnostics.Contracts;  [ContractClassFor(typeof(IStudent))]
  public sealed class StudentContract : IStudent
  {
    string IStudent.FirstName
    {
      [Pure]
      get { return Contract.Result<String>(); }
      set { Contract.Requires(value != null); }
    }
    string IStudent.LastName
    {
      [Pure]
      get { return Contract.Result<String>(); }
      set { Contract.Requires(value != null); }
    }
    int IStudent.Age
    {
      [Pure]
      get
      {
        Contract.Ensures(Contract.Result<int>() > 0 && Contract.Result<int>() < 121);
        return Contract.Result<int>();
      }
      set
      {
        Contract.Requires(value > 0 && value < 121);
      }
    }
    void IStudent.ChangeName(string firstName, string lastName)
    {
      Contract.Requires(firstName != null);
      Contract.Requires(lastName != null);
    }
  }

الان اي كلاس يعمل implement للانترفيس IStudent يجب أن يحقق كافة محددات الأكواد المطلوبة .
كلاس Student الموضع هو عبارة عن تطبيق بسيط للانترفيس IStudent حيث يحقق كافة شروط المحددات .

رمز برمجي:
//ملف Student.cs 

  public class Student:IStudent
  {
    #region IStudent Members

    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }

    public void ChangeName(string firstName, string lastName)
    {
      this.FirstName = firstName;
      this.LastName = lastName;
    }

    #endregion
  }

عند استعمال الكلاس Student يجب أن تتحقق المحددات فمثلاً ليس من المسموح اعطاء قيمة null ل property

رمز برمجي:
//انظر كودات Program.cs اخر الدرس
var p = new Person { FirstName = "Abed", LastName = null }; // contract error

ايضاً ليس من المسوح اعطاء قيمة ليست بين (0-120) للAge.

رمز برمجي:
var p = new Person { FirstName = "Abed", LastName = "Bukhari" };
p.Age = 131; // contract error

لمزيد من المعلومات عن هذه الأداة اذهب ل Microsoft DevLabs على الرابط
http:// msdn.microsoft.com/devlabs

عبد العظيم بخاري
http://www.el-bukhari.com

السلام عليكم
في الكودين الأخيرين وضعت new Person بدلا من new Student بالخطأ
فيرجى الإنتباه
+

كود program.cs

using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Linq;
using System.Text;
using System.Diagnostics;

  class Program
  {
    static void Main()
    {
      Contract.ContractFailed += (sender, e) =>
      {
        Console.WriteLine(e.Message);
        // e.SetHandled();
      };

      // var p = new Student { FirstName = "Abed", LastName = null }; // contract error
      //var p = new Student { FirstName = "Abed", LastName = "Bukhari" };
      //p.Age = 131; // contract error

      //int x = 7;
      //Invariant(ref x);
      //Postcondition();

      //int[] data = { 3, 5, 7, 11 };
      //ArrayTest(data);

      // MinMax(7, 2);
      //Preconditions(null);
      //PrecondtionsWithUsuallyCode(null);

    }

    static int ReturnValue()
    {
      Contract.Ensures(Contract.Result<int>() < 5000);
      return 3;
    }

    static int ReturnLargerThanInput(int x)
    {
      Contract.Ensures(Contract.Result<int>() > Contract.OldValue<int>(x));
      return x + 3;
    }

    private static int sharedAmount = 4000;
    static void Postcondition()
    {
      Contract.Ensures(sharedAmount < 5000);
      sharedAmount = 9000;
      Console.WriteLine("change sharedAmount invariant {0}", sharedAmount);
      sharedAmount = 3000;
      Console.WriteLine("before returning change it to a valid value {0}", sharedAmount);
    }

    static void Invariant(ref int x)
    {
      Contract.Invariant(x > 5);
      x = 3;
      Console.WriteLine("invariant value: {0}", x);
      x = 9;
    }

    static void ArrayTest(int[] data)
    {
      Contract.Requires(Contract.ForAll(data, i => i < 100));

      // Contract.Requires(Contract.Exists(data, i => i < 5));
      Console.WriteLine("ArrayTest contract succeeded");
    }

    static void OutParameters(out int x, out int y)
    {
      Contract.Ensures(Contract.ValueAtReturn<int>(out x) > 25 && Contract.ValueAtReturn<int>(out x) < 50);
      Contract.Ensures(Contract.ValueAtReturn<int>(out y) % 5 == 0);
      x = 30;
      y = 10;
    }

    static void MinMax(int min, int max)
    {
      Contract.Requires(min <= max);
      Contract.Requires<ArgumentException>(min <= max);
    }

    static void Preconditions(object o)
    {
      Contract.Requires<ArgumentNullException>(o != null, "Preconditions, o may not be null");
      Console.WriteLine(o.GetType().Name);
    }

    static void Foo(object o)
    {
      Contract.Requires(o != null, "o is null!");
      Contract.Ensures(o != null);

      Console.WriteLine("Foo");
    }

    static void PrecondtionsWithUsuallyCode(object o)
    {
      if (o == null) throw new ArgumentNullException("o");
      Contract.EndContractBlock();
    }

  }
Advertisements
  1. لا توجد تعليقات حتى الأن.
  1. No trackbacks yet.

شاركنا برأيك ,لكي نرقى بالمدونة,يمكنك أضافة تعليق عن طريق حسابك في Facebook

إملأ الحقول أدناه بالمعلومات المناسبة أو إضغط على إحدى الأيقونات لتسجيل الدخول:

WordPress.com Logo

أنت تعلق بإستخدام حساب WordPress.com. تسجيل خروج   / تغيير )

صورة تويتر

أنت تعلق بإستخدام حساب Twitter. تسجيل خروج   / تغيير )

Facebook photo

أنت تعلق بإستخدام حساب Facebook. تسجيل خروج   / تغيير )

Google+ photo

أنت تعلق بإستخدام حساب Google+. تسجيل خروج   / تغيير )

Connecting to %s

%d مدونون معجبون بهذه: