آموزش برنامه‌نویسی شی‌گرا (OOP) در C#

فهرست مطالب

آموزش برنامه‌نویسی شی‌گرا (OOP) در C#

برنامه‌نویسی شی‌گرا (Object-Oriented Programming یا OOP) یک پارادایم برنامه‌نویسی قدرتمند است که بر مفهوم “اشیاء” استوار است. این اشیاء می‌توانند حاوی داده‌ها به شکل فیلدها (attributes) و کدها به شکل رویه‌ها (methods) باشند. هدف اصلی OOP، سازماندهی و مدیریت کد به گونه‌ای است که باعث افزایش خوانایی، قابلیت نگهداری، مقیاس‌پذیری و قابلیت استفاده مجدد (reusability) شود.

در دنیای مدرن توسعه نرم‌افزار، C# به عنوان یکی از زبان‌های اصلی در اکوسیستم دات‌نت، به طور کامل از اصول برنامه‌نویسی شی‌گرا پشتیبانی می‌کند. درک عمیق این اصول برای هر توسعه‌دهنده‌ی C# حیاتی است تا بتواند سیستم‌های پیچیده و قوی را طراحی و پیاده‌سازی کند. این راهنمای جامع، شما را با تمام مفاهیم کلیدی OOP در C# آشنا خواهد کرد؛ از تعاریف پایه تا اصول پیشرفته و الگوهای طراحی.

ما نه تنها به توضیح تئوری خواهیم پرداخت، بلکه با ارائه‌ی مثال‌های کد عملی و تشریح دقیق، به شما کمک می‌کنیم تا درک جامعی از نحوه پیاده‌سازی هر اصل در C# پیدا کنید. این آموزش برای برنامه‌نویسانی طراحی شده است که با مبانی C# آشنایی دارند و می‌خواهند دانش خود را در زمینه برنامه‌نویسی شی‌گرا به سطح بالاتری ارتقا دهند.

مفاهیم بنیادی: کلاس‌ها و اشیاء

هسته اصلی برنامه‌نویسی شی‌گرا، مفاهیم کلاس و شیء است. بدون درک این دو، نمی‌توانیم به سراغ اصول پیشرفته‌تر برویم.

کلاس (Class) چیست؟

یک کلاس را می‌توان به عنوان یک نقشه (blueprint) یا قالب (template) برای ایجاد اشیاء تعریف کرد. این نقشه، ساختار و رفتار (داده‌ها و توابع) اشیائی را که از آن ساخته می‌شوند، مشخص می‌کند. کلاس‌ها خودشان داده نیستند، بلکه فقط نحوه ایجاد و سازماندهی داده‌ها و توابع مرتبط با آن‌ها را توصیف می‌کنند.

به عنوان مثال، فرض کنید می‌خواهیم در مورد “خودرو” اشیائی را مدل‌سازی کنیم. کلاس Car می‌تواند ویژگی‌هایی مانند رنگ، مدل، سال ساخت، و رفتارهایی مانند روشن کردن موتور، ترمز گرفتن یا شتاب گرفتن را داشته باشد. این کلاس به تنهایی یک خودرو نیست، بلکه فقط چگونگی ساخته شدن یک خودرو را تعریف می‌کند.

شیء (Object) چیست؟

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

با ادامه مثال خودرو، اگر کلاس Car را داشته باشیم، می‌توانیم اشیائی مانند myCar (یک پراید سفید مدل ۱۳۹۰) و yourCar (یک بی‌ام‌و مشکی مدل ۲۰۱۸) را ایجاد کنیم. هر یک از این‌ها اشیائی مجزا هستند که از همان کلاس Car ساخته شده‌اند، اما مقادیر ویژگی‌هایشان متفاوت است.

ساختار یک کلاس در C#

یک کلاس در C# معمولاً شامل موارد زیر است:

  • فیلدها (Fields): متغیرهایی که داده‌های مربوط به وضعیت شیء را نگهداری می‌کنند.
  • پراپرتی‌ها (Properties): مکانیزمی برای دسترسی ایمن به فیلدها. پراپرتی‌ها از متدهای get و set برای خواندن و نوشتن مقادیر استفاده می‌کنند و کنترل بیشتری بر نحوه دسترسی به داده‌ها فراهم می‌کنند.
  • متدها (Methods): توابعی که رفتار شیء را تعریف می‌کنند. این متدها عملیاتی را روی داده‌های شیء انجام می‌دهند.
  • سازنده‌ها (Constructors): متدهای خاصی که هنگام ایجاد یک شیء از کلاس فراخوانی می‌شوند. مسئولیت سازنده، مقداردهی اولیه به وضعیت شیء است.

مثال عملی از کلاس و شیء در C#:


using System;

// تعریف کلاس Car
public class Car
{
    // فیلدها (معمولا private هستند و از طریق Properties به آن‌ها دسترسی پیدا می‌کنیم)
    private string _model;
    private string _color;
    private int _year;

    // پراپرتی‌ها
    public string Model
    {
        get { return _model; }
        set { _model = value; }
    }

    public string Color
    {
        get { return _color; }
        set { _color = value; }
    }

    public int Year
    {
        get { return _year; }
        set
        {
            if (value > 1900 && value <= DateTime.Now.Year + 1) // اعتبار سنجی ساده
            {
                _year = value;
            }
            else
            {
                Console.WriteLine("سال ساخت نامعتبر است.");
                _year = 0; // یا مقدار پیش‌فرض دیگری
            }
        }
    }

    // سازنده (Constructor)
    public Car(string model, string color, int year)
    {
        this.Model = model; // استفاده از پراپرتی برای مقداردهی
        this.Color = color;
        this.Year = year;
    }

    // متد (Method)
    public void StartEngine()
    {
        Console.WriteLine($"{Model} با رنگ {Color} در حال روشن کردن موتور است.");
    }

    public void DisplayCarInfo()
    {
        Console.WriteLine($"مدل: {Model}, رنگ: {Color}, سال ساخت: {Year}");
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        // ایجاد اشیاء (Instances) از کلاس Car
        Car myCar = new Car("Pride", "White", 1390);
        Car yourCar = new Car("BMW X5", "Black", 2018);
        Car invalidCar = new Car("Tesla", "Red", 1800); // سال نامعتبر

        // دسترسی به پراپرتی‌ها و فراخوانی متدها
        myCar.DisplayCarInfo();
        myCar.StartEngine();

        yourCar.DisplayCarInfo();
        yourCar.StartEngine();

        invalidCar.DisplayCarInfo(); // نمایش سال نامعتبر یا 0
    }
}

در مثال بالا، Car یک کلاس است. myCar و yourCar نمونه‌هایی (اشیاء) از کلاس Car هستند. هر شیء دارای مقادیر خاص خود برای Model، Color و Year است و می‌تواند متدهای StartEngine() و DisplayCarInfo() را فراخوانی کند.

اصل اول: Encapsulation (کپسوله‌سازی)

کپسوله‌سازی یکی از چهار اصل اساسی OOP است که به معنای "بسته‌بندی" داده‌ها و متدها در یک واحد (کلاس) و "پنهان‌سازی" جزئیات داخلی از دنیای بیرون است. این اصل تضمین می‌کند که داده‌های یک شیء محافظت شده‌اند و فقط از طریق یک رابط کنترل شده (متدها یا پراپرتی‌ها) قابل دسترسی و تغییر هستند.

