اصل single Responsibility در سی شارپ
Single Responsibility Principle یا به اختصار SRP بیانگر مفهوم مسئولیت واحد هست. دو مورد رو باید برای پیاده سازی این اصل رعایت کنیم :
- تنها یک دلیل برای تغییر : هر کلاس، متد و یا فانکشنی که در پروژه مینویسیم باید فقط به فقط یک دلیل برای تغییر داشته باشن. دقت کنید که گفتم هر کلاس، متد و فانکشن، پس مختص به کلاس ها نیست فقط.
- هر کلاس، متد و فانکشنی که مینویسیم فقط باید یک مسئولیت رو در بر بگیره و نه بیشتر.
مزایای Single Responsibility Principle
Single Responsibility Principle (SRP) مزایای زیادی داره که باعث میشه پیچیدگی کد ها به شدت کاهش و قابلیت نگهداری کد ها نیز افزایش پیدا کنه. برخی از مزایای SRP رو در زیر لیست کردم :
- کاهش پیچیدگی کد : یک کد بر اساس عملکردش استواره پس زمانی که هر تکه کد ما (منظور فقط کلاس نیست، میتونه فانکشن یا هر متدی باشه که ما مینویسیم) فقط یک مسئولیت داشته باشه از پیچیدگی کد به شدت کاسته میشه.
- افزایش خوانایی، توسعه پذیری و نگهداری کد : از اونجایی که هر متد یک وظیفه رو انجام میده پس خیلی راحت قابل خوانایی و نگهداریه. (یه شعار هست که میگه برنامه نویس خوب کسیه که کدش رو برای آدم های دیگه قابل فهم بنویسه نه کامپیوتر ها. یکی از اصولی که برای رسیدن به این مرحله نیاز داریم همینه. )
- قابلیت استفاده مجدد و کاهش دادن خطا ها : هر وقت که یک متد فقط یک کار واحد رو برای ما انجام میده، میتونیم از همون کد در بخش های مختلف دیگه استفاده کنیم و نیاز به نوشتن مجدد اون نداریم. (هر جا تو کد نویسی دیدین دارین یه کد رو دوبار مینویسین یه جای کار میلنگه 😊).
- تست پذیری بهتر : وقتی که عملکرد کد ما نیاز به تغییر داشته باشه، دیگه نیازی نیست کل اون بخش رو تست کنیم. البته من با این موضوع که کد قرار تغییر کنه موافق نیستم چون همین باعث نقض یک اصل دیگه از SOLID میشه که توضیح میدم . اما در کل وقتی هر متد یه کار برامون انجام میده ما دیگه نیاز به نوشتن تست های پیچیده نیستیم.
- کاهش وابستگی : یک متد یا یک کلاس دیگه وابسته به متد یا کلاس های دیگه نیست. به عبارتی کاهش یا حذف وابستگی ها
پیاده سازی اصل Single Responsibility در سی شارپ
برای این که یه مثال خوب بزنم قصد دارم در قالب یه داستان براتون بیانش کنم. تو این داستان یه برنامه نویس دات نت داریم به اسم پوریا (خودمو میگم. البته من از این اشتباها نمیکنم 😊) و یه مدیر فنی به اسم محمد. پس محمد مدیر فنیه و پوریا هم برنامه نویس داستان. داستان از اونجایی شروع میشه که محمد از پوریا میخواد تو پروژه سیستم مدیریت کارمندان، نقش یه معمار رو بازی کنه. با هم دیگه میبینیم که پوریا چطوری از دانشش برای پیاده سازی SRP استفاده میکنه تا در نهایت به یک راه حل جامع و اصولی برسه.
محمد نیازمندی های ماژول جدیدی روکه میخواد به پروژه اضافه بشه رو به پوریا میگه. اون یه ماژول ثبت نام کارمندان رو میخواد. اما پوریا چطوری شروع میکنه ؟ پوریا اول یه کلاس جدید یا یه سرویس جدید به پروژه اضافه می کنه به اسم EmployeeService که هم یه سری داده ها برای کارمندان رو نگه داری میکنه هم یه متد داره برای ثبت نام. (این چه حرکتیه زدی آخه پوریا آبرومونو بردی 😊). به قطعه کد زیر نگاه کنین.
namespace SRPApp
{
public class EmployeeService
{
public string FirstName { get; set; }
public string LastName { get; set; }
public void EmployeeRegistration(EmployeeService employee)
{
StaticData.Employees.Add(employee);
}
}
}
using System.Collections.Generic;
namespace SRPApp
{
public class StaticData
{
public static List<EmployeeService> Employees { get; set; } = new List<EmployeeService>();
}
}
پوریا ازLocalStorage ها برای ذخیره داده ها داره استفاده میکنه بجای دیتابیس. (داخل کلاس StaticData یه پراپرتی داره که این کارو انجام میده).پوریا برای این که برنامه رو ساده تر کنه و مارو هم خسته نکنه از Console Application برای UI پروژه استفاده کرده (مثاله دیگه سخت نگیریم به پوریا)
using System;
namespace SRPApp
{
class Program
{
static void Main(string[] args)
{
EmployeeService employeeService = new EmployeeService
{
FirstName = "John",
LastName = "Deo"
};
employeeService.EmployeeRegistration(employeeService);
Console.ReadKey();
}
}
}
از اونجا که ماژول آماده شده، پوریا همینو به محمد نشون میده و محمد هم اتفاقا کلی ازش تشکر میکنه. همه چیز همونطور که انتظار میره انجام شده. مثلا : پوریا اومده به جای اینکه همه این کارها رو تو بدنه اصلی برنامه انجام بده خب اومده یه سرویس به اسم EmployeeService درست کرده و این عملیات رو اون جا انجام داده. عملیات ثبت نام هم از طریق متدی که داخل سرویس نوشته انجام میشه و نتیجه رو از طریق یه آبجکت نشون میده. اما این پایان ماجرا نبود. محمد از پوریا یه سری فیچر های دیگه هم خواست. محمد گفت ما نیاز داریم که ایمیل کارمندان رو هم نگه داری کنیم و همچنین وقتی ثبت نام کردن، یک ایمیل هم براشون ارسال بشه.
مشکلی که در این نوع مرحله ظاهر شد : الان دو تا نیازمندی وجود داره. هم باید یه فیلد دیگه برای نگهداری ایمیل اضافه بشه هم این که یه متد دیگه برای ارسال ایمیل. پس اینجا اصلSRP داره نقض مییشه. یعنی دو تا دلیل برای تغییر وجود داره. خب پوریا ی داستان ما متوجه این قضیه شد و اون رو به روش زیر اصلاح کرد. در ابتدا تصمیم گرفت برای نگهداری داده های کارمندان یک کلاس جداگانه داشته باشه و برای پیاده سازی عملیاتی مثل ثبت نام و ارسال ایمیل هم یک کلاس جدا پس :
namespace SRPApp
{
public class Employee
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
}
using System.Collections.Generic;
namespace SRPApp
{
public class StaticData
{
public static List<Employee> Employees { get; set; } = new List<Employee>();
}
}
using MailKit.Net.Smtp;
using MailKit.Security;
using MimeKit;
using System.Threading.Tasks;
namespace SRPApp
{
public class EmployeeService
{
public async Task EmployeeRegistration(Employee employee)
{
StaticData.Employees.Add(employee);
await SendEmailAsync(employee.Email, "Registration", "Congratulation ! Your are successfully registered.");
}
private async Task SendEmailAsync(string email, string subject, string message)
{
var emailMessage = new MimeMessage();
emailMessage.From.Add(new MailboxAddress("Mark Adam", "madam@sample.com"));
emailMessage.To.Add(new MailboxAddress(string.Empty, email));
emailMessage.Subject = subject;
emailMessage.Body = new TextPart("plain") { Text = message };
using (SmtpClient smtpClient = new SmtpClient())
{
smtpClient.LocalDomain = "sample.com";
await smtpClient.ConnectAsync("smtp.relay.uri", 25, SecureSocketOptions.None).ConfigureAwait(false);
await smtpClient.SendAsync(emailMessage).ConfigureAwait(false);
await smtpClient.DisconnectAsync(true).ConfigureAwait(false);
}
}
}
}
دستورایی که بالا میبینین در بستر.NetCore نوشته شده و شما میتونین با استفاده از پکیج MailKit برای ارسال ایمیل استفاده کنین ولی باید کریدنشیال هاشو درست بدین چون تو این مثال به صورت تستی مقدار دهی شدن.
بدنه برنامه هم به شکل زیر تغییر کرده :
using System;
namespace SRPApp
{
class Program
{
static void Main(string[] args)
{
Employee employee = new Employee
{
FirstName = "John",
LastName = "Deo",
Email = "jdeo@sample.com"
};
EmployeeService employeeService = new EmployeeService();
employeeService.EmployeeRegistration(employee).Wait();
Console.ReadKey();
}
}
}
خب تا اینجا مواردی رو که محمد درخواست کرده بود هم به پروژه اضافه شد. اما آیا درست بود ؟ آیا با جدا کردن داده ها و عملیات مربوط به مدل کارمندان اصل SRP رعایت شده بود؟ نه.
در حال حاضر ما دو تا متد در کلاس EmployeeService داریم که دو تا کار متفاوت و غیر مرتبط رو انجام میدن. یکی ثبت نام و اون یکی ارسال ایمیل به کارمندان. شاید با خودتون بگین که ارسال ایمیل برای کارمندانه دیگه. چرا بی ربط باشه، اما این نکته رو در نظر بگیرین ارسال ایمیل به خودی خود میتونه یه سرویس جدا باشه که بارها و بارها در برنامه ما قراره استفاده بشه. نمیخوام بگم که در یک سرویس فقط باید یک متد وجود داشته باشه اما هر چند تا متدی که میخواین در یک سرویس قرار بدین مطمئن بشین که روی یک موجودیت تمرکز دارن. و اینجا ما اصل SRP رو نقض کردیم چون عملا کلاس ما داره بیشتر از یک کار رو انجام میده.
پوریا متوجه این قضیه شد و سعی در اصلاح مجدد کد ها گرفت. نتیجه به شکل زیر شد که در نهایت اصل SRP رو برقرار کرد. یک کلاس جدا برای ارسال ایمیل تحت عنوان EmailService که فقط برای ارسال ایمیل به کارمندان بعد از ثبت نامه و کلاس EmployeeService هم که فقط برای ثبت نام کارمندان استفاده میشه و همینطور کلاس Employee که برای نگهداری داده های کارمندان ایجاد شده. در این حالت پوریا تونسته SRP رو پیاده سازی کنه. هر کلاس تنها یک مسئولیت داره و همینطور هر کلاس یک دلیل برای تغییر.
using MailKit.Net.Smtp;
using MailKit.Security;
using MimeKit;
using System.Threading.Tasks;
namespace SRPApp
{
public class EmailService
{
public async Task SendEmailAsync(string email, string subject, string message)
{
var emailMessage = new MimeMessage();
emailMessage.From.Add(new MailboxAddress("Mark Adam", "madam@sample.com"));
emailMessage.To.Add(new MailboxAddress(string.Empty, email));
emailMessage.Subject = subject;
emailMessage.Body = new TextPart("plain") { Text = message };
using (SmtpClient smtpClient = new SmtpClient())
{
smtpClient.LocalDomain = "paathshaala.com";
await smtpClient.ConnectAsync("smtp.relay.uri", 25, SecureSocketOptions.None).ConfigureAwait(false);
await smtpClient.SendAsync(emailMessage).ConfigureAwait(false);
await smtpClient.DisconnectAsync(true).ConfigureAwait(false);
}
}
}
}
using System.Threading.Tasks;
namespace SRPApp
{
public class EmployeeService
{
public async Task EmployeeRegistration(Employee employee)
{
StaticData.Employees.Add(employee);
EmailService emailService = new EmailService();
await emailService.SendEmailAsync(employee.Email, "Registration", "Congratulation ! Your are successfully registered.");
}
}
}
داستان ما اینجا به اتمام رسید و دیدین که پوریای داستان ما چطوری اصل Single Responsibility رو در پروژه پیاده سازی کرد.
نتیجه گیری
اصل single Responsiblity میگه اگه ما دو دلیل برای تغییر یک کلاس داشته باشیم باید اونو بشکنیم به دو تا کلاس مجزا. هر کلاس فقط باید برای یک مسئولیت پیاده سازی بشه و اگر در آینده قراره تغییر کنه باید اون رو در یک کلاس جدید پیاده سازی کنیم.اگه هر کلاس یا متد یا فانکشن برای بیشتر از پاسخ دادن به یک مسئولیت ایجاد شده باشن، در حقیقت دارن بهمون میگن که یک وابستگی بین اجزا ایجاد شده، بنابراین اگه نیاز به تغییر در یک بخش از کد باشه امکان این که دیگر بخش ها تحت تاثیر قرار بگیرن زیاده که در نهایت نیاز به نوشتن تست های متعدد و پیچیده برای جلوگیری از بروز خطا ها میشه. (البته قطعا کسی که تست مینویسه برای متد ها و کلاس هاش اصولSOLID رو باید بدونه 😊)