ارث‌بری و چندریختی در C#: پیشرفته‌تر شوید

فهرست مطالب

ارث‌بری و چندریختی در C#: پیشرفته‌تر شوید

در دنیای توسعه نرم‌افزار، به‌ویژه در اکوسیستم قدرتمند .NET و زبان برنامه‌نویسی C#، مفاهیم شی‌گرایی (Object-Oriented Programming – OOP) ستون فقرات هر طراحی موفق و پایدار را تشکیل می‌دهند. در میان اصول بنیادین OOP، ارث‌بری (Inheritance) و چندریختی (Polymorphism) نه تنها ابزارهایی برای سازماندهی کد هستند، بلکه قدرتی بی‌نظیر برای ایجاد سیستم‌هایی با قابلیت توسعه‌پذیری بالا، انعطاف‌پذیری و نگهداری آسان فراهم می‌آورند. این مفاهیم، در نگاه اول شاید ساده به نظر برسند، اما تسلط واقعی بر آن‌ها، نیازمند درکی عمیق از جزئیات، کاربردهای پیشرفته و چالش‌های نهفته آن‌هاست.

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

بازنگری اجمالی: ارث‌بری و چندریختی در سطح بنیادین

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

ارث‌بری (Inheritance): رابطه “یک-است-از” (is-a)

ارث‌بری مکانیزمی است که به یک کلاس (کلاس فرزند یا مشتق شده – Derived Class) اجازه می‌دهد تا ویژگی‌ها (Properties) و رفتارها (Methods) را از یک کلاس دیگر (کلاس والد یا پایه – Base Class) به ارث ببرد. این قابلیت، اصلی‌ترین راه برای تحقق مفهوم “یک-است-از” در طراحی شی‌گراست. برای مثال، یک “ماشین” یک-است-از یک “وسیله نقلیه”، یا یک “گربه” یک-است-از یک “حیوان”.

هدف اصلی ارث‌بری، قابلیت استفاده مجدد از کد (Code Reusability) و ایجاد سلسله‌مراتب منطقی بین اشیاء است. در C#، یک کلاس تنها می‌تواند از یک کلاس پایه به ارث ببرد (ارث‌بری تکی – Single Inheritance)، اما می‌تواند چندین اینترفیس را پیاده‌سازی کند (که نوعی از ارث‌بری قرارداد محسوب می‌شود).


public class Vehicle
{
    public string Brand { get; set; }
    public void StartEngine()
    {
        Console.WriteLine("Engine started.");
    }
}

public class Car : Vehicle // Car inherits from Vehicle
{
    public int NumberOfDoors { get; set; }
    public void Drive()
    {
        Console.WriteLine("Car is driving.");
    }
}

// Usage
Car myCar = new Car();
myCar.Brand = "Toyota";
myCar.StartEngine(); // Method inherited from Vehicle
myCar.Drive();       // Method defined in Car

چندریختی (Polymorphism): “اشکال بسیار”

کلمه چندریختی از ریشه یونانی “poly” (به معنای زیاد) و “morph” (به معنای شکل) گرفته شده است، که به معنای “اشکال بسیار” یا “توانایی گرفتن اشکال مختلف” است. در برنامه‌نویسی شی‌گرا، چندریختی به این معنی است که اشیاء از کلاس‌های مختلف می‌توانند به یکدیگر به عنوان اشیاء از یک کلاس پایه یا یک اینترفیس مشترک نگاه شوند و به همان روش با آن‌ها تعامل شود، اما رفتار متفاوتی از خود نشان دهند.

C# از چندریختی عمدتاً از طریق دو مکانیزم پشتیبانی می‌کند:

  1. چندریختی زمان کامپایل (Compile-time Polymorphism): که به آن بارگذاری بیش از حد متد (Method Overloading) نیز گفته می‌شود. این قابلیت به شما اجازه می‌دهد تا چندین متد با یک نام یکسان در یک کلاس داشته باشید، به شرطی که امضای آن‌ها (تعداد، نوع یا ترتیب پارامترها) متفاوت باشد. کامپایلر بر اساس آرگومان‌های ارسالی، متد مناسب را در زمان کامپایل انتخاب می‌کند.

    
            public class Calculator
            {
                public int Add(int a, int b) { return a + b; }
                public double Add(double a, double b) { return a + b; }
                public int Add(int a, int b, int c) { return a + b + c; }
            }
            
  2. چندریختی زمان اجرا (Runtime Polymorphism): که اغلب منظور از چندریختی، همین نوع است. این قابلیت از طریق متدهای `virtual`، `override` و `abstract`، و همچنین اینترفیس‌ها پیاده‌سازی می‌شود. این امکان را فراهم می‌آورد که یک متد در کلاس پایه تعریف شود و سپس در کلاس‌های مشتق شده، پیاده‌سازی متفاوتی داشته باشد. در زمان اجرا، بر اساس نوع واقعی شیء، متد مناسب فراخوانی می‌شود.

    
            public class Animal
            {
                public virtual void MakeSound() // virtual method
                {
                    Console.WriteLine("Animal makes a sound.");
                }
            }
    
            public class Dog : Animal
            {
                public override void MakeSound() // override base method
                {
                    Console.WriteLine("Dog barks.");
                }
            }
    
            public class Cat : Animal
            {
                public override void MakeSound() // override base method
                {
                    Console.WriteLine("Cat meows.");
                }
            }
    
            // Usage for runtime polymorphism
            Animal myAnimal1 = new Dog();
            Animal myAnimal2 = new Cat();
            Animal myAnimal3 = new Animal();
    
            myAnimal1.MakeSound(); // Output: Dog barks.
            myAnimal2.MakeSound(); // Output: Cat meows.
            myAnimal3.MakeSound(); // Output: Animal makes a sound.
            

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

انواع ارث‌بری و کاربردهای پیشرفته آن

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

