درس سیزدهم – واسطها (Interfaces)
در این درس با واسطها در زبان C# آشنا خواهیم شد. اهداف این درس بشرح زیر میباشند :
1- آشنایی با مفهوم کلی واسطها
2- تعریف یک واسط
3- استفاده از یک interface
4- پیادهسازی ارثبری در interface ها
5- نکات مهم و پیشرفته
6- مثالی کاربردی از واسطها
7- منابع مورد استفاده
واسطها از لحاظ ظاهری بسیار شبیه به کلاس هستند با این تفاوت که دارای هیچ گونه پیادهسازی نمیباشند. تنها چیزی که در interface به چشم میخورد تعاریفی نظیر رخدادها، متدها، اندیکسرها و یا property ها است. یکی از دلایل اینکه واسطها تنها دارای تعاریف هستند و پیادهسازی ندارند آنست که یک interface میتوان توسط چندین کلاس یا property مورد ارثبری قرار گیرد، از اینرو هر کلاس یا property خواستار آنست که خود به پیادهسازی اعضا بپردازد.
حال باید دید چرا با توجه به اینکه interface ها دارای پیادهسازی نیستند مورد استفاده قرار میگیرند یا بهتر بگوئیم سودمندی استفاده از interface ها در چیست؟ تصور کنید که در یک برنامه با مولفههایی سروکار دارید که متغیرند ولی دارای فیلدها یا متدهایی با نامهای یکسانی هستند و باید نام این متدها نیز یکسان باشد. با استفاده از یک interface مناسب میتوان تنها متدها و یا فیلدهای مورد نظر را اعلان نمود و سپس کلاسها و یا property های مورد از آن interface ارثبری نمایند. در این حالت تمامی کلاسها و property ها دارای فیلدها و یا متدهایی همنام هستند ولی هر یک پیادهسازی خاصی از آنها را اعمال مینمایند.
نکته مهم دیگر درباره interface ها، استفاده و کاربرد آنها در برنامههای بزرگی است که برنامهها و یا اشیاؤ مختلفی در تماس و تراکنش (transact) هستند. تصور کنید کلاسی در یک برنامه با کلاسی دیگر در برنامهای دیگر در ارتباط باشد. فرض کنید این کلاس متدی دارد که مقداری از نوع int بازمیگرداند. پس از مدتی طراح برنامه به این نتیجه میرسد که استفاده از int پاسخگوی مشکلش نیست و باید از long استفاده نماید. حال شرایط را در نظر بگیرید که برای تغییر یک چنین مسئله سادهای چه مشکل بزرگی پیش خواهد آمد. تمامی فیلدهای مورتبط با این متد باید تغییر داده شوند. در ضمن از مسئله side effect نیز نمیتوان چشم پوشی کرد.( تاثیرات ناخواسته و غیر منتظره و یا به عبارتی پیش بینی نشده که متغیر یا فیلدی بر روی متغیر یا فیلدی دیگر اعمال میکند، در اصطلاح side effect گفته میشود.) حال فرض کنید که در ابتدا interface ای طراحی شده بود. درصورت اعمال جزئیترین تغییر در برنامه مشکل تبدیل int به long قابل حل بود، چراکه کاربر یا برنامه و در کل user برنامه در هنگام استفاده از یک interface با پیادهسازی پشت پرده آن کاری ندارد و یا بهتر بگوئیم امکان دسترسی به آن را ندارد. از اینرو اعمال تغییرات درون آن تاثیری بر رفتار کاربر نخواهد داشت و حتی کاربر از آن مطلع نیز نمیشود. در مفاهیم کلی شیء گرایی، interface ها یکی از مهمترین و کاربردی ترین اجزاء هستند که در صورت درک صحیح بسیار مفید واقع میشوند. یکی از مثالهای مشهود درباره interface ها (البته در سطحی پیشرفته تر و بالاتر) رابطهای کاربر گرافیکی (GUI) هستند. کاربر تنها با این رابط سروکار دارد و کاری به نحوه عملیات پشت پرده آن ندارد و اعمال تغییرات در پیادهسازی interface کاربر را تحت تاثیر قرار نمیدهد.
از دیدگاه تکنیکی، واسطها بسط مفهومی هستند که از آن به عنوان انتزاع (Abstract) یاد میکنیم. در کلاسهای انتزاعی (که با کلمه کلید abstract مشخص میشدند.) سازندة کلاس قدر بود تا فرم کلاس خود را مشخص نماید : نام متدها، نوع بازگشتی آنها و تعداد و نوع پارامتر آنها، اما بدون پیادهسازی بدنه متد. یک interface همچنین میتواند دارای فیلدهایی باشد که تمامی آنها static و final هستند. یک interface تنها یک فرم کلی را بدون پیادهسازی به نمایش میگذارد.
از این دیدگاه، یک واسط بیان میدارد که : " این فرم کلی است که تمامی کلاسهایی که این واسط را پیادهسازی میکنند، باید آنرا داشته باشند." از سوی دیگر کلاسها و اشیاء دیگری که از کلاسی که از یک واسط مشتق شده استفاده میکنند، میدانند که این کلاس حتماً تمامی متدها و اعضای واسط را پیادهسازی میکند و میتوانند به راحتی از آن متدها و اعضا استفاده نمایند. پس به طور کلی میتوانیم بگوئیم که واسطها بمنظور ایجاد یک پروتکل (protocol) بین کلاسها مورد استفاده قرار میگیرند. (همچنان که برخی از زبانهای برنامهسازی بجای استفاده از کلمه کلیدی interface از protocol استفاده مینمایند.)
به دلیل اینکه کلاسها و ساختارهایی که از interface ها ارثبری میکنند موظف به پیادهسازی و تعریف آنها هستند، قانون و قاعدهای در این باره ایجاد میگردد. برای مثال اگر کلاس A از واسط IDisposable ارثبری کند، این ضمانت بوجود میآید که کلاس A دارای متد Dispose() است، که تنها عضو interface نیز میباشد. هر کدی که میخواهد از کلاس A استفاده کند، ابتدا چک مینماید که آیا کلاس A واسط IDisposable را پیادهسازی نموده یا خیر. اگر پاسخ مثبت باشد آنگاه کد متوجه میشود که میتواند از متد A.Dispose() نیز استفاده نماید. در زیر نحوه اعلان یک واسط نمایش داده شده است.
interface IMyInterface
{
void MethodToImplement();
}
در این مثال نحوه اعلان واسطی با نام IMyInterface نشان داده شده است. یک قاعده (نه قانون!) برای نامگذاری واسطها آنست که نام واسطها را با "I" آغاز کنیم که اختصار کلمه interface است. در interface این مثال تنها یک متد وجود دارد. این متد میتوان هر متدی با انواع مختلف پارامترها و نوع بازگشتی باشد. توجه نمایید همانطور که گفته شد این متد دارای پیادهسازی نیست و تنها اعلان شده است. نکته دیگر که باید به ان توجه کنید آنست که این متد به جای داشتن {} به عنوان بلوک خود، دارای ; در انتهای اعلان خود میباشد. علت این امر آنست که interface تنها نوع بازگشتی و پارامترهای متد را مشخص مینماید و کلاس یا شیای که از آن ارث میبرد باید آنرا پیادهسازی نماید. مثال زیر نحوه استفاده از این واسط را نشان میدهد.
مثال 1-13 : استفاده از واسطها و ارثبری از آنها
class InterfaceImplementer : IMyInterface
{
static void Main()
{
InterfaceImplementer iImp = new InterfaceImplementer();
iImp.MethodToImplement();
}
public void MethodToImplement()
{
Console.WriteLine("MethodToImplement() called.");
}
}
در این مثال، کلاس InterfaceImplementer همانند ارثبری از یک کلاس، از واسط IMyInterface ارثبری کرده است. حال که این کلاس از واسط مورد نظر ارثبری کرده است، باید، توجه نمایید باید، تمامی اعضای آنرا پیادهسازی کند. در این مثال این عمل با پیادهسازی تنها عضو واسط یعنی متد MethodToImplement() انجام گرفته است. توجه نمایید که پیادهسازی متد باید دقیقا از لحاظ نوع بازگشتی و تعداد و نوع پارامترها شبیه به اعلان موجود در واسط باشد، کوچکترین تغییری باعث ایجاد خطای کامپایلر میشود. مثال زیر نحوه ارثبری واسطها از یکدیگر نیز نمایش داده شده است.
مثال 2-13 : ارثبری واسطها از یکدیگر
using System;
interface IParentInterface
{
void ParentInterfaceMethod();
}
interface IMyInterface : IParentInterface
{
void MethodToImplement();
}
class InterfaceImplementer : IMyInterface
{
static void Main()
{
InterfaceImplementer iImp = new InterfaceImplementer();
iImp.MethodToImplement();
iImp.ParentInterfaceMethod();
}
public void MethodToImplement()
{
Console.WriteLine("MethodToImplement() called.");
}
public void ParentInterfaceMethod()
{
Console.WriteLine("ParentInterfaceMethod() called.");
}
}
مثال 2-13 دارای 2 واسط است : یکی IMyInterface و واسطی که از آن ارث میبرد یعنی IParentInterface. هنگامیکه واسطی از واسط دیگری ارثبری میکند، کلاس یا ساختاری که این واسطها را پیادهسازی میکند، باید تمامی اعضای واسطهای موجود در سلسله مراتب ارثبری را پیادهسازی نماید. در مثال 2-13، چون کلاس InterfaceImplementer از واسط IMyInterface ارثبری نموده، پس از واسط IParentInterface نیز ارثبری دارد، از اینرو باید کلیه اعضای این دو واسط را پیادهسازی نماید.
چند نکته مهم :
1- با استفاده از کلمه کلید interface در حقیقت یک نوع مرجعی (Reference Type) جدید ایجاد نمودهاید.
2- از لحاظ نوع ارتباطی که واسطها و کلاسها در ارثبری ایجاد مینمایند باید به این نکته اشاره کرد که، ارثبری از کلاس رابطه "است" یا "بودن" (is-a relation) را ایجاد میکند (ماشین یک وسیله نقلیه است) ولی ارثبری از یک واسط یا interface نوع خاصی از رابطه، تحت عنوان "پیادهسازی" (implement relation) را ایجاد میکند. ("میتوان ماشین را با وام بلند مدت خرید" که در این جمله ماشین میتواند خریداری شدن بوسیله وام را پیادهسازی کند.)
3- فرم کلی اعلان interface ها بشکل زیر است :
[attributes] [access-modifier] interface interface-name [:base-list]{interface-body}
که در اعضای آن بشرح زیر می باشند :
attributes : صفتهای واسط
access-modifiers : private یا public سطح دسترسی به واسط از قبیل
interface-name : نام واسط
:base-list : لیست واسطهایی که این واسط آنها را بسط میدهد.
Interface-body : بدنه واسط که در آن اعضای آن مشخص میشوند
توجه نمایید که نمیتوان یک واسط را بصورت virtual اعلان نمود.
4- هدف از ایجاد یک interface تعیین توانائیهاییست که میخواهیم در یک کلاس وجود داشته باشند.
5- به مثالی در زمینه استفاده از واسطها توجه کنید :
فرض کنید میخواهید واسطی ایجاد نمایید که متدها و property های لازم برای کلاسی را که میخواهد قابلیت خواندن و نوشتن از/به یک پایگاه داده یا هر فایلی را داشته باشد، توصیف نماید. برای این منظور میتوانید از واسط IStorable استفاده نمایید.
در این واسط دو متد Read() و Write() وجود دارند که در بدنه واسط تعریف میشوند ک
interface IStorable
{
void Read( );
void Write(object);
}
حال میخواهید کلاسی با عنوان Document ایجاد نمایید که این کلاس باید قابلیت خواندن و نوشتن از/به پایگاه داده را داشته باشد، پس میتوانید کلاس را از روی واسط IStorable پیادهسازی کنید.
public class Document : IStorable
{
public void Read( ) {...}
public void Write(object obj) {...}
// ...
}
حال بعنوان طراح برنامه، شما وظیفه داری تا به پیادهسازی این واسط بپردازید، بطوریکه کلیه نیازهای شما را برآورده نماید. نمونهای از این پیادهسازی در مثال 3-13 آورده شده است.
مثال 3-13 : پیادهسازی واسط و ارثبری – مثال کاربردی
using System;
// interface اعلان
interface IStorable
{
void Read( );
void Write(object obj);
int Status { get; set; }
}
public class Document : IStorable
{
public Document(string s)
{
Console.WriteLine("Creating document with: {0}", s);
}
public void Read( )
{
Console.WriteLine("Implementing the Read Method for IStorable");
}
public void Write(object o)
{
Console.WriteLine("Implementing the Write Method for IStorable");
}
public int Status
{
get
{
return status;
}
set
{
status = value;
}
}
private int status = 0;
}
public class Tester
{
static void Main( )
{
Document doc = new Document("Test Document");
doc.Status = -1;
doc.Read( );
Console.WriteLine("Document Status: {0}", doc.Status);
IStorable isDoc = (IStorable) doc;
isDoc.Status = 0;
isDoc.Read( );
Console.WriteLine("IStorable Status: {0}", isDoc.Status);
}
}
خروجی برنامه نیز بشکل زیر است :
Output:
Creating document with: Test Document
Implementing the Read Method for IStorable
Document Status: -1
Implementing the Read Method for IStorable
IStorable Status: 0
6- در مثال فوق توجه نمایید که برای متدها واسط IStorable هیچ سطح دسترسی (public,private و ...) در نظر گرفته نشده است. در حقیقت تعیین سطح دسترسی باعث ایجاد خطا میشود چراکه هدف اصلی از ایجاد یک واسط ایجاد شیء است که تمامی اعضای آن برای تمامی کلاسها قابل دسترسی باشند.
7- توجه نمایید که از روی یک واسط نمیتوان نمونهای جدید ایجاد کرد بلکه باید کلاسی از آن ارثبری نماید.
8- کلاسی که از واسط ارثبری میکند باید تمامی متدهای آنرا دقیقا همان گونه که در واسط مشخص شده پیادهسازی نماید. به بیان کلی، کلاسی که از یک واسط ارث میبرد، فرم و ساختار کلی خود را از واسط میگیرد و نحوه رفتار و پیادهسازی آنرا خود انجام میدهد.
خلاصه :
در این درس با مفاهیم کلی و اصلی درباره واسطها آشنا شدید. هم اکنون میدانید که واسطها چه هستند و سودمندی استفاده از آنها چیست. همچنین نحوه پیادهسازی واسط و ارثبری از آنرا آموختید.
مبحث واسطها بسیار گسترده و مهم است و امید است در بخشهای آینده در سایت، بتوانم تمامی مطالب را بطور حرفهای و کامل در اختیار شما قرار دهم.