مقدمه

تاکنون با وراثت از طریق کلاس‌های پایه و انتزاعی به عنوان روشی برای ایجاد سلسله مراتب و استفاده مجدد از کد آشنا شدیم. اکنون به یکی دیگر از قدرتمندترین ابزارهای برنامه‌نویسی شیءگرا در C# می‌رسیم: اینترفیس‌ها (Interfaces). یک اینترفیس، یک قرارداد (contract) یا یک مشخصات فنی است. اینترفیس مجموعه‌ای از اعضای عمومی (متدها، پراپرتی‌ها، رویدادها) را تعریف می‌کند، اما هیچ‌گونه پیاده‌سازی برای آن‌ها ارائه نمی‌دهد. هر کلاس یا ساختاری که تصمیم بگیرد این اینترفیس را "پیاده‌سازی" کند، متعهد می‌شود که برای تمام اعضای تعریف‌شده در آن قرارداد، یک پیاده‌سازی مشخص فراهم کند. اینترفیس‌ها ابزار اصلی برای دستیابی به اتصال سست (loose coupling) و چندریختی در سناریوهای پیچیده هستند.

اینترفیس (Interface) چیست؟

یک اینترفیس به این سؤال پاسخ می‌دهد که یک شیء "چه کارهایی می‌تواند انجام دهد؟" نه اینکه "چه چیزی است؟". وراثت یک رابطه‌ی "Is-A" (یک نوع از) را مدل‌سازی می‌کند، در حالی که اینترفیس یک رابطه‌ی "Can-Do" (قابلیت انجام کاری را دارد) را تعریف می‌کند. برای مثال، کلاس‌های Bird، Plane و Superman ممکن است هیچ وجه اشتراکی در سلسله مراتب وراثت خود نداشته باشند، اما همه‌ی آن‌ها قابلیت پرواز کردن را دارند. بنابراین، می‌توانیم یک اینترفیس به نام IFlyable تعریف کنیم که همه‌ی این کلاس‌ها آن را پیاده‌سازی کنند.

طبق یک قرارداد نوشتاری قوی در .NET، نام تمام اینترفیس‌ها با حرف بزرگ I شروع می‌شود.

تعریف و پیاده‌سازی یک اینترفیس

برای تعریف یک اینترفیس از کلمه‌ی کلیدی interface استفاده می‌کنیم. اعضای تعریف‌شده در یک اینترفیس به طور پیش‌فرض public هستند و نیازی به نوشتن سطح دسترسی برای آن‌ها نیست.

Copy Icon Program.cs
// Defining a contract for any type that can be logged.
public interface ILogger
{
    // Any class implementing ILogger MUST provide this method.
    void Log(string message);
}

اکنون هر کلاسی که بخواهد قابلیت لاگ کردن را داشته باشد، می‌تواند این اینترفیس را پیاده‌سازی کند. برای پیاده‌سازی، از همان سینتکس دو نقطه که برای وراثت استفاده می‌شد، بهره می‌بریم.

Copy Icon Program.cs
// First implementation: logs to the console.
public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine($"CONSOLE LOG: {message}");
    }
}

// Second implementation: logs to a file (simulated).
public class FileLogger : ILogger
{
    public void Log(string message)
    {
        System.IO.File.AppendAllText("log.txt", message + "\n");
        Console.WriteLine("Message logged to file.");
    }
}

در این مثال، دو کلاس کاملاً متفاوت، ConsoleLogger و FileLogger، هر دو قرارداد ILogger را پذیرفته و متد Log را به شیوه‌ی خود پیاده‌سازی کرده‌اند.

استفاده از اینترفیس‌ها برای چندریختی

زیبایی و قدرت اصلی اینترفیس‌ها در قابلیت آن‌ها برای ایجاد چندریختی است. شما می‌توانید متغیری از نوع یک اینترفیس تعریف کنید. این متغیر می‌تواند به هر شیئی از هر کلاسی که آن اینترفیس را پیاده‌سازی کرده باشد، ارجاع دهد.

Copy Icon Program.cs
// This method works with ANY object that implements ILogger.
void ProcessMessage(string text, ILogger logger)
{
    // It doesn't know or care about the concrete type (ConsoleLogger or FileLogger).
    logger.Log(text);
}

// --- Usage ---
ILogger consoleLogger = new ConsoleLogger();
ILogger fileLogger = new FileLogger();

ProcessMessage("First message", consoleLogger);
ProcessMessage("Second message", fileLogger);

متد ProcessMessage هیچ اطلاعی از نحوه‌ی پیاده‌سازی Log ندارد. او فقط می‌داند که شیء logger این قابلیت را دارد. این یعنی ما توانسته‌ایم منطق پردازش پیام را از نحوه‌ی لاگ کردن آن کاملاً جدا کنیم. اگر در آینده یک DatabaseLogger هم اضافه کنیم، متد ProcessMessage بدون هیچ تغییری با آن نیز کار خواهد کرد. این مفهوم اتصال سست (Loose Coupling) نام دارد و یکی از مهم‌ترین اصول در طراحی نرم‌افزارهای پایدار و قابل توسعه است.

پیاده‌سازی چندین اینترفیس

یکی از محدودیت‌های وراثت در C# این است که یک کلاس فقط می‌تواند از یک کلاس پایه ارث‌بری کند. اما یک کلاس می‌تواند هر تعداد اینترفیس که بخواهد را پیاده‌سازی کند. این ویژگی به یک کلاس اجازه می‌دهد تا چندین "قرارداد" یا "قابلیت" متفاوت را ارائه دهد.

Copy Icon Program.cs
public interface ISerializable
{
    string Serialize();
}

// This class implements two different contracts.
public class Report : ILogger, ISerializable
{
    public string Title { get; set; }

    // Implementation for ILogger
    public void Log(string message)
    {
        Console.WriteLine($"REPORT LOG: {message}");
    }
    
    // Implementation for ISerializable
    public string Serialize()
    {
        return $"<report>{Title}</report>";
    }
}

کلاس Report هم قابلیت لاگ شدن را دارد و هم قابلیت سریالایز شدن. این انعطاف‌پذیری با وراثت از کلاس‌ها ممکن نیست.

اینترفیس یا کلاس انتزاعی؟

انتخاب بین اینترفیس و کلاس انتزاعی یک تصمیم مهم در طراحی است.

  • از کلاس انتزاعی استفاده کنید زمانی که: می‌خواهید یک پیاده‌سازی مشترک و پیش‌فرض برای برخی از اعضا فراهم کنید و کلاس‌ها یک رابطه‌ی "Is-A" قوی با هم دارند. (مثلاً Dog و Cat هر دو Animal هستند).
  • از اینترفیس استفاده کنید زمانی که: می‌خواهید یک "قابلیت" یا "توانایی" را تعریف کنید که می‌تواند توسط کلاس‌های کاملاً نامرتبط پیاده‌سازی شود (رابطه‌ی "Can-Do"). یا زمانی که یک کلاس نیاز به ارائه‌ی چندین قرارداد متفاوت دارد.