ارث‌بری پیاده‌سازی (Implementation Inheritance) در مقابل ارث‌بری اینترفیس (Interface Inheritance)

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

در مقابل، ارث‌بری اینترفیس به معنای پیاده‌سازی یک اینترفیس توسط یک کلاس است. در این حالت، کلاس تنها “قرارداد” (Contract) یا “رفتار” را به ارث می‌برد، نه پیاده‌سازی آن را. اینترفیس‌ها تنها امضای متدها، ویژگی‌ها، رویدادها و ایندکسرها را تعریف می‌کنند و هیچ پیاده‌سازی‌ای ندارند (تا C# 8.0، که Default Interface Methods معرفی شدند، اما حتی با آن هم، هدف اصلی ارائه قرارداد است).

چه زمانی از کدام استفاده کنیم؟

  • ارث‌بری پیاده‌سازی (کلاس‌های پایه): زمانی مناسب است که بین کلاس‌ها رابطه “یک-است-از” قوی وجود دارد و کلاس‌های مشتق شده نیاز به استفاده مجدد از بخش قابل توجهی از منطق یا داده‌های کلاس پایه دارند. این رویکرد، اشتراک‌گذاری کد را تسهیل می‌کند. با این حال، استفاده بیش از حد یا نادرست از آن می‌تواند منجر به مشکلاتی مانند “مشکل کلاس پایه شکننده” (Fragile Base Class Problem) شود، که در آن تغییرات در کلاس پایه به طور ناخواسته کلاس‌های مشتق شده را تحت تأثیر قرار می‌دهند.
  • ارث‌بری اینترفیس: زمانی که می‌خواهید یک قرارداد رفتاری را بدون تحمیل جزئیات پیاده‌سازی تعریف کنید، اینترفیس‌ها انتخاب عالی هستند. آن‌ها انعطاف‌پذیری بالایی را فراهم می‌کنند و پایه و اساس برای Dependency Injection و تست‌پذیری هستند. اینترفیس‌ها به شما اجازه می‌دهند تا چندریختی را در سطحی انتزاعی‌تر اعمال کنید و “یک نوع-عمل” (is-a-kind-of) را بیان می‌کنند، نه لزوماً “یک-است-از”. مثلاً یک `Car` می‌تواند رانده شود (IDrivable)، اما Car لزوماً یک IDrivable نیست، بلکه Car نوعی از یک وسیله نقلیه است.

کلاس‌های `sealed`

کلمه کلیدی `sealed` در C# برای جلوگیری از ارث‌بری از یک کلاس به کار می‌رود. وقتی یک کلاس را `sealed` اعلام می‌کنید، هیچ کلاس دیگری نمی‌تواند از آن به ارث ببرد. این قابلیت می‌تواند به دلایل مختلفی مفید باشد:

  • امنیت: جلوگیری از تغییر رفتار یک کلاس حیاتی توسط کدهای مخرب یا ناخواسته.
  • بهینه‌سازی: کامپایلر می‌تواند فراخوانی متدهای غیرمجازی یک کلاس `sealed` را بهینه کند، زیرا می‌داند که هرگز پیاده‌سازی جایگزینی نخواهد داشت.
  • پایداری: تضمین می‌کند که منطق یک کلاس خاص در طول زمان ثابت می‌ماند و از “مشکل کلاس پایه شکننده” جلوگیری می‌کند، زیرا هیچ کلاس مشتق شده‌ای وجود ندارد که از تغییرات تأثیر بپذیرد.

public sealed class FinalConfiguration
{
    public string ConnectionString { get; set; }
    // No class can inherit from FinalConfiguration
}

کلاس‌های `abstract` و اینترفیس‌ها: تمایز دقیق‌تر

هم کلاس‌های `abstract` و هم اینترفیس‌ها برای تعریف قراردادها و انتزاعیات در C# استفاده می‌شوند، اما تفاوت‌های کلیدی بین آن‌ها وجود دارد که درک آن‌ها برای طراحی پیشرفته ضروری است:

  • پیاده‌سازی:

    • کلاس `abstract`: می‌تواند هم اعضای `abstract` (که باید توسط کلاس‌های مشتق شده پیاده‌سازی شوند) و هم اعضای غیر `abstract` (با پیاده‌سازی پیش‌فرض) داشته باشد. همچنین می‌تواند فیلدها (fields)، سازنده‌ها (constructors) و اعضای استاتیک (static members) داشته باشد.
    • اینترفیس: تا C# 8.0 تنها شامل امضای اعضا بود و هیچ پیاده‌سازی‌ای نداشت. از C# 8.0 به بعد، می‌توانند متدهای پیش‌فرض (Default Interface Methods) را داشته باشند، اما همچنان نمی‌توانند فیلدهای نمونه (instance fields) یا سازنده‌ها داشته باشند.
  • ارث‌بری/پیاده‌سازی:

    • کلاس `abstract`: یک کلاس تنها می‌تواند از یک کلاس پایه (abstract یا غیر abstract) به ارث ببرد.
    • اینترفیس: یک کلاس می‌تواند چندین اینترفیس را پیاده‌سازی کند. این به C# اجازه می‌دهد تا به نوعی “چند ارث‌بری” رفتار را از طریق اینترفیس‌ها شبیه‌سازی کند.
  • رابطه:

    • کلاس `abstract`: بهترین گزینه برای تعریف یک نوع پایه (Base Type) است که دارای رفتار مشترک و ویژگی‌های مشترک برای انواع مختلفی است که رابطه “یک-است-از” قوی دارند (مثلاً `Shape` به عنوان کلاس پایه برای `Circle` و `Rectangle`).
    • اینترفیس: بهترین گزینه برای تعریف قابلیت‌ها یا قراردادهایی است که کلاس‌ها می‌توانند بدون توجه به سلسله‌مراتب ارث‌بری خود، آن‌ها را پیاده‌سازی کنند (مثلاً `ILogger`، `ISerializable`).

انتخاب بین `abstract` class و Interface:

معمولاً از کلاس `abstract` زمانی استفاده می‌کنیم که:

  • می‌خواهیم کد مشترکی بین چندین کلاس مشتق شده به اشتراک بگذاریم.
  • می‌خواهیم رفتار پیش‌فرض را ارائه دهیم که کلاس‌های مشتق شده می‌توانند آن را بازنویسی کنند.
  • اعضای protected یا private مورد نیاز هستند.
  • می‌خواهیم یک سلسله‌مراتب نوع قوی (strong type hierarchy) ایجاد کنیم.

و از اینترفیس زمانی استفاده می‌کنیم که:

  • می‌خواهیم رفتار خاصی را تعریف کنیم که ممکن است توسط کلاس‌های مختلف با سلسله‌مراتب‌های ارث‌بری متفاوت پیاده‌سازی شود.
  • می‌خواهیم قابلیت چند پیاده‌سازی (multiple implementations) از یک قرارداد را داشته باشیم.
  • نیاز به قابلیت تست‌پذیری و انعطاف‌پذیری بالا (مانند Dependency Injection) داریم.

Composition over Inheritance: اصل طلایی طراحی

یکی از مهم‌ترین اصول طراحی شی‌گرا، “Composition over Inheritance” (ترکیب بر ارث‌بری) است. این اصل بیان می‌کند که به جای ارث بردن از یک کلاس برای استفاده مجدد از قابلیت‌های آن، بهتر است که یک شیء از آن کلاس را به عنوان یک کامپوننت (component) در کلاس خود گنجانده (compose) و از طریق آن با قابلیت‌هایش تعامل کنید.

چرا ترکیب بهتر است؟

  • کاهش وابستگی: ارث‌بری یک وابستگی محکم (tight coupling) بین کلاس والد و فرزند ایجاد می‌کند. تغییرات در والد می‌تواند به طور ناخواسته فرزند را تحت تأثیر قرار دهد. ترکیب، وابستگی ضعیف‌تری (loose coupling) ایجاد می‌کند، زیرا کلاس‌ها به جای اینکه “یک-است-از” یکدیگر باشند، “یک-دارد-از” (has-a) یکدیگر هستند.
  • انعطاف‌پذیری بیشتر: با ترکیب، می‌توانید رفتار یک کلاس را در زمان اجرا تغییر دهید، به سادگی با جایگزینی کامپوننت داخلی. با ارث‌بری، ساختار کلاس ثابت است.
  • اجتناب از مشکلات سلسله‌مراتب: سلسله‌مراتب ارث‌بری عمیق می‌تواند پیچیده و دشوار برای نگهداری شود. ترکیب به شما امکان می‌دهد تا سیستم‌های خود را از قطعات کوچک‌تر و قابل مدیریت‌تر بسازید.
  • عدم نیاز به ارث‌بری غیرضروری: گاهی اوقات یک کلاس فقط برای استفاده از یک یا دو متد از کلاس پایه به ارث می‌برد که این کار می‌تواند ساختار را بی جهت پیچیده کند. ترکیب این مشکل را حل می‌کند.

مثال: سیستم پرواز در بازی


// Bad example: Inheritance for behavior
public class Bird : Animal
{
    public void Fly() { Console.WriteLine("Bird is flying."); }
}

public class Penguin : Bird // Problem: Penguin cannot fly
{
    // Need to override Fly() to do nothing or throw error
    public override void Fly() { Console.WriteLine("Penguins can't fly!"); }
}

// Good example: Composition for behavior
public interface IFlyBehavior
{
    void Fly();
}

public class CanFly : IFlyBehavior
{
    public void Fly() { Console.WriteLine("Flapping wings!"); }
}

public class CannotFly : IFlyBehavior
{
    public void Fly() { Console.WriteLine("Cannot fly."); }
}

public class NewBird
{
    private IFlyBehavior _flyBehavior;

    public NewBird(IFlyBehavior flyBehavior)
    {
        _flyBehavior = flyBehavior;
    }

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

    public void SetFlyBehavior(IFlyBehavior flyBehavior)
    {
        _flyBehavior = flyBehavior;
    }
}

// Usage
NewBird duck = new NewBird(new CanFly());
duck.PerformFly(); // Output: Flapping wings!

NewBird penguin = new NewBird(new CannotFly());
penguin.PerformFly(); // Output: Cannot fly.

// Change behavior at runtime
duck.SetFlyBehavior(new CannotFly());
duck.PerformFly(); // Output: Cannot fly.

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

چندریختی در عمل: فراتر از `virtual` و `override`

در حالی که `virtual` و `override` سنگ بنای چندریختی زمان اجرا هستند، جنبه‌های دیگری نیز وجود دارد که به چندریختی قدرت و انعطاف‌پذیری بیشتری می‌بخشد.

چندریختی با اینترفیس‌ها: نهایت انعطاف‌پذیری

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

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


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); }
}

