این سند شامل سوالات و پاسخهای مربوط به اصول SOLID برای مصاحبههای توسعهدهندگان .NET است. هر پاسخ به صورت جامع و با مثالهای مرتبط ارائه شده است.
پاسخ: SOLID مخفف پنج اصل طراحی شیگرا است که توسط رابرت سی. مارتین (معروف به Uncle Bob) معرفی شدهاند. هدف این اصول، کمک به توسعهدهندگان برای ساخت سیستمهای نرمافزاری قابل نگهداری، انعطافپذیر و مقیاسپذیر است. این اصول عبارتند از:
Order
نباید هم مسئولیت ذخیره سفارش در پایگاه داده و هم مسئولیت ارسال ایمیل تأیید را داشته باشد. این دو مسئولیت باید به کلاسهای جداگانه (مثلاً OrderRepository
و EmailService
) واگذار شوند.if-else
یا switch
برای مدیریت انواع مختلف پرداخت، میتوانید یک اینترفیس IPaymentProcessor
تعریف کنید و برای هر نوع پرداخت (مثلاً CreditCardPaymentProcessor
, PayPalPaymentProcessor
) یک پیادهسازی جداگانه داشته باشید. با اضافه شدن نوع پرداخت جدید، فقط یک کلاس جدید اضافه میکنید و کد موجود را تغییر نمیدهید.Rectangle
و یک کلاس Square
(که از Rectangle
ارث میبرد) داشته باشیم، تغییر عرض Square
باید به طور خودکار ارتفاع آن را نیز تغییر دهد تا مربع باقی بماند. اگر این اتفاق نیفتد، Square
نمیتواند به درستی جایگزین Rectangle
شود.IWorker
با متدهای Work()
و Eat()
, Sleep()
, Manage()
, بهتر است آن را به اینترفیسهای کوچکتر مانند IWorkable
, IEatable
, ISleepable
, IManager
تقسیم کنیم. یک Robot
فقط IWorkable
را پیادهسازی میکند، در حالی که یک HumanWorker
ممکن است IWorkable
, IEatable
, ISleepable
را پیادهسازی کند.OrderProcessor
مستقیماً به یک کلاس SqlDatabase
وابسته باشد، باید به یک اینترفیس IDatabase
وابسته باشد. سپس SqlDatabase
(و هر پیادهسازی دیگر مانند MongoDbDatabase
) این اینترفیس را پیادهسازی میکند. این کار تستپذیری و انعطافپذیری را افزایش میدهد.نتیجهگیری: اعمال اصول SOLID به تولید کدی منجر میشود که خواناتر، قابل نگهداریتر، قابل تستتر و انعطافپذیرتر است و به راحتی میتواند با تغییرات آینده سازگار شود.
پاسخ: اصل مسئولیت واحد (SRP) بیان میکند که یک کلاس (یا ماژول) باید تنها یک دلیل برای تغییر داشته باشد. این به این معنی است که هر کلاس باید تنها یک وظیفه یا مسئولیت خاص را بر عهده بگیرد. این اصل به روشهای زیر به بهبود قابلیت نگهداری کد کمک میکند:/p>
ReportGenerator
که فقط مسئولیت تولید گزارش را دارد، تمام منطق مربوط به فرمتبندی، جمعآوری داده و خروجی گزارش را در خود جای میدهد. این باعث میشود درک و مدیریت آن آسانتر باشد.User
هم مسئولیت مدیریت اطلاعات کاربر و هم مسئولیت ارسال ایمیل تأیید را داشته باشد، هر تغییری در منطق ارسال ایمیل (مثلاً تغییر سرور SMTP) نیاز به تغییر در کلاس User
دارد. با جدا کردن مسئولیت ارسال ایمیل به یک کلاس EmailService
، تغییرات در EmailService
بر کلاس User
تأثیری نمیگذارد.UserRepository
که فقط مسئولیت ذخیره و بازیابی کاربران را دارد، بسیار سادهتر از تست کردن یک کلاس UserManager
است که هم مدیریت کاربران، هم ارسال ایمیل و هم لاگبرداری را انجام میدهد.مثال عملی:
فرض کنید یک کلاس Report
داریم که هم مسئولیت تولید محتوای گزارش و هم مسئولیت چاپ آن را بر عهده دارد:
// نقض SRP
public class Report
{
public string Content { get; set; }
public void GenerateReport()
{
// منطق تولید محتوا
Content = "This is the report content.";
}
public void PrintReport()
{
// منطق چاپ گزارش
Console.WriteLine("Printing report: " + Content);
}
}
برای رعایت SRP، میتوانیم این مسئولیتها را تفکیک کنیم:
// رعایت SRP
public class ReportContentGenerator
{
public string GenerateReportContent()
{
// منطق تولید محتوا
return "This is the report content.";
}
}
public class ReportPrinter
{
public void Print(string content)
{
// منطق چاپ گزارش
Console.WriteLine("Printing report: " + content);
}
}
در این مثال، ReportContentGenerator
فقط مسئولیت تولید محتوا را دارد و ReportPrinter
فقط مسئولیت چاپ را. این تفکیک باعث میشود هر کلاس متمرکزتر، قابل نگهداریتر و قابل تستتر باشد.
نتیجهگیری: SRP با ترویج کلاسهای کوچکتر، متمرکزتر و با وابستگی کمتر، به طور قابل توجهی قابلیت نگهداری کد را بهبود میبخشد و فرآیند توسعه و اشکالزدایی را سادهتر میکند.
پاسخ: اصل باز/بسته (OCP) بیان میکند که موجودیتهای نرمافزاری (کلاسها، ماژولها، توابع و غیره) باید برای توسعه (Extension) باز باشند، اما برای تغییر (Modification) بسته باشند. این بدان معناست که شما باید بتوانید قابلیتهای جدیدی را به سیستم اضافه کنید بدون اینکه نیاز به تغییر کد موجود و تست شده داشته باشید. این اصل معمولاً با استفاده از انتزاعات (مانند اینترفیسها و کلاسهای انتزاعی) و پلیمورفیسم (Polymorphism) پیادهسازی میشود.
مثال عملی: سیستم محاسبه تخفیف در یک فروشگاه آنلاین
فرض کنید یک سیستم فروشگاه آنلاین داریم که نیاز به اعمال انواع مختلف تخفیف بر روی سفارشات دارد. در ابتدا، ممکن است کد به این شکل باشد (نقض OCP):
// نقض OCP
public class DiscountCalculator
{
public decimal CalculateDiscount(decimal amount, string discountType)
{
decimal discount = 0;
if (discountType == "Percentage")
{
discount = amount * 0.10m; // 10% تخفیف
}
else if (discountType == "FixedAmount")
{
discount = 50; // 50 واحد تخفیف ثابت
}
// هر نوع تخفیف جدید نیاز به تغییر این کلاس دارد
return discount;
}
}
در این طراحی، هر بار که یک نوع تخفیف جدید (مثلاً تخفیف برای مشتریان وفادار، تخفیف فصلی) اضافه میشود، باید کلاس DiscountCalculator
را تغییر دهیم و یک if-else
یا switch
جدید اضافه کنیم. این کار نقض OCP است، زیرا کلاس برای تغییر بسته نیست.
اعمال OCP با استفاده از اینترفیسها و پلیمورفیسم:
برای رعایت OCP، میتوانیم یک اینترفیس برای استراتژیهای تخفیف تعریف کنیم و هر نوع تخفیف را به عنوان یک پیادهسازی جداگانه از این اینترفیس در نظر بگیریم:
// 1. تعریف اینترفیس برای استراتژی تخفیف
public interface IDiscountStrategy
{
decimal CalculateDiscount(decimal amount);
}
// 2. پیادهسازیهای خاص برای هر نوع تخفیف
public class PercentageDiscountStrategy : IDiscountStrategy
{
private readonly decimal _percentage;
public PercentageDiscountStrategy(decimal percentage)
{
_percentage = percentage;
}
public decimal CalculateDiscount(decimal amount)
{
return amount * _percentage;
}
}
public class FixedAmountDiscountStrategy : IDiscountStrategy
{
private readonly decimal _fixedAmount;
public FixedAmountDiscountStrategy(decimal fixedAmount)
{
_fixedAmount = fixedAmount;
}
public decimal CalculateDiscount(decimal amount)
{
return _fixedAmount;
}
}
// 3. کلاس DiscountCalculator که به اینترفیس وابسته است
public class DiscountCalculatorOCP
{
private readonly IDiscountStrategy _discountStrategy;
public DiscountCalculatorOCP(IDiscountStrategy discountStrategy)
{
_discountStrategy = discountStrategy;
}
public decimal Calculate(decimal amount)
{
return _discountStrategy.CalculateDiscount(amount);
}
}
نحوه استفاده:
// استفاده از تخفیف درصدی
var percentageDiscount = new DiscountCalculatorOCP(new PercentageDiscountStrategy(0.10m));
Console.WriteLine($"Percentage Discount: {percentageDiscount.Calculate(1000)}"); // خروجی: 100
// استفاده از تخفیف ثابت
var fixedDiscount = new DiscountCalculatorOCP(new FixedAmountDiscountStrategy(50));
Console.WriteLine($"Fixed Amount Discount: {fixedDiscount.Calculate(1000)}"); // خروجی: 50
مزایای این رویکرد (رعایت OCP):
VipDiscountStrategy
) ایجاد کنیم که IDiscountStrategy
را پیادهسازی کند. نیازی به تغییر کلاس DiscountCalculatorOCP
یا کلاسهای استراتژی موجود نیست.نتیجهگیری: با استفاده از انتزاعات و پلیمورفیسم، توانستیم کلاس DiscountCalculatorOCP
را برای توسعه باز و برای تغییر بسته نگه داریم، که این یک نمونه بارز از اعمال موفق OCP در یک پروژه است.
پاسخ: هر دو اصل LSP و ISP از اصول SOLID هستند که به طراحی شیگرا کمک میکنند، اما بر جنبههای متفاوتی از روابط بین کلاسها و اینترفیسها تمرکز دارند.
اصل جایگزینی لیسکوف (Liskov Substitution Principle - LSP):
Square
از Rectangle
است که در آن تغییر عرض Square
باید ارتفاع را نیز تغییر دهد، که این رفتار در Rectangle
وجود ندارد.اصل تفکیک اینترفیس (Interface Segregation Principle - ISP):
NotImplementedException
میشود.خلاصه تفاوتها:
ویژگی | Liskov Substitution Principle (LSP) | Interface Segregation Principle (ISP) |
---|---|---|
حوزه تمرکز | روابط ارثبری (کلاس به کلاس)/td> | روابط پیادهسازی اینترفیس (اینترفیس به کلاس) |
هدف | تضمین صحت رفتار زیرکلاسها | کاهش وابستگی کلاینتها به اینترفیسهای غیرضروری |
نتیجه | زیرکلاسها قابل جایگزینی با کلاس پایه هستند. | اینترفیسهای کوچکتر و هدفمندتر. |
مشکل حل شده | مشکلات رفتاری در سلسله مراتب ارثبری. | مشکلات اینترفیسهای حجیم و غیرضروری. |
نتیجهگیری: LSP بر حفظ رفتار صحیح در سلسله مراتب ارثبری تمرکز دارد، در حالی که ISP بر ایجاد اینترفیسهای کوچک و خاص برای جلوگیری از وابستگیهای غیرضروری کلاینتها تمرکز دارد. هر دو اصل به ایجاد کدی با کیفیت بالاتر و قابل نگهداریتر کمک میکنند.
پاسخ: اصل وارونگی وابستگی (DIP) یکی از قدرتمندترین اصول SOLID است که به طور مستقیم به کاهش وابستگی (Coupling) و افزایش انعطافپذیری (Flexibility) در طراحی نرمافزار کمک میکند. این اصل دو بخش اصلی دارد:/p>
به عبارت سادهتر، به جای اینکه کلاسهای شما مستقیماً به پیادهسازیهای خاص (جزئیات) وابسته باشند، باید به اینترفیسها یا کلاسهای انتزاعی (انتزاعات) وابسته باشند. سپس، پیادهسازیهای خاص این انتزاعات را پیادهسازی میکنند.
چگونه DIP به کاهش وابستگی کمک میکند؟
بدون DIP، معمولاً ماژولهای سطح بالا (که منطق کسبوکار اصلی را شامل میشوند) مستقیماً به ماژولهای سطح پایین (که جزئیات پیادهسازی مانند دسترسی به پایگاه داده یا سرویسهای خارجی را مدیریت میکنند) وابسته میشوند. این وابستگی مستقیم باعث میشود:
DIP این وابستگی را معکوس میکند. به جای اینکه ماژول سطح بالا به ماژول سطح پایین وابسته باشد، هر دو به یک انتزاع مشترک وابسته میشوند. این انتزاع (اینترفیس) قرارداد بین دو ماژول را تعریف میکند. ماژول سطح پایین این انتزاع را پیادهسازی میکند و ماژول سطح بالا از آن استفاده میکند.
چگونه DIP به افزایش انعطافپذیری کمک میکند؟
با وابستگی به انتزاعات به جای جزئیات، سیستم بسیار انعطافپذیرتر میشود:
SqlDatabase
به MongoDbDatabase
یا حتی یک MockDatabase
برای تست تغییر دهید.مثال عملی: سیستم لاگبرداری
بدون DIP (وابستگی مستقیم):
// نقض DIP
public class EventLogger
{
private FileLogger _fileLogger; // وابستگی مستقیم به جزئیات
public EventLogger()
{
_fileLogger = new FileLogger();
}
public void LogEvent(string message)
{
_fileLogger.Log(message);
}
}
public class FileLogger
{
public void Log(string message)
{
Console.WriteLine($"Logging to file: {message}");
}
}
در این مثال، EventLogger
(ماژول سطح بالا) مستقیماً به FileLogger
(ماژول سطح پایین) وابسته است. اگر بخواهیم لاگها را به پایگاه داده یا سرویس ابری ارسال کنیم، باید EventLogger
را تغییر دهیم.
با DIP (وابستگی به انتزاع):
// رعایت DIP
// 1. انتزاع (اینترفیس)
public interface ILogger
{
void Log(string message);
}
// 2. جزئیات (پیادهسازیها) وابسته به انتزاع
public class FileLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"Logging to file: {message}");
}
}
public class DatabaseLogger : ILogger
{
public void Log(string message)
{
Console.WriteLine($"Logging to database: {message}");
}
}
// 3. ماژول سطح بالا وابسته به انتزاع
public class EventLoggerDIP
{
private readonly ILogger _logger; // وابستگی به انتزاع
public EventLoggerDIP(ILogger logger)
{
_logger = logger;
}
public void LogEvent(string message)
{
_logger.Log(message);
}
}
نحوه استفاده (با Dependency Injection):
// استفاده از FileLogger
ILogger fileLogger = new FileLogger();
EventLoggerDIP eventLoggerFile = new EventLoggerDIP(fileLogger);
eventLoggerFile.LogEvent("User logged in.");
// استفاده از DatabaseLogger
ILogger dbLogger = new DatabaseLogger();
EventLoggerDIP eventLoggerDb = new EventLoggerDIP(dbLogger);
eventLoggerDb.LogEvent("Product updated.");
در این مثال، EventLoggerDIP
به ILogger
(انتزاع) وابسته است، نه به FileLogger
یا DatabaseLogger
(جزئیات). این باعث میشود EventLoggerDIP
کاملاً مستقل از نحوه ذخیره لاگها باشد و بتوان به راحتی پیادهسازی لاگبرداری را تغییر داد یا برای تست Mock کرد.
نتیجهگیری: DIP با وارونه کردن جهت وابستگیها از جزئیات به انتزاعات، به طور چشمگیری وابستگی بین ماژولها را کاهش میدهد و انعطافپذیری، تستپذیری و قابلیت نگهداری سیستم را افزایش میدهد. این اصل پایه و اساس الگوهایی مانند Dependency Injection و Inversion of Control است.