توضیح دوات: برای راهنمایی و اطلاعات بیشتر در مورد نمودار کلاس به این مقاله مراجعه کنید.
الگوی طراحی Chain of Responsibility یک الگوی رفتاری است که به گروهی از آیتمها اجازه میدهد درخواست را به ترتیب خاصی پردازش کنند. در این الگو، زنجیرهای از اشیاء ایجاد میکنیم که هر یک میتواند درخواست را خود پردازش کند یا آن را به شیء بعدی در زنجیره ارسال کند.
ایده اصلی پشت این الگو جدا کردن شیء ارسالکننده درخواست از شیء دریافتکننده آن است. این جداسازی به ما امکان میدهد که بیش از یک شیء درخواست را پردازش کند. به این ترتیب، کلاینت نیازی ندارد که از شیء پردازشکننده درخواست مطلع باشد و میتوان زنجیره را بدون ایجاد اختلال در جریان کنترل گسترش داد یا تغییر داد.
شما میتوانید نمونه کد این پست را در GitHub مشاهده کنید.
مفهومسازی مسئله
فرض کنید در حال توسعه یک سیستم شرطبندی آنلاین هستیم. میخواهیم دسترسی به سیستم را محدود کنیم تا فقط کاربران احراز هویت شده بتوانند شرطبندی کنند. علاوه بر این، کاربران دارای دسترسی مدیریتی باید به تمام شرطهای ثبت شده در سیستم دسترسی کامل داشته باشند.
پس از برنامهریزی، مشخص میشود که این بررسیها باید یکی پس از دیگری انجام شوند. زمانی که درخواستی با اعتبارنامه کاربر دریافت میشود، سیستم باید تلاش کند کاربر را احراز هویت کند. اما اگر احراز هویت به دلیل اشتباه بودن اعتبارنامه شکست بخورد، دیگر نیازی به بررسی موارد دیگر نیست.
با ادامه کدنویسی، موارد دیگری برای بررسیهای ترتیبی مشخص شدند. ابتدا، نگرانیهایی درباره ارسال دادههای خام به سیستم شرطبندی وجود داشت، بنابراین دادههای درخواستی باید از مرحله اضافی پاکسازی عبور میکردند. سپس متوجه شدیم که سیستم به حملات brute-force رمز عبور آسیبپذیر است. برای جلوگیری از دسترسی غیرمجاز، بررسیای اضافه کردیم که درخواستهای تکراری ناموفق از یک آدرس IP را فیلتر کند.
در نهایت، دولت قانونی تصویب کرد که محدودیتهایی بر مقدار شرطبندی روزانه و زمان استفاده کاربران از پلتفرم اعمال میکرد. به همین دلیل، بررسیهای بیشتری برای رعایت این قوانین اضافه کردیم.
با اضافه شدن ویژگیهای جدید، کد بررسیهای ترتیبی پیچیدهتر شد. کد اصلی که از ابتدا نمونه خوبی از یک ماژول تمیز و قابل نگهداری نبود، با افزودن ویژگیهای جدید، حجیمتر شد و در نهایت به مرحلهای رسید که تغییر یک بررسی، اثرات پیشبینینشدهای بر بخشهای غیرمرتبط سیستم داشت. حتی بدتر، زمانی که همکار دیگری تلاش کرد بررسی را برای محافظت از بخش دیگری استفاده کند، مجبور شد کل آن را کپی کند تا کار کند.
در این مرحله، وضعیت به دور از ایدهآل بود و نگهداری سیستم زمان، هزینه و تلاش زیادی میبرد. هر تغییری در روند، پیامدهای منفی به همراه داشت. پس از مدتی تصمیم گرفتیم روش کار سیستم را به کلی تغییر دهیم.
الگوی Chain of Responsibility، مانند بسیاری از الگوهای رفتاری دیگر، بر اساس ایده تبدیل اقدامات جداگانه به اشیائی مستقل به نام handlerها طراحی شده است. در این مورد، هر بررسی باید در کلاس خود انجام شود و یک متد واحد آن را انجام دهد. جزئیات درخواست به عنوان پارامتر به این متد ارسال میشود.
برای عملکرد این سیستم، handlerها به صورت زنجیرهای به هم متصل میشوند. هر handler مرجعی به handler بعدی در زنجیره ذخیره میکند. handlerها درخواست را پردازش کرده و آن را به زنجیره ارسال میکنند تا همه بتوانند روی آن کار کنند.
یکی از مزایای اصلی این طراحی این است که handler میتواند تصمیم بگیرد که درخواست را به زنجیره پایینتر ارسال نکند، که از پردازش بیشتر جلوگیری میکند. این امر امکان ایجاد سیستمهای انعطافپذیر و ماژولار را فراهم میکند.
در مثال سیستم شرطبندی ما، یک handler پردازش لازم را انجام میدهد و سپس تصمیم میگیرد آیا درخواست را به زنجیره پایینتر ارسال کند یا نه.
راههای مختلفی برای استفاده از الگوی طراحی Chain of Responsibility وجود دارد. در برخی موارد، handler به محض دریافت درخواست تصمیم میگیرد که آیا آن را پردازش کند یا خیر. در صورت امکان، درخواست را پردازش کرده و آن را به entity بعدی نمیفرستد. این روش اغلب برای مدیریت رویدادها در رابطهای کاربری گرافیکی استفاده میشود.
به عنوان مثال، وقتی کاربر دکمهای را فشار میدهد، رویداد از طریق زنجیرهای از عناصر رابط کاربری، از دکمه شروع شده و به فرمها یا پنلها و سپس به پنجره اصلی برنامه میرسد. رویداد توسط اولین عنصری که میتواند آن را مدیریت کند، رسیدگی میشود.
این روش زمانی مفید است که خط وظایف مشخص باشد و ترتیب کارها واضح باشد.
همه کلاسهای handler باید یک واسط مشترک را پیادهسازی کنند. هر handler مشخص تنها باید به اجرای متد handler بعدی توجه کند. این طراحی به ما امکان میدهد زنجیرهها را در زمان اجرا ایجاد کرده و بدون وابستگی به کلاسهای خاص کدنویسی کنیم.
ساختار الگوی طراحی Chain of Responsibility
در پیادهسازی ساده، الگوی Chain of Responsibility شامل ۴ جزء اصلی است:
- Handler: واسطی را تعریف میکند که توسط تمام handlerهای مشخص به اشتراک گذاشته میشود. معمولاً تنها یک متد برای مدیریت درخواستها دارد اما ممکن است متدی برای تعیین handler بعدی نیز داشته باشد.
- Basic Handler: یک کلاس پایه اختیاری که کد مشترک بین تمام handlerها را شامل میشود.
- Concrete Handlers: حاوی کد واقعی برای پردازش درخواستها هستند.
- Client: مسئول ایجاد زنجیرهها، به صورت ایستا یا پویا، بسته به منطق برنامه.
برای نشان دادن الگوی طراحی Chain of Responsibility، سیستمی مشابه توصیف بالا ایجاد خواهیم کرد. در این مثال، با درخواستهایی حاوی اطلاعات کاربر سروکار خواهیم داشت و handlerها وظایف مختلفی مانند احراز هویت، مجوزدهی، اعتبارسنجی و پاکسازی را انجام میدهند.
ابتدا، شرکتکننده IHandler را ایجاد میکنیم:
public abstract class BaseMiddleware
{
private BaseMiddleware? _nextHandler = null;
public static BaseMiddleware Link(BaseMiddleware head, params BaseMiddleware[] chain)
{
var internalHead = head;
foreach(var nextLink in chain)
{
internalHead._nextHandler = nextLink;
internalHead = nextLink;
}
return head;
}
public abstract bool Check(string email, string password);
///
/// Runs the check of the next object in the chain or ends traversal if this is the last object.
///
/// The email to check
/// The password to check
/// The result of the next check in the chain, or true if this is the last link.
protected bool CheckNext(string email, string password)
{
return _nextHandler == null || _nextHandler.Check(email, password);
}
}
کلاس BaseMiddleware
شامل تمامی کدهای اولیه برای ایجاد زنجیرهای از کامپوننتهای میانافزار است. این کلاس متد Link
را اعلام میکند که به ما اجازه میدهد با اتصال یک هندلر به لیستی از هندلرهای دیگر، زنجیره مسئولیت را ایجاد کنیم. همچنین متد CheckNext
را اعلام میکند که امکان پیمایش زنجیره و فراخوانی متد Check
هندلرها را فراهم میکند.
در نهایت، این کلاس یک متد انتزاعی به نام Check
اعلام میکند که باید توسط تمامی زیرکلاسها پیادهسازی شود. این متد شامل منطق کسبوکار برای هر هندلر مجزا خواهد بود.
گام بعدی پیادهسازی ConcreteHandlers است. ابتدا، ما کلاس LoginThrottlingMiddleware
را پیادهسازی خواهیم کرد:
public class LoginThrottlingMiddleware : BaseMiddleware
{
private readonly int _requestsPerMinute;
private int _requestCount;
private long _currentTime;
public LoginThrottlingMiddleware(int requestsPerMinute)
{
_requestsPerMinute = requestsPerMinute;
_currentTime = DateTimeOffset.Now.ToUnixTimeMilliseconds();
}
public override bool Check(string email, string password)
{
if (DateTimeOffset.Now.ToUnixTimeMilliseconds() > _currentTime + 60000)
{
_requestCount = 0;
_currentTime = DateTimeOffset.Now.ToUnixTimeMilliseconds();
}
_requestCount++;
if (_requestCount <= _requestsPerMinute)
return CheckNext(email, password);
Console.WriteLine("Request limit exceeded. Please try again later.");
return false;
}
}
این متد تعداد درخواستهای ورود به سیستم را که یک کاربر میتواند در هر دقیقه ارسال کند محدود میکند. متد Check
ابتدا بررسی میکند که آیا محدودیت زمانی گذشته است یا خیر و سپس مقدار _requestCount
را بازنشانی میکند. سپس مقدار _requestCount
یک واحد افزایش مییابد. اگر _requestCount
از حد مجاز تجاوز نکند، هندلر بعدی در زنجیره را فراخوانی میکند. اما اگر واقعاً از حد مجاز فراتر رود، فرآیند متوقف میشود.
حالا نوبت به کلاس AuthenticationMiddleware
میرسد:
public class AuthenticationMiddleware : BaseMiddleware
{
private readonly Server _server;
public AuthenticationMiddleware(Server server)
{
_server = server;
}
public override bool Check(string email, string password)
{
if (!_server.EmailExists(email) || _server.PassWordIsValid(email, password))
{
Console.WriteLine("Invalid username or password");
return false;
}
return CheckNext(email, password);
}
}
کلاس AuthenticationMiddleware
پیادهسازی خاص خود را برای متد Check ارائه میدهد. این متد با سرور ارتباط برقرار میکند و اگر ایمیل وجود نداشته باشد یا رمز عبور با رمز ثبتشده تحت آن ایمیل مطابقت نداشته باشد، زنجیره را متوقف میکند. اگر همه چیز بهخوبی پیش برود، هندلر بعدی در زنجیره را فراخوانی میکند.
در نهایت، به کلاس AuthorizationMiddleware
میرسیم:
public class AuthorizationMiddleware : BaseMiddleware
{
public override bool Check(string email, string password)
{
if (email.Contains("admin"))
{
Console.WriteLine("Hello, admin!");
return CheckNext(email, password);
}
Console.WriteLine("Hello, user!");
return CheckNext(email, password);
}
}
این متد سادهترین و احتمالاً امنترین متدی است که تاکنون نوشتهام. اگر ایمیل شامل کلمه admin
باشد، کاربر طبیعتاً مدیر است! در هر صورت، سپس متد، هندلر بعدی در زنجیره را فراخوانی میکند تا فرآیند ادامه یابد.
حالا، بیایید به کلاس Server
بپردازیم:
public class Server
{
private Dictionary _users = new Dictionary();
private BaseMiddleware _middleware;
public void SetMiddleware(BaseMiddleware middleware)
{
_middleware = middleware;
}
public bool Login(string email, string password)
{
if (_middleware.Check(email, password))
{
Console.WriteLine("Authorization successful");
return true;
}
return false;
}
public void Register(string email, string password)
{
_users.Add(email, CalculateHash(password));
}
public bool EmailExists(string email)
{
return _users.ContainsKey(email);
}
public bool PassWordIsValid(string email, string password)
{
var provided = CalculateHash(password);
return _users[email].Equals(provided);
}
private string CalculateHash(string value)
{
byte[] inputBytes = Encoding.UTF8.GetBytes(value);
// Create an instance of the SHA-512 algorithm
SHA512 sha512 = SHA512.Create();
// Compute the has value of the input bytes
byte[] hashBytes = sha512.ComputeHash(inputBytes);
// Convert the hash bytes to a hex string
StringBuilder sb = new StringBuilder();
for (var i = 0; i < hashBytes.Length; i++)
sb.Append(hashBytes[i].ToString("x2"));
return sb.ToString();
}
}
کلاس Server
نماینده سیستم بکاند است. این کلاس متدهایی برای ثبتنام کاربران و بررسی اعتبار آنها ارائه میدهد. همچنین من یک متد ایجاد کردم که هش SHA-512
رمز عبور را محاسبه میکند و از آن به عنوان مقدار رمز عبور استفاده میکند تا شبیه به نحوه مدیریت رمز عبور در یک سیستم واقعی عمل کند.
در نهایت، بیایید کلاس Client، یعنی کلاس Program
را ایجاد کنیم:
class Program
{
private static Server server;
private static void Init()
{
server = new Server();
server.Register("admin@gmail.com", "this_is_super_secure_admin_password");
server.Register("user@gmail.com", "password123");
BaseMiddleware middleware = BaseMiddleware.Link(
new LoginThrottlingMiddleware(3),
new AuthenticationMiddleware(server),
new AuthorizationMiddleware());
server.SetMiddleware(middleware);
}
public static void Main(string[] args)
{
Init();
bool success;
do
{
Console.Write("Enter email: ");
string email = Console.ReadLine();
Console.Write("Input password: ");
string password = Console.ReadLine();
success = server.Login(email, password);
} while (!success);
}
حالا بیایید برنامه خود را اجرا کنیم. در اجرای زیر، فردی تلاش کرده به عنوان یک مدیر وارد سیستم شود. پس از سه تلاش ناموفق، کاربر محدود شده و درخواستهای بیشتری برای یک بازه زمانی مشخص پردازش نمیشود:
مزایا و معایب الگوی طراحی Chain of Responsibility
مزایا
- ✔ ما میتوانیم ترتیب پردازش درخواستها را کنترل کنیم.
- ما میتوانیم کلاسهایی که عملیاتها را فراخوانی میکنند از کلاسهایی که عملیات را انجام میدهند جدا کنیم و به این ترتیب از اصل مسئولیت یگانه پیروی کنیم.
- ما میتوانیم handlerهای جدیدی را به برنامه اضافه کنیم بدون اینکه کدهای کلاینت موجود را خراب کنیم، و به این ترتیب از اصل باز/بسته پیروی کنیم.
معایب
برخی درخواستها ممکن است بدون پردازش باقی بمانند.
روابط با سایر الگوها
الگوهای طراحی Chain of Responsibility، Mediator و Observer روشهای متفاوتی برای اتصال کلاینتهایی که درخواستها را ارسال میکنند به نقاط انتهایی که آنها را دریافت میکنند ارائه میدهند:
- Chain of Responsibility: درخواست را به ترتیب به لیستی از کاربران احتمالی ارسال میکند تا زمانی که یکی از آنها آن را مدیریت کند.
- Chain of Responsibility: ارتباط بین ارسالکننده و گیرنده را فقط به یک جهت محدود میکند.
- Mediator: ارتباط مستقیم بین ارسالکننده و گیرنده را قطع کرده و آنها را وادار میکند از طریق یک شیء میانجی صحبت کنند.
- Observer: به گیرندگان اجازه میدهد که به صورت لحظهای برای دریافت یا عدم دریافت درخواست ثبتنام کنند.
الگوهای Chain of Responsibility و Composite اغلب با هم استفاده میشوند. در این حالت، زمانی که یک مؤلفه برگ درخواست دریافت میکند، ممکن است آن را به زنجیرهای از مؤلفههای والد ارسال کند تا به انتهای درخت اشیاء برسد.
Handlerها میتوانند به عنوان Chain of Responsibility در زنجیره استفاده شوند. در این حالت، میتوانید از همان شیء context، که به صورت یک درخواست نشان داده میشود، برای انجام کارهای مختلف استفاده کنید. اما روش دیگری نیز وجود دارد که در آن درخواست خود یک شیء Command است. در این حالت، میتوانید همین کار را در چندین موقعیت مختلف مرتبط انجام دهید.
الگوهای Chain of Responsibility و Decorator طراحیهای کلاسی بسیار مشابهی دارند. هر دو الگو از ترکیب بازگشتی برای عبور اجرای عملیات از چندین شیء استفاده میکنند. اما چند تفاوت مهم وجود دارد. handlerهای Chain of Responsibility میتوانند کارهای مختلفی انجام دهند بدون اینکه روی یکدیگر تأثیر بگذارند و میتوانند ارسال درخواست به مرحله بعدی را در هر زمان متوقف کنند. از طرف دیگر، Decoratorهای مختلف میتوانند رفتار یک شیء را بدون تغییر رابط اصلی آن افزایش دهند. همچنین، Decoratorها نمیتوانند عبور درخواست را از مراحل معمول آن متوقف کنند.
نتیجهگیری
در این مقاله، الگوی طراحی Chain of Responsibility، زمان استفاده از آن و مزایا و معایب آن را مورد بررسی قرار دادیم. سپس برخی موارد استفاده از این الگو و ارتباط آن با سایر الگوهای طراحی کلاسیک را بررسی کردیم.
شایان ذکر است که الگوی Chain of Responsibility، به همراه سایر الگوهای طراحی ارائه شده توسط Gang of Four، یک راهحل همهجانبه یا نهایی برای طراحی یک برنامه نیست. انتخاب اینکه چه زمانی از یک الگوی خاص استفاده شود، بر عهده مهندسین است. در نهایت، این الگوها زمانی مفید هستند که به عنوان یک ابزار دقیق استفاده شوند، نه به عنوان یک پتک بزرگ.