// Usage demonstrating interface polymorphism
public static class ShapeCalculator
{
    public static void PrintShapeDetails(IShape shape)
    {
        Console.WriteLine($"Area: {shape.GetArea()}, Perimeter: {shape.GetPerimeter()}");
    }
}

// In Main method or another class:
IShape myCircle = new Circle(5);
IShape myRectangle = new Rectangle(4, 6);

ShapeCalculator.PrintShapeDetails(myCircle);    // Output: Area: 78.53..., Perimeter: 31.41...
ShapeCalculator.PrintShapeDetails(myRectangle); // Output: Area: 24, Perimeter: 20

در این مثال، متد `PrintShapeDetails` نیازی به دانستن اینکه آیا شیء `Circle` است یا `Rectangle` ندارد؛ تنها با اینترفیس `IShape` کار می‌کند. این قابلیت، بنیاد بسیاری از الگوهای طراحی پیشرفته و فریم‌ورک‌های مدرن است.

چندریختی عمومی (Generic Polymorphism): کار با انواع ناشناخته

Generics (ژنریک‌ها) در C# به شما اجازه می‌دهند تا کلاس‌ها، متدها و اینترفیس‌ها را با یک یا چند پارامتر نوع تعریف کنید. این قابلیت، نوعی از چندریختی است که به شما اجازه می‌دهد تا کدی بنویسید که روی انواع مختلف داده کار کند، بدون اینکه نیاز به کپی‌پیست کردن کد یا استفاده از `object` و تبدیل نوع (casting) باشد.

