برنامه نویسی

چند شکلی پویا: مفهوم کلیدی برای تسلط بر OOP

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

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

public class A
{
    public virtual void Foo()
    {
        Console.WriteLine("A");
    }
}

public class B : A
{
    public override void Foo()
    {
        Console.WriteLine("B");
    }
}

public class C : A
{
    public void Foo()
    {
        Console.WriteLine("C");
    }
}

var b = new B();
var c = new C();
b.Foo();
c.Foo();
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

آیا می توانید بگویید که کد خروجی در هر مورد چیست؟ اگر به درستی پاسخ داده اید که خروجی مربوطه “B” و “C” خواهد بود، پس چرا این کار را می کند override کلمه کلیدی مهم است؟

چند شکلی پویا را وارد کنید

اعتقاد بر این است که چند شکلی یکی از ارکان برنامه نویسی شی گرا است. اما دقیقا به چه معناست؟ ویکی‌پدیا به ما می‌گوید که چندشکلی ارائه یک رابط واحد برای موجودیت‌های انواع مختلف یا استفاده از یک نماد واحد برای نشان دادن چندین نوع مختلف است.

من انتظار ندارم که شما از اولین بار این تعریف را درک کنید، بنابراین اجازه دهید به چند نمونه نگاهی بیندازیم.

string Add(string input1, string input2) => string.Concat(input1, input2);
int Add(int input1, int input2) => input1 + input2;
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در بالا نمونه‌ای از چندشکلی ad-hoc وجود دارد که به توابع چندشکلی اشاره دارد که می‌توانند برای آرگومان‌های انواع مختلف اعمال شوند، اما بسته به نوع استدلالی که برای آن اعمال می‌شوند، رفتار متفاوتی دارند. پس چرا چندشکلی برای کدهای شی گرا اینقدر مهم است؟ این قطعه پاسخ روشنی به این سوال ارائه نمی دهد. بیایید نمونه های بیشتری را بررسی کنیم.