چرا Encapsulation مهم است؟

  • حفظ یکپارچگی داده‌ها (Data Integrity): با کنترل دسترسی، می‌توانیم اطمینان حاصل کنیم که داده‌ها فقط به روش‌های معتبر تغییر می‌کنند و از ورود داده‌های نامعتبر جلوگیری کنیم (مانند اعتبارسنجی سال ساخت در مثال بالا).
  • کاهش وابستگی (Reduced Coupling): جزئیات پیاده‌سازی داخلی یک کلاس از دنیای بیرون پنهان می‌ماند. اگر این جزئیات تغییر کنند، کدهای خارجی که از آن کلاس استفاده می‌کنند، تحت تأثیر قرار نمی‌گیرند، تا زمانی که رابط عمومی (public interface) کلاس ثابت بماند.
  • افزایش قابلیت نگهداری (Increased Maintainability): وقتی کپسوله‌سازی رعایت شود، هر کلاس به یک واحد مستقل تبدیل می‌شود که تغییرات در آن، تأثیر کمی بر سایر بخش‌های سیستم دارد.
  • افزایش خوانایی و سادگی: با مخفی کردن پیچیدگی‌های داخلی، کلاس‌ها آسان‌تر قابل درک و استفاده می‌شوند.

پیاده‌سازی Encapsulation در C#

C# کپسوله‌سازی را عمدتاً از طریق Access Modifiers (تعیین‌کننده‌های دسترسی) و Properties (پراپرتی‌ها) پیاده‌سازی می‌کند.

Access Modifiers (تعیین‌کننده‌های دسترسی)

این کلمات کلیدی مشخص می‌کنند که اعضای یک کلاس (فیلدها، متدها، پراپرتی‌ها و ...) از کجا قابل دسترسی هستند:

  • public: دسترسی نامحدود. عضو از هر جایی قابل دسترسی است.
  • private: دسترسی فقط از داخل کلاس تعریف‌کننده. این پیش‌فرض برای اعضای کلاس است.
  • protected: دسترسی فقط از داخل کلاس تعریف‌کننده و کلاس‌های مشتق شده (فرزندان).
  • internal: دسترسی فقط از داخل اسمبلی جاری.
  • protected internal: دسترسی از داخل اسمبلی جاری و از کلاس‌های مشتق شده (حتی اگر در اسمبلی دیگری باشند).
  • private protected: دسترسی فقط از داخل کلاس تعریف‌کننده یا کلاس‌های مشتق شده در همان اسمبلی. (از C# 7.2 به بعد)

برای پیاده‌سازی کپسوله‌سازی، معمولاً فیلدها را private تعریف می‌کنیم تا مستقیماً از بیرون قابل دسترسی نباشند و سپس از پراپرتی‌های public برای خواندن و نوشتن آن‌ها استفاده می‌کنیم.

Properties (پراپرتی‌ها)

پراپرتی‌ها متدهایی "خاص" هستند که شبیه به فیلدها به نظر می‌رسند اما در واقع شامل یک متد get (برای خواندن) و/یا یک متد set (برای نوشتن) هستند. این متدها به ما اجازه می‌دهند منطق اعتبارسنجی یا سایر عملیات را هنگام دسترسی به داده‌ها اضافه کنیم.

مثال کامل‌تر از کپسوله‌سازی:


using System;

public class BankAccount
{
    private decimal _balance; // فیلد خصوصی برای نگهداری موجودی

    // پراپرتی عمومی برای دسترسی به نام حساب‌دار
    public string AccountHolderName { get; set; } // Auto-implemented property (ساده‌ترین شکل)

    // پراپرتی عمومی برای دسترسی کنترل‌شده به موجودی
    public decimal Balance
    {
        get { return _balance; } // متد get: فقط مقدار موجودی را برمی‌گرداند
        private set // متد set: خصوصی است تا فقط از داخل کلاس قابل تغییر باشد
        {
            if (value >= 0)
            {
                _balance = value;
            }
            else
            {
                Console.WriteLine("موجودی نمی‌تواند منفی باشد.");
            }
        }
    }

    // سازنده
    public BankAccount(string accountHolderName, decimal initialBalance)
    {
        AccountHolderName = accountHolderName;
        Balance = initialBalance; // از set پراپرتی استفاده می‌شود
    }

    // متد برای واریز پول
    public void Deposit(decimal amount)
    {
        if (amount > 0)
        {
            Balance += amount; // استفاده از set پراپرتی
            Console.WriteLine($"{amount} واحد پول به حساب {AccountHolderName} واریز شد. موجودی جدید: {Balance}");
        }
        else
        {
            Console.WriteLine("مقدار واریزی باید مثبت باشد.");
        }
    }

    // متد برای برداشت پول
    public bool Withdraw(decimal amount)
    {
        if (amount > 0 && Balance >= amount)
        {
            Balance -= amount; // استفاده از set پراپرتی
            Console.WriteLine($"{amount} واحد پول از حساب {AccountHolderName} برداشت شد. موجودی جدید: {Balance}");
            return true;
        }
        else if (amount <= 0)
        {
            Console.WriteLine("مقدار برداشتی باید مثبت باشد.");
            return false;
        }
        else
        {
            Console.WriteLine($"موجودی کافی نیست. موجودی فعلی: {Balance}");
            return false;
        }
    }

    public void DisplayAccountInfo()
    {
        Console.WriteLine($"صاحب حساب: {AccountHolderName}, موجودی: {Balance}");
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        BankAccount myAccount = new BankAccount("علی احمدی", 1000m);
        myAccount.DisplayAccountInfo(); // صاحب حساب: علی احمدی, موجودی: 1000

        myAccount.Deposit(500m);       // 500 واحد پول به حساب علی احمدی واریز شد. موجودی جدید: 1500
        myAccount.Withdraw(200m);      // 200 واحد پول از حساب علی احمدی برداشت شد. موجودی جدید: 1300
        myAccount.Withdraw(1500m);     // موجودی کافی نیست. موجودی فعلی: 1300

        // تلاش برای دسترسی مستقیم به فیلد خصوصی _balance (امکان‌پذیر نیست و خطای کامپایل می‌دهد)
        // myAccount._balance = 5000m; 

        // تلاش برای تغییر Balance از بیرون با set (به دلیل private بودن set، امکان‌پذیر نیست)
        // myAccount.Balance = 2000m; // این خط باعث خطای کامپایل می‌شود

        // اما خواندن Balance امکان‌پذیر است
        Console.WriteLine($"موجودی نهایی: {myAccount.Balance}"); // موجودی نهایی: 1300

        myAccount.Deposit(-100m); // مقدار واریزی باید مثبت باشد.
        myAccount.Withdraw(0m);   // مقدار برداشتی باید مثبت باشد.
    }
}

در این مثال، _balance یک فیلد private است که مستقیماً قابل دسترسی نیست. پراپرتی Balance دارای یک get عمومی و یک set خصوصی است. این بدان معناست که موجودی را می‌توان از بیرون خواند (myAccount.Balance) اما نمی‌توان مستقیماً از بیرون تغییر داد (myAccount.Balance = 2000m;). تغییر موجودی فقط از طریق متدهای Deposit و Withdraw که دارای منطق اعتبارسنجی هستند، انجام می‌شود. این نمونه‌ای عالی از کپسوله‌سازی و محافظت از داده‌ها است.

اصل دوم: Inheritance (وراثت)

وراثت یکی از اصول کلیدی OOP است که به کلاس‌ها اجازه می‌دهد تا ویژگی‌ها و رفتارهای کلاس دیگری را به ارث ببرند. این اصل مفهوم سلسله مراتب (hierarchy) "IS-A" (هست یک) را ایجاد می‌کند، به این معنی که "یک کلاس فرزند، یک کلاس والد است". وراثت به قابلیت استفاده مجدد کد (code reusability) کمک شایانی می‌کند و باعث افزایش سازماندهی کد می‌شود.

چرا Inheritance مهم است؟

  • قابلیت استفاده مجدد کد (Code Reusability): کدهای مشترک (فیلدها، پراپرتی‌ها، متدها) می‌توانند در کلاس والد تعریف شوند و توسط تمام کلاس‌های فرزند به ارث برده شوند، بدون نیاز به نوشتن مجدد.
  • کاهش تکرار کد (Reduced Code Duplication): با به ارث بردن، از نوشتن کدهای تکراری در کلاس‌های مختلف جلوگیری می‌شود.
  • توسعه‌پذیری (Extensibility): می‌توان به راحتی قابلیت‌های جدید را به سیستم اضافه کرد. با ایجاد کلاس‌های فرزند جدید، می‌توان رفتارهای خاص را اضافه یا رفتارهای موجود را تغییر داد.
  • ایجاد سلسله مراتب منطقی: کلاس‌ها می‌توانند در یک ساختار درختی سازماندهی شوند که منعکس‌کننده روابط واقعی بین موجودیت‌ها است.

پیاده‌سازی Inheritance در C#

در C#، یک کلاس می‌تواند فقط از یک کلاس والد به ارث ببرد (وراثت یگانه). برای نشان دادن وراثت، از علامت : (کولون) بعد از نام کلاس فرزند و سپس نام کلاس والد استفاده می‌شود.

  • کلاس والد (Base Class/Parent Class): کلاسی که ویژگی‌ها و رفتارها را برای کلاس‌های فرزند فراهم می‌کند.
  • کلاس فرزند (Derived Class/Child Class/Subclass): کلاسی که از یک کلاس والد به ارث می‌برد و می‌تواند ویژگی‌ها و رفتارهای جدیدی اضافه کند یا رفتارهای موجود را تغییر دهد.

مثال عملی از وراثت:


using System;

// کلاس والد: Person (فرد)
public class Person
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    public int Age { get; set; }

    public Person(string firstName, string lastName, int age)
    {
        FirstName = firstName;
        LastName = lastName;
        Age = age;
    }

    public void Greet()
    {
        Console.WriteLine($"سلام، من {FirstName} {LastName} هستم.");
    }

    public virtual void DisplayInfo() // متد virtual تا بتوان در کلاس‌های فرزند آن را override کرد
    {
        Console.WriteLine($"نام: {FirstName} {LastName}, سن: {Age}");
    }
}