ژنریک‌ها چندریختی را در سطح نوع (Type-level polymorphism) ارائه می‌دهند. به عنوان مثال، `List<T>` می‌تواند یک لیست از `int`، `string` یا هر نوع دلخواه دیگری باشد، و متدهای آن به طور یکسان برای همه این انواع کار می‌کنند.


public class GenericProcessor<T>
{
    public void Process(T item)
    {
        Console.WriteLine($"Processing item of type {typeof(T).Name}: {item}");
    }
}

// Usage
GenericProcessor<int> intProcessor = new GenericProcessor<int>();
intProcessor.Process(100); // Output: Processing item of type Int32: 100

GenericProcessor<string> stringProcessor = new GenericProcessor<string>();
stringProcessor.Process("Hello Generics"); // Output: Processing item of type String: Hello Generics

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

کوواریانس (Covariance) و کنتراواریانس (Contravariance): انعطاف‌پذیری در وراثت نوع

کوواریانس و کنتراواریانس، مفاهیم پیشرفته‌تری هستند که انعطاف‌پذیری بیشتری را در تخصیص نوع در سیستم‌های چندریختی C# فراهم می‌کنند. این مفاهیم بیشتر در مورد اینترفیس‌ها و Delegateهای عمومی (Generic Delegates) کاربرد دارند.

  • کوواریانس (`out` keyword): به شما اجازه می‌دهد تا یک نوع عمومی را با نوع مشتق شده‌تری نسبت به آنچه که در اصل مشخص شده است، استفاده کنید. این قابلیت برای پارامترهای نوع خروجی (return types) در اینترفیس‌ها و Delegateها کاربرد دارد. یعنی اگر `TDerived` از `TBase` مشتق شده باشد، `IEnumerable` را می‌توان به عنوان `IEnumerable` در نظر گرفت. (مثال: `IEnumerable` به `IEnumerable`)

    
            // public interface IEnumerable<out T> : IEnumerable
            // 'out' keyword indicates covariance
    
            IEnumerable<string> strings = new List<string> { "Hello", "World" };
            IEnumerable<object> objects = strings; // Valid due to covariance
            
  • کنتراواریانس (`in` keyword): به شما اجازه می‌دهد تا یک نوع عمومی را با نوع پایه‌ای‌تر از آنچه که در اصل مشخص شده است، استفاده کنید. این قابلیت برای پارامترهای نوع ورودی (method arguments) در اینترفیس‌ها و Delegateها کاربرد دارد. یعنی اگر `TDerived` از `TBase` مشتق شده باشد، یک Delegate/Interface که `TBase` را به عنوان ورودی می‌گیرد را می‌توان به عنوان یک Delegate/Interface که `TDerived` را به عنوان ورودی می‌گیرد، استفاده کرد. (مثال: `Action` به `Action`)

    
            // public delegate void Action<in T>(T obj);
            // 'in' keyword indicates contravariance
    
            Action<object> printObject = (obj) => Console.WriteLine(obj.ToString());
            Action<string> printString = printObject; // Valid due to contravariance
            printString("C# Advanced");
            

    کوواریانس و کنتراواریانس در سناریوهای پیشرفته‌تر برای افزایش انعطاف‌پذیری سیستم‌های نوع در C# به کار می‌روند، به ویژه در کار با LINQ، فریم‌ورک‌های ORM و کتابخانه‌های پایه که با مجموعه داده‌ها یا دلیگیت‌ها سروکار دارند.

    الگوهای طراحی (Design Patterns) مرتبط با ارث‌بری و چندریختی

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

    الگوی متد الگو (Template Method Pattern)

    این الگو، یک اسکلت از الگوریتم را در یک متد از یک کلاس پایه تعریف می‌کند، اما مراحل خاصی از الگوریتم را به کلاس‌های مشتق شده واگذار می‌کند. ارث‌بری، و به‌ویژه متدهای `abstract` و `virtual`، هسته این الگو را تشکیل می‌دهند. این الگو تضمین می‌کند که ساختار کلی یک عملیات ثابت باقی بماند، در حالی که جزئیات پیاده‌سازی آن قابل تغییر است.

    
    public abstract class ReportGenerator
    {
        // Template Method
        public void GenerateReport()
        {
            GetData();
            ProcessData();
            FormatReport();
            PrintReport();
        }
    
        protected abstract void GetData();
        protected abstract void ProcessData();
        protected virtual void FormatReport() // Can have default implementation
        {
            Console.WriteLine("Formatting report with default style.");
        }
        protected abstract void PrintReport();
    }
    
    public class PdfReportGenerator : ReportGenerator
    {
        protected override void GetData() { Console.WriteLine("Getting data for PDF report."); }
        protected override void ProcessData() { Console.WriteLine("Processing data for PDF report."); }
        protected override void PrintReport() { Console.WriteLine("Printing PDF report."); }
    }
    
    public class ExcelReportGenerator : ReportGenerator
    {
        protected override void GetData() { Console.WriteLine("Getting data for Excel report."); }
        protected override void ProcessData() { Console.WriteLine("Processing data for Excel report."); }
        protected override void FormatReport() // Override default for Excel
        {
            Console.WriteLine("Formatting report for Excel spreadsheet.");
        }
        protected override void PrintReport() { Console.WriteLine("Printing Excel report."); }
    }
    
    // Usage
    ReportGenerator pdfGen = new PdfReportGenerator();
    pdfGen.GenerateReport();
    
    ReportGenerator excelGen = new ExcelReportGenerator();
    excelGen.GenerateReport();
    

    الگوی استراتژی (Strategy Pattern)

    الگوی استراتژی به شما اجازه می‌دهد تا مجموعه‌ای از الگوریتم‌ها را تعریف کنید، هر یک را درون یک کلاس encapsulate کنید، و آن‌ها را قابل تعویض (interchangeable) سازید. چندریختی (اغلب از طریق اینترفیس‌ها) و ترکیب، نقش اصلی را در این الگو ایفا می‌کنند. به جای استفاده از ارث‌بری برای تغییر رفتار، کلاس‌ها یک اینترفیس استراتژی را نگهداری می‌کنند و رفتار را به شیء استراتژی delegate می‌کنند.

    
    public interface ISortStrategy
    {
        void Sort(List<int> data);
    }
    
    public class BubbleSort : ISortStrategy
    {
        public void Sort(List<int> data) { Console.WriteLine("Sorting using Bubble Sort."); }
    }
    
    public class QuickSort : ISortStrategy
    {
        public void Sort(List<int> data) { Console.WriteLine("Sorting using Quick Sort."); }
    }
    
    public class Sorter
    {
        private ISortStrategy _strategy;
    
        public Sorter(ISortStrategy strategy)
        {
            _strategy = strategy;
        }
    
        public void SetStrategy(ISortStrategy strategy)
        {
            _strategy = strategy;
        }
    
        public void PerformSort(List<int> data)
        {
            _strategy.Sort(data);
        }
    }
    
    // Usage
    Sorter sorter = new Sorter(new BubbleSort());
    sorter.PerformSort(new List<int> { 5, 2, 8 }); // Output: Sorting using Bubble Sort.
    
    sorter.SetStrategy(new QuickSort());
    sorter.PerformSort(new List<int> { 1, 9, 3 }); // Output: Sorting using Quick Sort.
    

    الگوی متد کارخانه (Factory Method Pattern)

    این الگو یک اینترفیس برای ایجاد یک شیء تعریف می‌کند، اما به زیرکلاس‌ها اجازه می‌دهد تا تصمیم بگیرند کدام کلاس را نمونه‌سازی کنند. این الگو از چندریختی (متدهای `virtual` یا `abstract`) برای defer کردن فرآیند ایجاد شیء به کلاس‌های مشتق شده استفاده می‌کند. این امر، سیستم را نسبت به نوع اشیائی که ایجاد می‌کند، decoupled می‌کند.

    
    public abstract class Product
    {
        public abstract void Ship();
    }
    
    public class ConcreteProductA : Product
    {
        public override void Ship() { Console.WriteLine("Shipping Product A."); }
    }
    
    public class ConcreteProductB : Product
    {
        public override void Ship() { Console.WriteLine("Shipping Product B."); }
    }
    
    public abstract class Creator
    {
        public abstract Product FactoryMethod(); // Factory Method
    
        public void AnOperation()
        {
            Product product = FactoryMethod();
            product.Ship();
        }
    }
    
    public class ConcreteCreatorA : Creator
    {
        public override Product FactoryMethod() { return new ConcreteProductA(); }
    }
    
    public class ConcreteCreatorB : Creator
    {
        public override Product FactoryMethod() { return new ConcreteProductB(); }
    }
    
    // Usage
    Creator creatorA = new ConcreteCreatorA();
    creatorA.AnOperation(); // Output: Shipping Product A.
    
    Creator creatorB = new ConcreteCreatorB();
    creatorB.AnOperation(); // Output: Shipping Product B.
    

    الگوی دکوراتور (Decorator Pattern)

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

    
    public interface ICoffee
    {
        string GetDescription();
        double GetCost();
    }
    
    public class SimpleCoffee : ICoffee
    {
        public string GetDescription() { return "Simple Coffee"; }
        public double GetCost() { return 2.0; }
    }
    
    public abstract class CoffeeDecorator : ICoffee
    {
        protected ICoffee _decoratedCoffee;
    
        public CoffeeDecorator(ICoffee coffee)
        {
            _decoratedCoffee = coffee;
        }
    
        public virtual string GetDescription() { return _decoratedCoffee.GetDescription(); }
        public virtual double GetCost() { return _decoratedCoffee.GetCost(); }
    }
    
    public class MilkDecorator : CoffeeDecorator
    {
        public MilkDecorator(ICoffee coffee) : base(coffee) { }
    
        public override string GetDescription() { return _decoratedCoffee.GetDescription() + ", Milk"; }
        public override double GetCost() { return _decoratedCoffee.GetCost() + 0.5; }
    }
    
    public class SugarDecorator : CoffeeDecorator
    {
        public SugarDecorator(ICoffee coffee) : base(coffee) { }
    
        public override string GetDescription() { return _decoratedCoffee.GetDescription() + ", Sugar"; }
        public override double GetCost() { return _decoratedCoffee.GetCost() + 0.2; }
    }
    
    // Usage
    ICoffee coffee = new SimpleCoffee();
    Console.WriteLine($"{coffee.GetDescription()}, Cost: {coffee.GetCost()}"); // Simple Coffee, Cost: 2
    
    ICoffee milkCoffee = new MilkDecorator(coffee);
    Console.WriteLine($"{milkCoffee.GetDescription()}, Cost: {milkCoffee.GetCost()}"); // Simple Coffee, Milk, Cost: 2.5
    
    ICoffee sweetenedMilkCoffee = new SugarDecorator(milkCoffee);
    Console.WriteLine($"{sweetenedMilkCoffee.GetDescription()}, Cost: {sweetenedMilkCoffee.GetCost()}"); // Simple Coffee, Milk, Sugar, Cost: 2.7
    

    چالش‌ها و سوء‌کاربردهای ارث‌بری و چندریختی

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

    مشکل کلاس پایه شکننده (Fragile Base Class Problem)

    این یکی از شناخته‌شده‌ترین مشکلات مرتبط با ارث‌بری است. زمانی رخ می‌دهد که تغییرات در یک کلاس پایه (حتی تغییرات seemingly بی‌اهمیت) به طور ناخواسته رفتار کلاس‌های مشتق شده را خراب می‌کند. این به دلیل وابستگی محکم (tight coupling) است که ارث‌بری بین کلاس والد و فرزند ایجاد می‌کند. برای مثال، اگر یک متد جدید با همان نام یک متد در کلاس مشتق شده به کلاس پایه اضافه شود، یا اگر ترتیب فراخوانی متدهای virtual در کلاس پایه تغییر کند، ممکن است کلاس‌های مشتق شده خراب شوند.

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

    وابستگی محکم (Tight Coupling)

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

    راه حل: استفاده حداکثری از اینترفیس‌ها برای تعریف وابستگی‌ها و پیاده‌سازی Dependency Injection. این رویکرد باعث می‌شود که کامپوننت‌ها به جای وابستگی به پیاده‌سازی‌های خاص، به انتزاعیات (اینترفیس‌ها) وابسته باشند.

    نقض اصل جایگزینی لیسکوف (Liskov Substitution Principle – LSP)

    LSP یکی از اصول SOLID است که بیان می‌کند: “اشیاء یک نوع پایه باید بتوانند با اشیاء نوع مشتق شده خود بدون تغییر در صحت برنامه جایگزین شوند.” به عبارت ساده‌تر، اگر `S` یک زیرنوع از `T` است، آنگاه اشیاء از نوع `T` ممکن است با اشیاء از نوع `S` بدون تغییر خواص مطلوب آن برنامه جایگزین شوند.

    نقض LSP زمانی اتفاق می‌افتد که یک کلاس مشتق شده، رفتاری غیرمنتظره یا ناسازگار با کلاس پایه خود نشان می‌دهد. این می‌تواند شامل:

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

    مثال نقض LSP (Anti-Pattern: Square is a Rectangle)

    
    public class Rectangle
    {
        public virtual int Width { get; set; }
        public virtual int Height { get; set; }
        public int Area => Width * Height;
    }
    
    public class Square : Rectangle
    {
        public override int Width
        {
            get { return base.Width; }
            set { base.Width = base.Height = value; } // Violates LSP
        }
    
        public override int Height
        {
            get { return base.Height; }
            set { base.Width = base.Height = value; } // Violates LSP
        }
    }
    
    // Problematic Usage
    public static void TestRectangleArea(Rectangle r)
    {
        r.Width = 5;
        r.Height = 10;
        Console.WriteLine($"Expected Area: 50, Actual Area: {r.Area}");
    }
    
    // In Main
    Rectangle rect = new Rectangle();
    TestRectangleArea(rect); // Output: Expected Area: 50, Actual Area: 50
    
    Square sq = new Square();
    TestRectangleArea(sq); // Output: Expected Area: 50, Actual Area: 100 (if Height set last) or 25 (if Width set last)
                          // The behavior of Square.Width/Height setter is not consistent with Rectangle's
                          // It breaks the expectation that setting Width/Height independently works.
    

    در این مثال، `Square` از `Rectangle` به ارث می‌برد، اما setterهای `Width` و `Height` در `Square` به گونه‌ای رفتار می‌کنند که هر دو بعد را تغییر می‌دهند، که این رفتار `Rectangle` را نقض می‌کند. `Square` در واقع “یک-مستطیل-است” که می‌تواند جایگزین هر `Rectangle` شود، اما رفتار آن را حفظ نمی‌کند.

    راه حل: استفاده از اینترفیس‌ها برای تعریف قابلیت‌ها یا استفاده از ترکیب. در این مورد خاص، `Square` و `Rectangle` می‌توانند هر دو از اینترفیس `IShape` استفاده کنند یا یک کلاس پایه انتزاعی مشترک داشته باشند که هیچ فرضی در مورد ابعاد خاص ندارد.

    سلسله‌مراتب‌های ارث‌بری عمیق

    ایجاد سلسله‌مراتب‌های ارث‌بری بسیار عمیق (deep inheritance hierarchies) می‌تواند منجر به کدهای دشوار برای درک، نگهداری و تست شود. هرچه سلسله‌مراتب عمیق‌تر باشد، وابستگی‌ها پیچیده‌تر می‌شوند و تشخیص اینکه چه کدی در کجا اجرا می‌شود، دشوارتر خواهد بود.

    راه حل: تلاش برای نگه داشتن سلسله‌مراتب نسبتاً کم عمق و ترجیح ترکیب بر ارث‌بری. هر کلاس باید مسئولیت واحدی داشته باشد (اصل مسئولیت واحد – Single Responsibility Principle).

    پرمخاطره بودن Overriding

    بازنویسی متدها (`override`) باید با احتیاط انجام شود. هرگاه متدی را بازنویسی می‌کنید، باید مطمئن شوید که رفتار جدید همچنان با قرارداد متد پایه سازگار است و LSP را نقض نمی‌کند. همچنین، توجه به متدهای `virtual` در کلاس‌های پایه از کتابخانه‌های شخص ثالث مهم است؛ این متدها ممکن است برای توسعه توسط ارث‌بری در نظر گرفته نشده باشند.

    راه حل: مستندسازی واضح رفتار متدهای `virtual` در کلاس‌های پایه و پیروی از قراردادها. در صورت امکان، از اینترفیس‌ها برای تعریف قراردادها استفاده کنید تا از سوءتفاهم در مورد انتظارات رفتاری جلوگیری شود.

    ارث‌بری و چندریختی در اکوسیستم .NET و C# پیشرفته

    ارث‌بری و چندریختی نه تنها مفاهیم تئوری هستند، بلکه در سراسر فریم‌ورک .NET و ویژگی‌های پیشرفته C# نیز نفوذ کرده‌اند. درک نحوه استفاده از آن‌ها در این زمینه‌ها، به شما کمک می‌کند تا از قدرت کامل زبان و فریم‌ورک بهره‌مند شوید.

    LINQ و IQueryable/IEnumerable

    Language Integrated Query (LINQ) به شدت بر چندریختی متکی است. به عنوان مثال، `IEnumerable` یک اینترفیس است که به انواع مختلفی از مجموعه‌ها (مثل `List`, `Array`, `HashSet`) اجازه می‌دهد تا از طریق یک اینترفیس واحد query شوند. این چندریختی به LINQ اجازه می‌دهد تا به صورت یکپارچه با منابع داده مختلف کار کند، بدون اینکه نیاز به کد خاص برای هر نوع مجموعه باشد.

    `IQueryable` نیز یک اینترفیس مشتق شده از `IEnumerable` است که چندریختی را به سطوح بالاتری می‌برد. `IQueryable` به LINQ اجازه می‌دهد تا queryها را به یک زبان قابل فهم برای منبع داده (مانند SQL برای پایگاه داده یا URL برای سرویس‌های وب) ترجمه کند، که این امر به لطف قابلیت چندریختی و پیاده‌سازی‌های مختلف اینترفیس `IQueryable` است.

    
    List<int> numbers = new List<int> { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
    IEnumerable<int> evenNumbers = numbers.Where(n => n % 2 == 0); // Where is an extension method on IEnumerable<T>
    
    // If we were working with a database context:
    // var dbContext = new MyDbContext();
    // IQueryable<Product> expensiveProducts = dbContext.Products.Where(p => p.Price > 100);
    // This 'Where' would be translated to SQL, showcasing polymorphism of IQueryable.
    

    Dependency Injection (DI)

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

    مزایای DI (با کمک polymorphism):

    • تست‌پذیری: می‌توانید به راحتی پیاده‌سازی‌های mock یا stub از وابستگی‌ها را برای تست واحد (Unit Testing) تزریق کنید.
    • انعطاف‌پذیری: تغییر پیاده‌سازی یک وابستگی (مثلاً تغییر دیتابیس) بدون تغییر کدی که از آن استفاده می‌کند، آسان می‌شود.
    • کاهش وابستگی محکم: کلاس‌ها به اینترفیس‌ها وابسته می‌شوند، نه به پیاده‌سازی‌های بتنی.
    
    public interface ILogger
    {
        void Log(string message);
    }
    
    public class ConsoleLogger : ILogger
    {
        public void Log(string message) { Console.WriteLine($"Console Log: {message}"); }
    }
    
    public class FileLogger : ILogger
    {
        public void Log(string message) { Console.WriteLine($"File Log: {message}"); } // In reality, writes to file
    }
    
    public class MyService
    {
        private readonly ILogger _logger;
    
        // Dependency Injected via constructor
        public MyService(ILogger logger)
        {
            _logger = logger;
        }
    
        public void DoSomething()
        {
            _logger.Log("Doing something important.");
        }
    }
    
    // Usage with a simple DI container (or manually)
    // ILogger logger = new ConsoleLogger(); // Or new FileLogger();
    // MyService service = new MyService(logger);
    // service.DoSomething();
    

    رکوردها (Records) و وراثت

    Records در C# 9.0 معرفی شدند تا کلاس‌هایی با معنای مقدار (value semantics) و ویژگی‌های immutable را ساده‌تر ایجاد کنند. Records نیز می‌توانند از یکدیگر ارث ببرند، که این قابلیت چندریختی را برای آن‌ها نیز به ارمغان می‌آورد. با این حال، باید توجه داشت که `with` expression (که برای ایجاد کپی‌های تغییر یافته از رکوردها استفاده می‌شود) در سلسله‌مراتب‌های ارث‌بری، مفهوم متفاوتی از کپی را ارائه می‌دهد که باید به آن دقت کرد.

    
    public record Person(string FirstName, string LastName);
    public record Employee(string FirstName, string LastName, int EmployeeId) : Person(FirstName, LastName);
    
    Employee emp = new Employee("John", "Doe", 123);
    Person person = emp; // Valid due to inheritance
    
    Console.WriteLine(person.FirstName); // John
    

    استفاده از `with` expression با رکوردهای مشتق شده، به طور پیش‌فرض، یک کپی از نوع دقیق شیء فعلی (نه لزوماً نوع پایه) ایجاد می‌کند، که رفتار “covariant returns” را برای آن فراهم می‌کند.

    Pattern Matching

    Pattern Matching در C# (با استفاده از `is`, `switch` expressions, `switch` statements) قابلیت‌های چندریختی را در زمان اجرا به طور قوی‌تر و خواناتری پشتیبانی می‌کند. به شما اجازه می‌دهد تا بر اساس نوع واقعی یک شیء، منطق متفاوتی را اعمال کنید و به طور ایمن به اعضای خاص آن نوع دسترسی پیدا کنید، بدون نیاز به cast صریح.

    
    public class Shape { }
    public class Circle : Shape { public double Radius { get; set; } }
    public class Rectangle : Shape { public double Width { get; set; } public double Height { get; set; } }
    
    public void PrintShapeInfo(Shape shape)
    {
        switch (shape)
        {
            case Circle c:
                Console.WriteLine($"Circle with Radius: {c.Radius}");
                break;
            case Rectangle r:
                Console.WriteLine($"Rectangle with Width: {r.Width} and Height: {r.Height}");
                break;
            default:
                Console.WriteLine("Unknown shape.");
                break;
        }
    }
    
    // Usage
    PrintShapeInfo(new Circle { Radius = 10 });
    PrintShapeInfo(new Rectangle { Width = 5, Height = 7 });
    PrintShapeInfo(new Shape());
    

    Pattern matching یک راه قدرتمند برای کار با سلسله‌مراتب‌های ارث‌بری و اینترفیس‌ها است، به ویژه زمانی که نیاز به انجام عملیات متفاوت بر اساس نوع خاص شیء دارید.

    بهترین شیوه‌ها (Best Practices) و توصیه‌های کلیدی

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

    ۱. ترکیب را بر ارث‌بری ترجیح دهید (Favor Composition Over Inheritance)

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

    ۲. اینترفیس‌ها را به طور گسترده استفاده کنید (Use Interfaces Extensively)

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

    ۳. از اصول SOLID پیروی کنید

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

    • مسئولیت واحد (Single Responsibility Principle – SRP): هر کلاس (یا ماژول) باید تنها یک دلیل برای تغییر داشته باشد. این به شما کمک می‌کند کلاس‌های پایه کوچکتر و با تمرکز بیشتری داشته باشید.
    • باز/بسته (Open/Closed Principle – OCP): موجودیت‌های نرم‌افزاری (کلاس‌ها، ماژول‌ها، توابع و غیره) باید برای توسعه “باز” باشند، اما برای تغییر “بسته” باشند. یعنی بتوانید قابلیت‌های جدید را اضافه کنید بدون اینکه کد موجود را تغییر دهید. ارث‌بری و چندریختی (به ویژه از طریق اینترفیس‌ها) ابزارهای اصلی برای تحقق OCP هستند.
    • جایگزینی لیسکوف (Liskov Substitution Principle – LSP): همانطور که قبلاً بحث شد، اشیاء نوع پایه باید بتوانند بدون تغییر در صحت برنامه، با اشیاء نوع مشتق شده خود جایگزین شوند. این اصل به شما کمک می‌کند سلسله‌مراتب‌های ارث‌بری درست و با رفتاری قابل پیش‌بینی بسازید.
    • جداسازی اینترفیس (Interface Segregation Principle – ISP): کلاینت‌ها نباید مجبور به پیاده‌سازی اینترفیس‌هایی شوند که از آن‌ها استفاده نمی‌کنند. اینترفیس‌های بزرگ را به اینترفیس‌های کوچکتر و خاص‌تر تقسیم کنید.
    • وارونگی وابستگی (Dependency Inversion Principle – DIP): ماژول‌های سطح بالا نباید به ماژول‌های سطح پایین وابسته باشند. هر دو باید به انتزاعیات (اینترفیس‌ها) وابسته باشند. انتزاعیات نباید به جزئیات وابسته باشند. جزئیات باید به انتزاعیات وابسته باشند. این اصل پایه و اساس Dependency Injection است.

    ۴. کلاس‌های پایه را به گونه‌ای طراحی کنید که برای ارث‌بری ایمن باشند

    اگر قصد دارید کلاسی را برای ارث‌بری طراحی کنید، باید این کار را با دقت انجام دهید تا از مشکل کلاس پایه شکننده جلوگیری شود:

    • فقط متدهایی را `virtual` کنید که واقعاً قصد بازنویسی آن‌ها را دارید.
    • اعضایی که برای استفاده داخلی هستند را `protected` یا `private` اعلام کنید.
    • مستندسازی واضحی از انتظارات برای هر متد `virtual` ارائه دهید.
    • اگر کلاسی برای ارث‌بری در نظر گرفته نشده است، آن را `sealed` کنید تا از سوءاستفاده جلوگیری شود.

    ۵. مسئولیت‌های کلاس پایه را کوچک و متمرکز نگه دارید (SRP)

    یک کلاس پایه نباید بیش از حد وظیفه داشته باشد. اگر یک کلاس پایه مسئولیت‌های متعددی داشته باشد، تغییر در یکی از آن مسئولیت‌ها می‌تواند بر تمام کلاس‌های مشتق شده تأثیر بگذارد. این مفهوم “مشکل کلاس پایه چاق” (Fat Base Class Problem) نامیده می‌شود. سعی کنید کلاس‌های پایه را به گونه‌ای طراحی کنید که یک مسئولیت واحد و واضح داشته باشند.

    ۶. تست کنید، تست کنید، تست کنید!

    سیستم‌های چندریختی می‌توانند پیچیده باشند، به خصوص زمانی که چندین سطح از ارث‌بری و اینترفیس‌ها درگیر هستند. تست واحد (Unit Testing) برای اطمینان از صحت رفتار در تمامی سناریوهای ممکن حیاتی است. مطمئن شوید که متدهای `virtual` و `override` شده به درستی کار می‌کنند و LSP رعایت می‌شود.

    ۷. به جای “چه چیزی هستی؟”، “چه کاری می‌توانی بکنی؟” بپرسید

    این یک تغییر دیدگاه مهم در برنامه‌نویسی شی‌گرا است. به جای تمرکز بر سلسله‌مراتب کلاس‌ها و اینکه یک شیء از چه نوع خاصی است، بر روی قابلیت‌های آن تمرکز کنید. اینترفیس‌ها بهترین راه برای بیان “چه کاری می‌توانی بکنی؟” هستند. این رویکرد به کد شما انعطاف‌پذیری و قابلیت توسعه‌پذیری بسیار بیشتری می‌بخشد.

    نتیجه‌گیری

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

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

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

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

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

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

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

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

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

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

    سبد خرید