چند شکلی پویا: مفهوم کلیدی برای تسلط بر 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
}
}
این یک نمونه از چندشکلی پویا است که اصطلاحی برای اعمال چندشکلی در زمان اجرا (بیشتر از طریق زیر تایپ کردن) است. و اگر سعی کرده اید تمام این الگوهای طراحی را قبل از مصاحبه یا برخی از اصول جامد به خاطر بسپارید، ممکن است متوجه شکل چیزی آشنا شوید. بیایید ببینیم چندشکلی پویا چگونه خود را در این مفاهیم نشان می دهد.
چند شکلی پویا و الگوهای طراحی
اکثر الگوهای طراحی (استراتژی، فرمان، دکوراتور و غیره) به تزریق کلاس انتزاعی یا رابط و انتخاب اجرای آن در زمان اجرا متکی هستند. بیایید به برخی از نمودارهای کلاس نگاهی بیندازیم تا مطمئن شویم که درست است.
در بالا نمودار الگوی استراتژی است که در آن Client
با انتزاع کار می کند و اجرای ملموس آن در زمان اجرا انتخاب می شود.
و اینجا دکوراتور است.
در این حالت، 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، الگوهای طراحی) صرفاً یادگاریهایی هستند که حول آن ساخته شدهاند.