// کلاس فرزند: Student (دانشجو) از Person به ارث می‌برد
public class Student : Person
{
    public string StudentId { get; set; }
    public string Major { get; set; }

    // سازنده کلاس فرزند. باید سازنده کلاس والد را فراخوانی کند.
    public Student(string firstName, string lastName, int age, string studentId, string major)
        : base(firstName, lastName, age) // فراخوانی سازنده کلاس والد با استفاده از base
    {
        StudentId = studentId;
        Major = major;
    }

    // متد جدید مختص کلاس Student
    public void Study()
    {
        Console.WriteLine($"{FirstName} {LastName} در حال مطالعه درس {Major} است.");
    }

    // Override کردن متد DisplayInfo از کلاس والد
    public override void DisplayInfo()
    {
        // فراخوانی متد DisplayInfo از کلاس والد برای استفاده از منطق موجود
        base.DisplayInfo();
        Console.WriteLine($"شماره دانشجویی: {StudentId}, رشته تحصیلی: {Major}");
    }
}

// کلاس فرزند: Teacher (معلم) از Person به ارث می‌برد
public class Teacher : Person
{
    public string Subject { get; set; }
    public decimal Salary { get; set; }

    public Teacher(string firstName, string lastName, int age, string subject, decimal salary)
        : base(firstName, lastName, age)
    {
        Subject = subject;
        Salary = salary;
    }

    public void Teach()
    {
        Console.WriteLine($"{FirstName} {LastName} در حال تدریس درس {Subject} است.");
    }

    public override void DisplayInfo()
    {
        base.DisplayInfo();
        Console.WriteLine($"موضوع تدریس: {Subject}, حقوق: {Salary:C}"); // :C برای فرمت ارز
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        Person person1 = new Person("سارا", "رضایی", 30);
        person1.Greet();
        person1.DisplayInfo();
        Console.WriteLine("--------------------");

        Student student1 = new Student("مجید", "کریمی", 22, "S12345", "مهندسی کامپیوتر");
        student1.Greet(); // متد به ارث برده شده از Person
        student1.Study(); // متد مختص Student
        student1.DisplayInfo(); // متد override شده
        Console.WriteLine("--------------------");

        Teacher teacher1 = new Teacher("فاطمه", "رحیمی", 45, "ریاضیات", 50000000m);
        teacher1.Greet(); // متد به ارث برده شده از Person
        teacher1.Teach(); // متد مختص Teacher
        teacher1.DisplayInfo(); // متد override شده
        Console.WriteLine("--------------------");

        // استفاده از پلی‌مورفیسم با وراثت
        Person[] people = new Person[] { person1, student1, teacher1 };
        foreach (Person p in people)
        {
            Console.WriteLine($"نمایش اطلاعات عمومی برای {p.FirstName}:");
            p.DisplayInfo(); // در زمان اجرا مشخص می‌شود کدام DisplayInfo فراخوانی شود
            Console.WriteLine("---");
        }
    }
}

در این مثال:

  • کلاس Person یک کلاس والد است.
  • کلاس‌های Student و Teacher کلاس‌های فرزندی هستند که از Person به ارث می‌برند.
  • متدهای Greet() از Person توسط Student و Teacher به ارث برده شده‌اند.
  • متد DisplayInfo() در Person به عنوان virtual تعریف شده و در Student و Teacher با استفاده از override بازنویسی شده است تا اطلاعات خاص هر نوع را نیز نمایش دهد.
  • سازنده‌های کلاس‌های فرزند، با استفاده از : base(...) سازنده کلاس والد را فراخوانی می‌کنند.

مفهوم virtual و override در اینجا بسیار مهم است و ما را به سمت مفهوم پلی‌مورفیسم هدایت می‌کند.

اصل سوم: Polymorphism (چندریختی)

پلی‌مورفیسم (Polymorphism) به معنای "چندریختی" یا "اشکال گوناگون" است. این اصل به اشیاء با رفتارهای متفاوت اجازه می‌دهد تا از طریق یک رابط مشترک، به یک روش یکسان مدیریت شوند. در OOP، پلی‌مورفیسم به توانایی یک شیء برای پذیرش اشکال مختلف اشاره دارد، به این معنی که یک متد ممکن است در کلاس‌های مختلف، رفتار متفاوتی داشته باشد.

چرا Polymorphism مهم است؟

  • انعطاف‌پذیری و توسعه‌پذیری (Flexibility and Extensibility): با استفاده از پلی‌مورفیسم، می‌توانید کدی بنویسید که با انواع مختلفی از اشیاء کار کند، بدون اینکه نیاز به دانستن نوع دقیق آن‌ها در زمان کامپایل داشته باشید. این باعث می‌شود اضافه کردن انواع جدید در آینده آسان‌تر شود.
  • کاهش وابستگی (Reduced Coupling): کد شما به جزئیات پیاده‌سازی کلاس‌های خاص وابسته نخواهد بود، بلکه به رابط‌های عمومی یا کلاس‌های والد انتزاعی متکی خواهد بود.
  • سادگی و خوانایی کد: کد شما می‌تواند ساختاری ساده‌تر و قابل درک‌تر داشته باشد، زیرا از یک الگوی واحد برای تعامل با اشیاء مختلف استفاده می‌کند.