class List<T> {
    class Node<T> {
        T elem;
        Node<T> next;
    }
    Node<T> head;
    int length() { ... }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این نمونه‌ای از چندشکلی پارامتریک است و صادقانه بگوییم که بیشتر کاربردی به نظر می‌رسد تا شی گرا. بیایید نگاهی به مثال نهایی بیندازیم.

interface IDiscountCalculator
{
    decimal CalculateDiscount(Item item);
}

class ThanksgivingDayDiscountCalculator : IDiscountCalculator
{
    public decimal CalculateDiscount(Item discount)
    {
        //omitted
    }
}

class RegularCustomerDiscountCalculator : IDiscountCalculator
{
    public decimal CalculateDiscount(Item discount)
    {
        //omitted
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

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

چند شکلی پویا و الگوهای طراحی

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

چند شکلی پویا مفهوم کلیدی برای تسلط بر OOP

در بالا نمودار الگوی استراتژی است که در آن Client با انتزاع کار می کند و اجرای ملموس آن در زمان اجرا انتخاب می شود.

و اینجا دکوراتور است.

1688695645 56 چند شکلی پویا مفهوم کلیدی برای تسلط بر OOP

در این حالت، wrapper wrappee را می‌پذیرد که نمونه‌ای از انتزاع است و اجرای آن ممکن است در طول زمان اجرا متفاوت باشد.

پلی مورفیسم دینامیک و جامد

هنگامی که در طول مصاحبه در مورد SOLID می پرسم، پاسخ معمولی که می شنوم این است که “S مخفف مسئولیت واحد و O مخفف uhm …” است. برعکس، من استدلال می‌کنم که چهار حرف آخر این مخفف مهم‌تر هستند، زیرا مجموعه‌ای از پیش‌شرط‌ها را برای اجرای روان چندشکلی پویا نشان می‌دهند.

به عنوان مثال، اصل باز-بسته نشان دهنده روشی از تفکر است که در آن شما با هر مشکل جدید به عنوان زیرمجموعه ای برای انتزاع خود برخورد می کنید. به خاطر آوردن IDiscountCalculator مثال. حالتی را تصور کنید که باید یک تخفیف دیگر اضافه کنید (مثلاً برای روز پدر). برای ارضای اصل باز-بسته باید یک زیر کلاس دیگر اضافه کنید FathersDayDiscountCalculator که محاسبه را انجام می دهد.

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

class FathersDayDiscountCalculator : IDiscountCalculator
{
    public decimal CalculateDiscount(Item discount)
    {
        //omitted
    }

    public bool IsEligible(User user, DateTime date)
    {
        //omitted
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون کد تماس با برخی پیچیدگی ها روبرو خواهد شد

private IReadOnlyCollection<IDiscountCalculator> _discountCalculators;
public decimal CalculateDiscountForItem(Item item, User user)
{
    decimal result = 0;
    foreach (var  discountCalculator in _discountCalculators)
    {
        if (discountCalculator is FathersDayDiscountCalculator)
        {
            var fathersDayDiscountCalculator = discountCalculator as FathersDayDiscountCalculator;
            if (fathersDayDiscountCalculator.IsEligible(user, DateTime.UtcNow))
            {
                result += fathersDayDiscountCalculator.CalculateDiscount(item);
            }
        }
        else
        {
            result += discountCalculator.CalculateDiscount(item);
        }
    }
    return result;
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

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

یکی دیگر از مواردی که کاربرد چندشکلی دینامیکی را پیچیده می کند، داشتن انتزاع بیش از حد گسترده است. تصور کنید که ما ساخته ایم IsEligible بخشی از رابط ما و اکنون تمام کلاس های بتن آن را پیاده سازی می کنند. کد تماس بسیار ساده شده است.

private IReadOnlyCollection<IDiscountCalculator> _discountCalculators;
public decimal CalculateDiscountForItem(Item item, User user)
{
    decimal result = 0;
    foreach (var  discountCalculator in _discountCalculators)
    {
        result += discountCalculator.CalculateDiscount(item);
    }
    return result;
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اما حالا تصور کنید (می دانم که مثال کمی ساختگی است اما فقط برای استدلال!) که یکی از پیاده سازی ها انداخته است. NotImplementedException زیرا برای این نوع خاص از تخفیف منطقی نیست. در این مرحله، ممکن است مشکل را پیش بینی کنید CalculateDiscountForItem شکست با استثنای زمان اجرا.

این همان چیزی است که اصل تفکیک واسط در مورد آن است: انتزاعات را خیلی گسترده نکنید تا انواع بتن در اجرای آنها دچار مشکل نشوند و در نتیجه چندشکلی پویا خود را با موارد غیر ضروری پیچیده کنید. NotImplementedExceptionس

و در این زمان ممکن است اصل وارونگی وابستگی را در عمل رعایت کنید. در مثال بالا با مجموعه ای از انتزاعات سروکار داریم و هیچ ایده ای از انواع زمان اجرا آنها نداریم.

ترکیب را بر ارث ترجیح دهید

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

مجازی و لغو

در این مرحله، کسانی از شما که پاسخ سؤال را در ابتدای مقاله به درستی نمی دانستند، ممکن است این ظن را داشته باشند که سؤال پیچیده ای است. و در واقع در حالی که رفتار مشابه است زمانی که ما استفاده می کنیم var کلمه کلیدی، زمانی که چندشکلی پویا را اعمال می کنیم، تفاوت ها ظاهر می شوند. برای این موضوع، اجازه دهید هر دو نمونه را به نوع والد تبدیل کنیم.

A b = new B();
A c = new C();
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون خروجی به ترتیب “B” و “A” خواهد بود. داغ این را حفظ کنید؟ هدف از override کلید واژه تسهیل چندشکلی پویا است. بنابراین به این شکل فکر کنید: وقتی ما انتزاع را تزریق می کنیم، انتظار داریم که با تحقق نوع عینی و به عنوان کار کنیم override این هدف را تسهیل می کند بنابراین اجرای B استناد خواهد شد.

چرا این مهم است؟

بنابراین اکنون می دانید که چگونه تمام این سوالات مزاحم مصاحبه را حفظ کنید. اما کنجکاوترین شما ممکن است بپرسید: مزایای چنین سبک برنامه نویسی چیست؟ چرا ما تلاش می کنیم چند شکلی پویا را در پایگاه های کد شی گرا خود اعمال کنیم؟

تصور کنید ما دو روش در جایی در پایگاه کد خود داریم.

public string GetCurrencySign(string currencyCode)
{
    return currencyCode switch
    {
        "US" => "$",
        "JP" => "¥",
        _ => throw new ArgumentOutOfRangeException(nameof(currencyCode)),
    };
}

public decimal GetRoundUpAmount(decimal amount, string currencyCode)
{
    return currencyCode switch
    {
        "US" => Math.Floor(amount + 1),
        "JP" => Math.Floor(amount / 100 + 1) * 100,
        _ => throw new ArgumentOutOfRangeException(nameof(currencyCode))
    };
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

حالا تصور کنید که باید یک کشور دیگر را پشتیبانی کنیم. به نظر کار مهمی نیست، اما تصور کنید که این دو روش در یکی از آن پایگاه های کد “دنیای واقعی” با هزاران کلاس و صدها هزار خط کد پنهان شده اند. به احتمال زیاد همه جاهایی را که باید پشتیبانی کشوری را اضافه کنید فراموش خواهید کرد. این دقیقا همان بوی کد جراحی شاتگان است.

ما چگونه این را درست کنیم؟ بیایید تمام اطلاعات مربوط به کد کشور را در یک مکان استخراج کنیم.

public interface IPaymentStrategy
{
    string CurrencySign { get; }
    decimal GetRoundUpAmount(decimal amount);
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

اکنون زمانی که باید یک کد کشور جدید اضافه کنیم، مجبور می شویم رابط بالا را پیاده سازی کنیم، بنابراین قطعا چیزی را فراموش نخواهیم کرد. ما از کارخانه برای بازگشت نمونه استفاده می کنیم IPaymentStrategy.

public string GetCurrencySign(string currencyCode)
{
    var strategy = _strategyFactory.CreateStrategy(currencyCode);
    return strategy.CurrencySign;
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

در مثال بالا، بوی کد را با اعمال چندشکلی پویا رفع کردیم. گاهی اوقات ما موفق شده‌ایم برخی از اصول SOLID (یعنی Open-Closed با ایجاد قابلیت‌های جدید با برنامه‌های افزودنی به جای اصلاح) و اعمال الگوهای طراحی را رعایت کنیم. انبوهی از چیزهای جالب سازمانی برای رزومه شما با استفاده از تنها یک اصل OOD!

نتیجه

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

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

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

دکمه بازگشت به بالا