انواع Polymorphism در C#

پلی‌مورفیسم در C# به دو دسته اصلی تقسیم می‌شود:

  1. پلی‌مورفیسم زمان کامپایل (Compile-time Polymorphism / Static Polymorphism):

    این نوع پلی‌مورفیسم از طریق Method Overloading (بازگذاری متد) و Operator Overloading (بازگذاری عملگر) به دست می‌آید. کامپایلر نوع متدی را که باید فراخوانی شود، در زمان کامپایل تعیین می‌کند.

    Method Overloading: به شما اجازه می‌دهد چندین متد با یک نام مشترک در یک کلاس داشته باشید، به شرطی که امضای آن‌ها (تعداد یا نوع پارامترها) متفاوت باشد.

    
    public class Calculator
    {
        public int Add(int a, int b)
        {
            return a + b;
        }
    
        public double Add(double a, double b) // Overload با نوع پارامتر متفاوت
        {
            return a + b;
        }
    
        public int Add(int a, int b, int c) // Overload با تعداد پارامتر متفاوت
        {
            return a + b + c;
        }
    }
    
    // استفاده:
    // Calculator calc = new Calculator();
    // int sum1 = calc.Add(5, 10);     // فراخوانی Add(int, int)
    // double sum2 = calc.Add(5.5, 10.5); // فراخوانی Add(double, double)
    // int sum3 = calc.Add(1, 2, 3);   // فراخوانی Add(int, int, int)
            
  2. پلی‌مورفیسم زمان اجرا (Runtime Polymorphism / Dynamic Polymorphism):

    این نوع پلی‌مورفیسم از طریق Method Overriding (بازنویسی متد) با استفاده از کلمات کلیدی virtual، override و abstract، و همچنین از طریق Interface (رابط) به دست می‌آید. تصمیم‌گیری در مورد اینکه کدام متد در زمان اجرا فراخوانی شود، انجام می‌شود.

    Method Overriding: به یک کلاس فرزند اجازه می‌دهد تا پیاده‌سازی متدی را که در کلاس والد خود با کلمه کلیدی virtual (یا abstract) تعریف شده است، تغییر دهد. در کلاس فرزند، از کلمه کلیدی override برای این کار استفاده می‌شود.

    مثال قبلی مربوط به Person، Student و Teacher که از DisplayInfo() استفاده می‌کردند، نمونه‌ای از پلی‌مورفیسم زمان اجرا از طریق Overriding بود. وقتی آرایه‌ای از نوع Person داشتیم و متد DisplayInfo() را روی هر عنصر فراخوانی می‌کردیم، در زمان اجرا (نه کامپایل) مشخص می‌شد که متد DisplayInfo() مربوط به کدام نوع خاص (Person، Student یا Teacher) باید اجرا شود.

    
    // از همان مثال قبلی Person، Student، Teacher
    // Person[] people = new Person[] { person1, student1, teacher1 };
    // foreach (Person p in people)
    // {
    //     p.DisplayInfo(); // در زمان اجرا، متد درست فراخوانی می‌شود
    // }
            

    اگر DisplayInfo در Person virtual نبود و در فرزندان override نمی‌شد، آنگاه همیشه متد DisplayInfo کلاس Person فراخوانی می‌شد، حتی اگر شیء از نوع Student یا Teacher بود (اصطلاحاً "hiding" به جای "overriding" اتفاق می‌افتاد که با کلمه کلیدی new برای متدها مشخص می‌شود).

    استفاده از رابط‌ها (Interfaces): رابط‌ها یکی دیگر از ابزارهای قدرتمند برای پیاده‌سازی پلی‌مورفیسم هستند. یک رابط، قراردادی را تعریف می‌کند که کلاس‌ها باید آن را پیاده‌سازی کنند. این به شما اجازه می‌دهد تا اشیاء مختلفی را که یک رابط مشترک را پیاده‌سازی می‌کنند، به یک روش یکسان مدیریت کنید، حتی اگر هیچ رابطه وراثتی مستقیمی بین آن‌ها نباشد.

    مثال پلی‌مورفیسم با استفاده از رابط‌ها:

    
    public interface IShape
    {
        double GetArea();
        double GetPerimeter();
    }
    
    public class Circle : IShape
    {
        public double Radius { get; set; }
    
        public Circle(double radius)
        {
            Radius = radius;
        }
    
        public double GetArea()
        {
            return Math.PI * Radius * Radius;
        }
    
        public double GetPerimeter()
        {
            return 2 * Math.PI * Radius;
        }
    }
    
    public class Rectangle : IShape
    {
        public double Width { get; set; }
        public double Height { get; set; }
    
        public Rectangle(double width, double height)
        {
            Width = width;
            Height = height;
        }
    
        public double GetArea()
        {
            return Width * Height;
        }
    
        public double GetPerimeter()
        {
            return 2 * (Width + Height);
        }
    }
    
    public class Program
    {
        public static void Main(string[] args)
        {
            IShape circle = new Circle(5);
            IShape rectangle = new Rectangle(4, 6);
    
            // استفاده پلی‌مورفیک: هر دو شیء از نوع IShape هستند و می‌توانند GetArea و GetPerimeter را فراخوانی کنند.
            // در زمان اجرا، متد مربوط به نوع واقعی شیء (Circle یا Rectangle) فراخوانی می‌شود.
            Console.WriteLine($"مساحت دایره: {circle.GetArea():F2}, محیط دایره: {circle.GetPerimeter():F2}");
            Console.WriteLine($"مساحت مستطیل: {rectangle.GetArea():F2}, محیط مستطیل: {rectangle.GetPerimeter():F2}");
    
            // می‌توانیم یک لیست از IShape داشته باشیم و روی همه آن‌ها یک عملیات مشترک انجام دهیم.
            List shapes = new List { new Circle(3), new Rectangle(2, 5), new Circle(7) };
            foreach (IShape shape in shapes)
            {
                Console.WriteLine($"نوع شیء: {shape.GetType().Name}, مساحت: {shape.GetArea():F2}");
            }
        }
    }
            

    در این مثال، IShape یک رابط است. Circle و Rectangle هر دو این رابط را پیاده‌سازی می‌کنند. می‌توانیم اشیائی از نوع IShape داشته باشیم (با اشاره‌گر به یک شیء Circle یا Rectangle) و متدهای GetArea() و GetPerimeter() را روی آن‌ها فراخوانی کنیم. در زمان اجرا، متد مناسب برای شیء واقعی (دایره یا مستطیل) فراخوانی می‌شود. این نمونه بارز پلی‌مورفیسم است که به ما اجازه می‌دهد با انواع مختلف به صورت یکنواخت کار کنیم.

اصل چهارم: Abstraction (انتزاع)

انتزاع (Abstraction) یکی دیگر از اصول اساسی OOP است که به معنای نمایش فقط اطلاعات ضروری و پنهان کردن جزئیات پیاده‌سازی از کاربر است. این اصل بر "چیستی" تمرکز دارد تا "چگونگی". به عبارت دیگر، انتزاع به شما امکان می‌دهد تا یک نمای ساده‌سازی شده از یک شیء یا سیستم را ارائه دهید، بدون اینکه پیچیدگی‌های داخلی آن را آشکار کنید.

چرا Abstraction مهم است؟

  • کاهش پیچیدگی: با پنهان کردن جزئیات غیرضروری، سیستم ساده‌تر به نظر می‌رسد و راحت‌تر قابل درک و استفاده است.
  • افزایش قابلیت نگهداری: تغییرات در جزئیات پیاده‌سازی داخلی تأثیری بر کدهای خارجی که از رابط انتزاعی استفاده می‌کنند، نخواهد داشت.
  • ایجاد رابط‌های کاربری تمیز: توسعه‌دهندگان فقط نیاز دارند با رابط‌های تعریف شده تعامل داشته باشند، نه با جزئیات پیاده‌سازی پشت صحنه.
  • طراحی ماژولار: به شما کمک می‌کند تا سیستم را به بخش‌های مستقل و قابل مدیریت تقسیم کنید.

پیاده‌سازی Abstraction در C#

در C#، انتزاع عمدتاً از طریق دو مکانیزم به دست می‌آید:

  1. کلاس‌های انتزاعی (Abstract Classes)
  2. رابط‌ها (Interfaces)

کلاس‌های انتزاعی (Abstract Classes)

یک کلاس انتزاعی کلاسی است که با کلمه کلیدی abstract تعریف می‌شود. این کلاس‌ها نمی‌توانند مستقیماً نمونه‌سازی شوند (یعنی نمی‌توانید از آن‌ها شیء بسازید). آن‌ها معمولاً حاوی پیاده‌سازی‌های کامل برای برخی متدها و تعریف (ولی بدون پیاده‌سازی) برای برخی متدهای دیگر (به نام متدهای انتزاعی) هستند. متدهای انتزاعی باید در کلاس‌های فرزندی که از کلاس انتزاعی ارث می‌برند، پیاده‌سازی (override) شوند.

  • می‌توانند هم متدهای عادی و هم متدهای انتزاعی داشته باشند.
  • می‌توانند فیلدها، پراپرتی‌ها، سازنده‌ها و رویدادها را شامل شوند.
  • یک کلاس فقط می‌تواند از یک کلاس انتزاعی به ارث ببرد (وراثت یگانه).
  • هدف اصلی آن‌ها فراهم کردن یک پایه مشترک و رفتارهای پیش‌فرض برای مجموعه‌ای از کلاس‌های مرتبط است.

رابط‌ها (Interfaces)

یک رابط با کلمه کلیدی interface تعریف می‌شود. یک رابط، یک "قرارداد" را تعریف می‌کند که شامل مجموعه‌ای از تعاریف متدها، پراپرتی‌ها، رویدادها یا ایندکسرها است، اما بدون هیچ‌گونه پیاده‌سازی. کلاس‌هایی که یک رابط را پیاده‌سازی می‌کنند (با استفاده از علامت :)، متعهد می‌شوند که تمام اعضای آن رابط را پیاده‌سازی کنند.

  • نمی‌توانند فیلدها (فقط پراپرتی‌ها) یا سازنده‌ها را شامل شوند (قبل از C# 8).
  • تمام اعضای یک رابط به طور ضمنی public و abstract هستند (نیازی به استفاده صریح از این کلمات کلیدی نیست).
  • یک کلاس می‌تواند چندین رابط را پیاده‌سازی کند (پلی‌مورفیسم چندگانه رفتاری).
  • از C# 8 به بعد، رابط‌ها می‌توانند پیاده‌سازی پیش‌فرض برای متدها (default interface methods) داشته باشند.
  • هدف اصلی آن‌ها تعریف یک قرارداد برای رفتار است که می‌تواند توسط انواع مختلفی از کلاس‌ها پیاده‌سازی شود، حتی اگر هیچ رابطه وراثتی بین آن‌ها وجود نداشته باشد.

تفاوت‌های کلیدی بین Abstract Classes و Interfaces

این دو مفهوم ابزارهای قدرتمندی برای انتزاع هستند، اما تفاوت‌های مهمی دارند:

ویژگی کلاس انتزاعی (Abstract Class) رابط (Interface)
امکان نمونه‌سازی نمی‌تواند مستقیماً نمونه‌سازی شود. نمی‌تواند مستقیماً نمونه‌سازی شود.
پیاده‌سازی متدها می‌تواند متدهای انتزاعی (بدون پیاده‌سازی) و متدهای عادی (با پیاده‌سازی) داشته باشد. تا C# 8 فقط تعاریف (بدون پیاده‌سازی). از C# 8 به بعد می‌تواند default interface methods داشته باشد.
اعضا فیلدها، پراپرتی‌ها، متدها، سازنده‌ها، رویدادها، ایندکسرها. پراپرتی‌ها، متدها، رویدادها، ایندکسرها (بدون فیلد، بدون سازنده قبل از C# 8).
سطح دسترسی می‌تواند هر تعیین‌کننده دسترسی (public, private, protected) را برای اعضای خود داشته باشد. تمام اعضا به طور ضمنی public هستند.
وراثت یک کلاس فقط می‌تواند از یک کلاس انتزاعی به ارث ببرد (وراثت یگانه). یک کلاس می‌تواند چندین رابط را پیاده‌سازی کند (وراثت چندگانه رفتاری).
هدف تعریف یک "IS-A" (هست یک) رابطه سلسله مراتبی. ارائه یک پایه مشترک با برخی رفتارهای پیش‌فرض و برخی رفتارهای اجباری. تعریف یک "CAN-DO" (می‌تواند انجام دهد) قابلیت. تعریف یک قرارداد که کلاس‌ها باید آن را پیاده‌سازی کنند، بدون دیکته کردن پیاده‌سازی داخلی.

مثال عملی از Abstraction (با Abstract Class و Interface)


using System;
using System.Collections.Generic; // برای استفاده از List

// ** استفاده از Abstract Class برای انتزاع **
// کلاس پایه انتزاعی برای اشکال
public abstract class Shape
{
    public string Name { get; set; }

    public Shape(string name)
    {
        Name = name;
    }

    // متد انتزاعی: هر شکل باید مساحت خود را محاسبه کند، اما نحوه محاسبه متفاوت است.
    public abstract double GetArea();

    // متد عادی: همه اشکال می‌توانند اطلاعات خود را نمایش دهند.
    public void DisplayShapeName()
    {
        Console.WriteLine($"این یک شکل است: {Name}");
    }
}

// کلاس فرزند: Circle که از Shape به ارث می‌برد
public class Circle : Shape
{
    public double Radius { get; set; }

    public Circle(string name, double radius) : base(name)
    {
        Radius = radius;
    }

    // پیاده‌سازی متد انتزاعی GetArea
    public override double GetArea()
    {
        return Math.PI * Radius * Radius;
    }
}

// کلاس فرزند: Rectangle که از Shape به ارث می‌برد
public class Rectangle : Shape
{
    public double Width { get; set; }
    public double Height { get; set; }

    public Rectangle(string name, double width, double height) : base(name)
    {
        Width = width;
        Height = height;
    }

    // پیاده‌سازی متد انتزاعی GetArea
    public override double GetArea()
    {
        return Width * Height;
    }
}

// ** استفاده از Interface برای انتزاع **
// رابط برای قابلیت چاپ
public interface IPrintable
{
    void PrintDetails();
}

// کلاس Triangle که Shape نیست اما قابلیت چاپ دارد
public class Triangle : IPrintable
{
    public double Base { get; set; }
    public double Height { get; set; }

    public Triangle(double @base, double height)
    {
        Base = @base;
        Height = height;
    }

    // پیاده‌سازی متد رابط PrintDetails
    public void PrintDetails()
    {
        Console.WriteLine($"جزئیات مثلث: قاعده {Base}, ارتفاع {Height}");
    }
}

public class Program
{
    public static void Main(string[] args)
    {
        // استفاده از کلاس‌های انتزاعی و فرزندان آن‌ها
        List shapes = new List
        {
            new Circle("دایره کوچک", 3.0),
            new Rectangle("مستطیل بزرگ", 5.0, 8.0)
        };

        foreach (Shape shape in shapes)
        {
            shape.DisplayShapeName(); // متد از کلاس انتزاعی Shape
            Console.WriteLine($"مساحت {shape.Name}: {shape.GetArea():F2}"); // متد override شده
        }

        Console.WriteLine("\n--------------------\n");

        // استفاده از رابط‌ها
        List printables = new List
        {
            new Triangle(4, 7),
            new Circle("دایره قابل چاپ", 6.0) // Circle هم می‌تواند IPrintable را پیاده‌سازی کند اگر لازم باشد
        };

        // فرض کنید Circle هم IPrintable را پیاده‌سازی کرده باشد:
        // public class Circle : Shape, IPrintable
        // { ... public void PrintDetails() { Console.WriteLine($"جزئیات دایره: شعاع {Radius}"); } }

        // اگرچه Triangle و Circle رابطه وراثتی با یکدیگر ندارند، اما هر دو IPrintable هستند.
        // در نتیجه، می‌توانیم از طریق رابط با آن‌ها به صورت پلی‌مورفیک تعامل کنیم.
        foreach (IPrintable printable in printables)
        {
            printable.PrintDetails();
        }
    }
}

در این مثال:

  • Shape یک کلاس انتزاعی است که متد انتزاعی GetArea() را تعریف می‌کند. این بدان معناست که هر کلاسی که از Shape ارث می‌برد (مثل Circle و Rectangle) باید متد GetArea() را پیاده‌سازی کند. همچنین Shape یک متد غیرانتزاعی DisplayShapeName() را ارائه می‌دهد که تمام فرزندان می‌توانند از آن استفاده کنند.
  • IPrintable یک رابط است که یک قرارداد (متد PrintDetails()) را تعریف می‌کند. هر کلاسی که IPrintable را پیاده‌سازی کند (مثل Triangle)، متعهد می‌شود که متد PrintDetails() را ارائه دهد. این امکان را می‌دهد که قابلیت چاپ را به کلاس‌هایی اضافه کنیم که ممکن است در یک سلسله مراتب وراثتی مشترک نباشند.

این دو مکانیزم، ابزارهای اصلی برای دستیابی به انتزاع در C# هستند و به توسعه‌دهندگان کمک می‌کنند تا سیستم‌های پیچیده‌ای را با رابط‌های تمیز و ساختاری منعطف طراحی کنند.

اصول SOLID: ستون‌های طراحی شی‌گرا

اصول SOLID مجموعه‌ای از پنج اصل راهنما در برنامه‌نویسی شی‌گرا و طراحی نرم‌افزار هستند که توسط رابرت سی. مارتین (معروف به Uncle Bob) معرفی شده‌اند. پیروی از این اصول به توسعه‌دهندگان کمک می‌کند تا کدی بنویسند که:

  • قابل نگهداری (Maintainable) باشد: آسان‌تر قابل درک، رفع اشکال و تغییر باشد.
  • انعطاف‌پذیر (Flexible) باشد: به راحتی به تغییرات نیازها پاسخ دهد.
  • قابل توسعه (Extensible) باشد: افزودن قابلیت‌های جدید به سیستم بدون نیاز به تغییر کدهای موجود آسان باشد.
  • قابل استفاده مجدد (Reusable) باشد: اجزای کد بتوانند در زمینه‌های مختلف دوباره استفاده شوند.

این اصول بر روی ساختاردهی کد و کلاس‌ها برای رسیدن به این اهداف تمرکز دارند و مکمل مفاهیم اصلی OOP (کپسوله‌سازی، وراثت، پلی‌مورفیسم، انتزاع) هستند.

بیایید به اختصار هر یک از این اصول را بررسی کنیم:

1. Single Responsibility Principle (SRP) - اصل مسئولیت واحد

"یک کلاس باید فقط یک دلیل برای تغییر داشته باشد."

این اصل بیان می‌کند که هر کلاس یا ماژول باید تنها یک مسئولیت و یک هدف مشخص داشته باشد. اگر یک کلاس بیش از یک مسئولیت را بر عهده بگیرد، تغییر در یکی از مسئولیت‌ها ممکن است بر دیگری تأثیر بگذارد و کلاس را شکننده کند. به عنوان مثال، یک کلاس نباید هم مسئولیت محاسبه حقوق را داشته باشد و هم مسئولیت چاپ گزارش حقوق را.

مزایا: افزایش خوانایی، کاهش Coupling، افزایش Cohesion (چسبندگی داخلی)، آسان‌تر شدن تست‌نویسی.

2. Open/Closed Principle (OCP) - اصل باز/بسته

"یک موجودیت نرم‌افزاری (کلاس، ماژول، تابع و غیره) باید برای توسعه (Extension) باز و برای تغییر (Modification) بسته باشد."

این اصل به این معناست که باید بتوانیم با اضافه کردن کدهای جدید، رفتار یک ماژول را گسترش دهیم، نه با تغییر کدهای موجود آن. این امر معمولاً با استفاده از انتزاع (کلاس‌های انتزاعی و رابط‌ها) و پلی‌مورفیسم حاصل می‌شود. به جای تغییر مستقیم یک کلاس، کلاس‌های جدیدی ایجاد می‌کنیم که رابط یا کلاس انتزاعی آن را پیاده‌سازی یا از آن ارث می‌برند.

مزایا: کاهش ریسک معرفی باگ‌های جدید در کدهای موجود، افزایش پایداری و پویایی سیستم.

3. Liskov Substitution Principle (LSP) - اصل جایگزینی لیسکوف

"اشیاء یک کلاس پایه (Superclass) باید بتوانند با اشیاء کلاس‌های مشتق شده (Subclass) جایگزین شوند، بدون اینکه درستی برنامه از بین برود."

این اصل بیان می‌کند که اگر یک کلاس B از کلاس A مشتق شده باشد، آنگاه هر جایی که انتظار شیء از نوع A می‌رود، باید بتوان بدون هیچ مشکلی از شیء از نوع B استفاده کرد. به عبارت دیگر، کلاس‌های فرزند باید رفتار کلاس والد را حفظ کنند و هیچ متدی را به گونه‌ای override نکنند که انتظارات استفاده‌کننده از کلاس والد را نقض کند.

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

4. Interface Segregation Principle (ISP) - اصل تفکیک رابط

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

این اصل توصیه می‌کند که به جای داشتن یک رابط بزرگ و عمومی، بهتر است رابط‌های کوچک‌تر و خاص‌تر داشته باشیم. به این ترتیب، کلاسی که یک رابط را پیاده‌سازی می‌کند، فقط متدهایی را پیاده‌سازی می‌کند که واقعاً به آن‌ها نیاز دارد و از "متدهای بی‌ربط" رها می‌شود. این اصل به کاهش coupling و افزایش cohesion کمک می‌کند.

مزایا: طراحی سیستم‌های انعطاف‌پذیرتر، کاهش متدهای بلااستفاده در کلاس‌ها، افزایش نگهداری.

5. Dependency Inversion Principle (DIP) - اصل وارونگی وابستگی

"ماژول‌های سطح بالا نباید به ماژول‌های سطح پایین وابسته باشند. هر دو باید به انتزاعات (Abstractions) وابسته باشند. انتزاعات نباید به جزئیات وابسته باشند، بلکه جزئیات باید به انتزاعات وابسته باشند."

این اصل بر این تأکید دارد که به جای اینکه کدهای سطح بالا (منطق تجاری اصلی) مستقیماً به جزئیات پیاده‌سازی کدهای سطح پایین وابسته باشند، هر دو باید به انتزاعات (مثل رابط‌ها یا کلاس‌های انتزاعی) وابسته باشند. این کار باعث می‌شود سیستم از جزئیات پیاده‌سازی decouple (جداسازی) شود و تغییرات در جزئیات، تأثیری بر ماژول‌های سطح بالا نداشته باشد. این اصل معمولاً با الگوهایی مانند Dependency Injection (DI) پیاده‌سازی می‌شود.

مزایا: افزایش تست‌پذیری، افزایش انعطاف‌پذیری، کاهش Coupling.

اهمیت اصول SOLID

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

سایر مفاهیم پیشرفته و الگوهای طراحی شی‌گرا

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

Composition over Inheritance (ترکیب بر وراثت)

این یک اصل طراحی است که پیشنهاد می‌کند به جای استفاده گسترده از وراثت برای اشتراک‌گذاری رفتار، از ترکیب (Composition) استفاده شود. ترکیب به معنای این است که یک کلاس، اشیائی از کلاس‌های دیگر را به عنوان اعضای خود شامل شود (Has-A relationship)، و سپس از طریق این اشیاء، رفتار مورد نظر را به دست آورد. در مقابل، وراثت (Is-A relationship) به معنای به ارث بردن تمام رفتارها و ویژگی‌ها از یک کلاس والد است.

چرا ترکیب ارجحیت دارد؟

  • کاهش Coupling: ترکیب منجر به Coupling کمتر می‌شود، زیرا کلاس‌های ترکیب شده می‌توانند مستقل از یکدیگر تغییر کنند، در حالی که در وراثت، تغییر در کلاس والد می‌تواند بر تمام کلاس‌های فرزند تأثیر بگذارد.
  • انعطاف‌پذیری بیشتر: با ترکیب، می‌توان در زمان اجرا (Runtime) رفتارهای جدید را به یک شیء اضافه یا از آن حذف کرد، در حالی که وراثت ساختار ثابتی را در زمان کامپایل (Compile-time) ایجاد می‌کند.
  • جلوگیری از مشکل "سلسله مراتب شکننده" (Fragile Base Class Problem): تغییرات در کلاس پایه در وراثت ممکن است بدون اینکه کدهای کلاس‌های فرزند تغییر کنند، رفتار آن‌ها را بشکند. ترکیب این مشکل را ندارد.
  • پشتیبانی بهتر از ISP: با استفاده از ترکیب و رابط‌ها، می‌توانیم کلاس‌هایی بسازیم که فقط رفتارهای مورد نیاز خود را به کار گیرند، نه اینکه مجبور به پیاده‌سازی متدهای بی‌ربط از یک کلاس والد بزرگ شوند.

مثال: به جای اینکه کلاس Dog از کلاس Animal و Flyable ارث ببرد (که Dog پرواز نمی‌کند)، Dog از Animal ارث می‌برد و یک شیء از نوع IWalkable یا IBarkable را ترکیب می‌کند. اگر بخواهیم یک پرنده داشته باشیم، Bird از Animal ارث می‌برد و یک شیء از نوع IFlyable را ترکیب می‌کند.


// رویکرد Composition (بهتر):
public interface IFlyable
{
    void Fly();
}

public class Wings : IFlyable
{
    public void Fly()
    {
        Console.WriteLine("در حال پرواز با بال!");
    }
}

public class Bird : Animal // Animal یک کلاس پایه ساده
{
    private IFlyable _flyBehavior; // ترکیب (Composition)

    public Bird(string name, IFlyable flyBehavior) : base(name)
    {
        _flyBehavior = flyBehavior;
    }

    public void PerformFly()
    {
        _flyBehavior.Fly();
    }
}

// استفاده:
// Bird eagle = new Bird("عقاب", new Wings());
// eagle.PerformFly(); // در حال پرواز با بال!

Delegates و Events: فراتر از متدهای کلاسیک

در C#، Delegates و Events مکانیزم‌هایی هستند که به شما اجازه می‌دهند تا به صورت loose coupled (با وابستگی کم) بین کامپوننت‌ها ارتباط برقرار کنید، که این خود یک جنبه مهم از طراحی شی‌گرا است.

  • Delegate (نماینده): یک Delegate یک نوع شیء است که به یک یا چند متد ارجاع می‌دهد. می‌توانید آن را به عنوان یک "پوینتر به متد" امن از نظر نوع (type-safe) در نظر بگیرید. Delegates پایه‌ای برای Event Handling و بسیاری از الگوهای طراحی دیگر هستند. آن‌ها امکان فراخوانی متدها به صورت غیرمستقیم را فراهم می‌کنند و اغلب برای پیاده‌سازی Callbackها استفاده می‌شوند.
  • Event (رویداد): رویدادها بر پایه Delegates ساخته شده‌اند و مکانیزمی را برای یک کلاس فراهم می‌کنند تا به کلاس‌های دیگر اطلاع دهد که اتفاق خاصی رخ داده است. این یک راه عالی برای پیاده‌سازی الگوی Observer است، جایی که یک "ناشر" (Publisher) رویدادها را اعلام می‌کند و "مشترکین" (Subscribers) به آن‌ها واکنش نشان می‌دهند، بدون اینکه ناشر از مشترکین خود اطلاعی داشته باشد. این به شدت Coupling بین کامپوننت‌ها را کاهش می‌دهد.

public delegate void StockPriceChangeHandler(decimal oldPrice, decimal newPrice);

public class Stock
{
    public event StockPriceChangeHandler OnPriceChanged; // تعریف رویداد

    private decimal _price;
    public decimal Price
    {
        get { return _price; }
        set
        {
            if (_price != value)
            {
                decimal oldPrice = _price;
                _price = value;
                OnPriceChanged?.Invoke(oldPrice, _price); // فراخوانی رویداد
            }
        }
    }
}

// کلاس مشترک (Subscriber)
public class StockMonitor
{
    public StockMonitor(Stock stock)
    {
        stock.OnPriceChanged += HandlePriceChange; // اشتراک در رویداد
    }

    private void HandlePriceChange(decimal oldPrice, decimal newPrice)
    {
        Console.WriteLine($"قیمت سهام از {oldPrice:C} به {newPrice:C} تغییر یافت.");
    }
}

// استفاده:
// Stock googleStock = new Stock { Price = 100m };
// StockMonitor monitor = new StockMonitor(googleStock);
// googleStock.Price = 105m; // خروجی: قیمت سهام از $100.00 به $105.00 تغییر یافت.

الگوهای طراحی شی‌گرا (Design Patterns)

الگوهای طراحی، راه‌حل‌های عمومی و قابل استفاده مجدد برای مشکلات رایج در طراحی نرم‌افزار هستند. این الگوها، بهترین شیوه‌های (best practices) توسعه‌دهندگان باتجربه را در طول سال‌ها کدنویسی جمع‌آوری کرده‌اند و به شما کمک می‌کنند تا کدهایی با ساختار بهتر، قابل نگهداری‌تر و مقیاس‌پذیرتر بنویسید.

اصول OOP و SOLID، پایه و اساس درک و پیاده‌سازی الگوهای طراحی هستند. برخی از معروف‌ترین الگوها عبارتند از:

  • الگوهای Creational (ایجاد کننده): مربوط به فرآیند ساخت اشیاء. مثال:
    • Singleton: تضمین می‌کند که یک کلاس فقط یک نمونه (instance) دارد و یک نقطه دسترسی عمومی به آن نمونه فراهم می‌کند.
    • Factory Method: یک رابط برای ایجاد اشیاء در یک کلاس پایه تعریف می‌کند، اما به کلاس‌های فرزند اجازه می‌دهد تا نوع شیء را که باید ایجاد شود، تغییر دهند.
  • الگوهای Structural (ساختاری): مربوط به نحوه ترکیب کلاس‌ها و اشیاء برای تشکیل ساختارهای بزرگتر. مثال:
    • Adapter: رابط یک کلاس را به رابط دیگری که کلاینت انتظار دارد، تبدیل می‌کند و امکان همکاری کلاس‌ها با رابط‌های ناسازگار را فراهم می‌آورد.
    • Decorator: قابلیت‌های جدید را به صورت پویا و بدون تغییر ساختار کلاس به اشیاء اضافه می‌کند.
  • الگوهای Behavioral (رفتاری): مربوط به تعامل و توزیع مسئولیت‌ها بین اشیاء. مثال:
    • Observer: یک وابستگی یک به چند بین اشیاء ایجاد می‌کند، به طوری که وقتی یک شیء حالت خود را تغییر می‌دهد، تمام وابستگان آن مطلع و به روز می‌شوند.
    • Strategy: یک خانواده از الگوریتم‌ها را تعریف می‌کند، هر یک را کپسوله‌سازی می‌کند و آن‌ها را قابل تعویض می‌کند. این الگو به الگوریتم‌ها اجازه می‌دهد که به طور مستقل از کلاینت‌هایی که از آن‌ها استفاده می‌کنند، تغییر کنند.
    • Command: یک درخواست را به عنوان یک شیء کپسوله‌سازی می‌کند، بنابراین به شما امکان می‌دهد کلاینت‌های مختلف را با درخواست‌های مختلف پارامتری کنید، صف‌های درخواست را تشکیل دهید یا عملیات را پشتیبانی کنید.

مطالعه و به کارگیری الگوهای طراحی به شما کمک می‌کند تا به عنوان یک معمار نرم‌افزار، راه‌حل‌های پایدارتر و انعطاف‌پذیرتری را برای مشکلات رایج طراحی ارائه دهید. آن‌ها زبان مشترکی را برای بحث در مورد راه‌حل‌های طراحی فراهم می‌کنند و بهره‌وری شما را در پروژه‌های پیچیده افزایش می‌دهند.

نتیجه‌گیری و گام‌های بعدی

برنامه‌نویسی شی‌گرا (OOP) بیش از یک مجموعه از ویژگی‌های زبانی، یک روش تفکر و یک فلسفه طراحی نرم‌افزار است. همانطور که در این مقاله جامع آموختیم، مفاهیم اصلی OOP – کپسوله‌سازی، وراثت، پلی‌مورفیسم و انتزاع – ابزارهای قدرتمندی را برای سازماندهی، مدیریت و مقیاس‌بندی کدهای پیچیده در C# فراهم می‌کنند. این اصول نه تنها به ما کمک می‌کنند کدهای خواناتر و قابل نگهداری‌تری بنویسیم، بلکه زیربنای ساخت سیستم‌هایی هستند که می‌توانند در برابر تغییرات آینده انعطاف‌پذیر باشند.

با پرداختن به اصول SOLID، دیدگاه خود را از نحوه طراحی صحیح کلاس‌ها و روابط بین آن‌ها گسترش دادیم. این اصول به عنوان ستون‌های محکمی عمل می‌کنند که کد شی‌گرای شما را پایدار، قابل نگهداری و آسان برای توسعه می‌سازند. در نهایت، با معرفی مفهوم "ترکیب بر وراثت" و نگاهی اجمالی به Delegates، Events و الگوهای طراحی، افق دید شما را به سمت راه‌حل‌های پیشرفته‌تر و ساختارهای طراحی آزموده شده گشودیم.

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

  1. تمرین عملی: هیچ چیز جایگزین کدنویسی نیست. مثال‌های ارائه شده در این آموزش را بازنویسی کنید، آن‌ها را تغییر دهید و سعی کنید ویژگی‌های جدیدی به آن‌ها اضافه کنید. پروژه‌های کوچک خود را با تمرکز بر پیاده‌سازی اصول OOP طراحی و توسعه دهید.
  2. مطالعه عمیق‌تر SOLID: هر یک از اصول SOLID به تنهایی می‌تواند موضوع یک مقاله کامل باشد. با مطالعه مثال‌های بیشتر و سناریوهای واقعی برای هر اصل، درک خود را عمیق‌تر کنید.
  3. آشنایی با الگوهای طراحی: کتاب "Design Patterns: Elements of Reusable Object-Oriented Software" (معروف به Gang of Four یا GoF) نقطه شروع عالی برای الگوهای طراحی است. همچنین منابع آنلاین و کتاب‌های متعددی در مورد الگوهای طراحی در C# وجود دارد. سعی کنید الگوهای رایج را در پروژه‌های خود به کار ببرید.
  4. خواندن کدهای دیگران: پروژه‌های منبع‌باز (open-source) را بررسی کنید. ببینید توسعه‌دهندگان باتجربه چگونه از اصول OOP و الگوهای طراحی در کدهای واقعی استفاده می‌کنند.
  5. کنجکاوی و پرسشگری: همیشه بپرسید "چرا؟". چرا یک راه حل شی‌گرا بهتر از دیگری است؟ چه مزایا و معایبی دارد؟ این تفکر انتقادی به شما کمک می‌کند تا به یک طراح نرم‌افزار بهتر تبدیل شوید.

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

“تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT”

قیمت اصلی 2.290.000 ریال بود.قیمت فعلی 1.590.000 ریال است.

"تسلط به برنامه‌نویسی پایتون با هوش مصنوعی: آموزش کدنویسی هوشمند با ChatGPT"

"با شرکت در این دوره جامع و کاربردی، به راحتی مهارت‌های برنامه‌نویسی پایتون را از سطح مبتدی تا پیشرفته با کمک هوش مصنوعی ChatGPT بیاموزید. این دوره، با بیش از 6 ساعت محتوای آموزشی، شما را قادر می‌سازد تا به سرعت الگوریتم‌های پیچیده را درک کرده و اپلیکیشن‌های هوشمند ایجاد کنید. مناسب برای تمامی سطوح با زیرنویس فارسی حرفه‌ای و امکان دانلود و تماشای آنلاین."

ویژگی‌های کلیدی:

بدون نیاز به تجربه قبلی برنامه‌نویسی

زیرنویس فارسی با ترجمه حرفه‌ای

۳۰ ٪ تخفیف ویژه برای دانشجویان و دانش آموزان