توضیح دوات: برای راهنمایی و اطلاعات بیشتر در مورد نمودار کلاس به این مقاله مراجعه کنید.
الگوی طراحی Template Method یک الگوی رفتاری در طراحی نرمافزار است که در آن یک کلاس والد ساختار کلی یک الگوریتم را تعریف میکند، اما به زیرکلاسها اجازه میدهد تا مراحل خاصی از الگوریتم را بازنویسی کنند. این الگو برای ایجاد الگوریتمهای قابل تنظیم به کار میرود و مانع از ایجاد تغییرات ناسازگار توسط زیرکلاسها در الگوریتم میشود.
در این الگو، یک کلاس پایه مراحل اصلی الگوریتم و ترتیب اجرای آنها را تعریف میکند. این مراحل بهصورت متدهای انتزاعی تعریف میشوند که باید توسط زیرکلاسهای مشخص پیادهسازی شوند. الگوریتم بهگونهای طراحی شده است که قابل گسترش باشد، بنابراین زیرکلاسها میتوانند قسمتهای خاصی از فرآیند را برای تنظیم رفتار آن بازنویسی کنند.
الگوی Template Method زمانی مفید است که کلاسهای متعددی دارید که الگوریتمهای مشابهی را با تفاوتهای جزئی در جزئیات پیادهسازی میکنند. بهجای تکرار کد برای هر الگوریتم، این الگو به شما اجازه میدهد کد مشترک را در یک کلاس پایه قرار دهید و نگهداری و بهروزرسانی آن را آسانتر کنید.
میتوانید نمونه کد این پست را در GitHub پیدا کنید.
درک مسئله
فرض کنید در حال ساخت یک اپلیکیشن تحلیل داده هستیم که دادههای تحلیلی مختلف را بررسی میکند. کاربران میتوانند نقاط پایانی (endpoints) مختلف از تولیدکنندگان تحلیل را به برنامه بدهند، و برنامه سعی میکند دادههای معنادار را از پاسخهای سیستمها در یک قالب یکسان استخراج کند.
نسخه اول برنامه فقط میتوانست با Google Analytics کار کند. در نسخه بعدی، از Hotjar Analytics نیز پشتیبانی شد. یک ماه بعد، به آن یاد دادیم که دادهها را از Sitecore Analytics نیز دریافت کند.
در مقطعی متوجه شدیم که کدهای زیادی در هر سه کلاس یکسان هستند. در هر کلاس، کد مربوط به کار با فرمتهای مختلف داده کاملاً متفاوت بود، اما کد مربوط به مدیریت و تحلیل داده تقریباً یکسان بود. آیا بهتر نبود کدهای تکراری را بدون تغییر در نحوه کار متد حذف کنیم؟
کدهای مشتری که از این کلاسها استفاده میکرد نیز منبع مشکل بود. شرطهای زیادی داشت که انتخاب عمل مناسب را بر اساس کلاس شیء پردازششده مشخص میکرد. اگر هر سه کلاس پردازشی یک رابط یا کلاس پایه مشترک داشتند، میتوانستیم شرطها را در کد مشتری حذف کنیم و از پلیمورفیسم هنگام فراخوانی توابع یک شیء پردازشی استفاده کنیم.
الگوی طراحی Template Method پیشنهاد میدهد که الگوریتم را به یک سری مراحل تقسیم کنیم، این مراحل را به متدها تبدیل کنیم، و مجموعهای از فراخوانیها به این متدها را در یک متد قالب قرار دهیم. مراحل میتوانند abstract
باشند یا دارای یک پیادهسازی پیشفرض باشند. برای استفاده از الگوریتم، مشتری باید زیرکلاس خود را ارائه دهد، همه مراحل انتزاعی را پیادهسازی کند و در صورت لزوم برخی از مراحل اختیاری را تغییر دهد (اما خود متد قالب را نمیتواند تغییر دهد).
بیایید ببینیم این کار چگونه در اپلیکیشن تحلیل داده ما عمل میکند. هر سه متد تجزیه را میتوان بر اساس یک کلاس پایه مشترک ساخت. این کلاس یک متد قالب تنظیم میکند که شامل چندین فراخوانی به مراحل مختلف در فرآیند پردازش سند است.
ابتدا میتوانیم تمام مراحل را abstract
کنیم، به این معنا که زیرکلاسها باید کد خاص خود را برای این متدها ایجاد کنند. در مورد ما، زیرکلاسها قبلاً همه پیادهسازیهای مناسب را دارند، بنابراین تنها کاری که ممکن است لازم باشد انجام دهیم این است که امضاهای متدها را تغییر دهیم تا با امضاهای متدهای کلاس والد مطابقت داشته باشند.
حالا ببینیم برای حذف کد تکراری چه میتوان کرد. به نظر میرسد کد مربوط به باز کردن و بستن اتصالات و استخراج و تجزیه دادهها برای هر ارائهدهنده متفاوت است، بنابراین دلیلی برای تغییر این متدها وجود ندارد. اما کد سایر مراحل، مانند پردازش داده خام و نوشتن گزارش، بسیار شبیه به هم است و میتوان آن را به کلاس پایه منتقل کرد تا توسط زیرکلاسها به اشتراک گذاشته شود.
سه نوع مرحله وجود دارد:
- مراحل انتزاعی: مراحلی که باید توسط هر زیرکلاس پیادهسازی شوند، مانند باز کردن و بستن اتصالات با یک ارائهدهنده داده.
- مراحل اختیاری: مراحلی که دارای یک پیادهسازی پیشفرض هستند، اما در صورت نیاز میتوانند بازنویسی شوند. این مرحله میتواند شامل پردازش داده خام یا نوشتن گزارش باشد.
- قلابها (Hooks): مراحل اختیاری با یک بدنه خالی. یک متد قالب حتی اگر یک قلاب تغییر نکند نیز کار خواهد کرد. بیشتر اوقات، قلابها قبل و بعد از مراحل مهم در الگوریتم قرار میگیرند. این به زیرکلاسها مکانهای بیشتری برای افزودن به یک الگوریتم میدهد.
ساختار الگوی طراحی Template Method
در پیادهسازی پایه، Template Method دارای دو شرکتکننده است:
- کلاس انتزاعی: این کلاس متدهایی را تعریف میکند که بهعنوان مراحل یک الگوریتم عمل میکنند، همچنین متد قالبی که این مراحل را به ترتیب خاصی فراخوانی میکند. مراحل یا میتوانند انتزاعی باشند یا دارای یک پیادهسازی پیشفرض.
- کلاسهای مشخص: تمام مراحل توسط کلاسهای مشخص قابل تغییر هستند، اما خود متد قالب تغییر نمیکند.
برای نشان دادن نحوه کار الگوی Template Method، قصد داریم یک جمعآوریکننده کوچک برای رسانههای اجتماعی ایجاد کنیم. در این مثال، متد قالب ما یک الگوریتم برای ارتباط با یک شبکه اجتماعی تعریف خواهد کرد. زیرکلاسهایی که با شبکه اجتماعی خاصی مطابقت دارند، این مراحل را مطابق با API خاص آن شبکه پیادهسازی خواهند کرد.
ابتدا، شرکتکننده Template Method خود را تعریف میکنیم، یعنی کلاس Network.
namespace TemplateMethod.SocialNetworks;
public abstract class SocialNetwork
{
protected string username;
protected string password;
protected SocialNetwork()
{
}
///
/// Publish a post to a social network.
///
/// The message to post.
/// true if the message is successfully posted, false otherwise.
public bool Post(string message)
{
if (!LogIn(username, password))
return false;
var result = SendData(Encoding.ASCII.GetBytes(message));
LogOut();
return result;
}
protected abstract bool LogIn(string username, string password);
protected abstract bool SendData(byte[] data);
protected abstract void LogOut();
}
ابتدا باید یک کلاس انتزاعی به نام SocialNetwork
تعریف کنیم که چارچوب لازم برای ارسال پست به شبکههای اجتماعی را فراهم میکند. همچنین دو عضو محافظتشده به نامهای username
و password
تعریف میکنیم که برای فرآیند ورود به سیستم استفاده میشوند.
متد Post
بهصورت عمومی تعریف شده و یک پارامتر رشتهای به نام message
را بهعنوان ورودی دریافت میکند. این متد در کلاس Template Method پیادهسازی میشود، زیرا مکانیسم ارسال پست در تمام شبکههای اجتماعی یکسان است. این متد یک فرآیند ورود به سیستم را اجرا میکند و در صورت موفقیت، دادهها را به شبکه اجتماعی ارسال کرده و سپس خارج میشود. توجه داشته باشید که در حالی که کلاس متد Post
را پیادهسازی میکند، جزئیات مربوط به ورود، ارسال داده و خروج به کلاسهای مشخص واگذار شده است.
متدهای LogIn
، SendData
و Logout
همگی بهصورت انتزاعی تعریف شدهاند، به این معنی که در کلاس SocialNetwork
پیادهسازی نمیشوند و باید توسط کلاسهای مشتقشده از آن پیادهسازی شوند. به این ترتیب، کلاسهای مشتقشده میتوانند گردش کار و منطق خاص خود را بر اساس API ارائهشده توسط هر شبکه اجتماعی تعریف کنند.
حالا بیایید کلاسهای مشخص خود را پیادهسازی کنیم. اولین کلاس، کلاس Facebook
است:
public class Facebook : SocialNetwork
{
public Facebook(string username, string password)
{
base.username = username;
base.password = password;
}
protected override bool LogIn(string username, string password)
{
Console.WriteLine("\nChecking user's credentials");
Console.WriteLine($"Name: {username}");
Console.Write("Password: ");
for(var i = 0; i < password.Length; i++)
Console.Write("*");
SimulateLatency();
Console.WriteLine("\n\nFacebook login successful");
return true;
}
protected override bool SendData(byte[] data)
{
Console.WriteLine($"Message was posted successfully on Facebook");
return true;
}
protected override void LogOut()
{
Console.WriteLine($"User '{username}' was logged out from Facebook");
}
private void SimulateLatency()
{
var i = 0;
var end = new Random().Next(5, 15);
Console.WriteLine();
while (i < end)
{
Console.Write(".");
Thread.Sleep(new Random().Next(200, 800));
i++;
}
}
}
کلاس Facebook
تمام متدهای انتزاعی تعریفشده توسط کلاس SocialNetwork
را پیادهسازی میکند. توجه داشته باشید که در حالی که میتواند متد Post کلاس والد را فراخوانی کند، از پیادهسازیهای خاص خود برای متدهای Login
، Logout
و SendData
استفاده خواهد کرد.
همین موضوع برای کلاس Twitter
نیز صدق میکند:
namespace TemplateMethod.SocialNetworks;
public class Twitter : SocialNetwork
{
public Twitter(string username, string password)
{
base.username = username;
base.password = password;
}
protected override bool LogIn(string username, string password)
{
Console.WriteLine("\nChecking user's credentials");
Console.WriteLine($"Name: {username}");
Console.Write("Password: ");
for(var i = 0; i < password.Length; i++)
Console.Write("*");
SimulateLatency();
Console.WriteLine("\n\nTwitter login successful");
return true;
}
protected override bool SendData(byte[] data)
{
Console.WriteLine($"Message was posted successfully on Twitter");
return true;
}
protected override void LogOut()
{
Console.WriteLine($"User '{username}' was logged out from Twitter");
}
private void SimulateLatency()
{
var i = 0;
var end = new Random().Next(5, 15);
Console.WriteLine();
while (i < end)
{
Console.Write(".");
Thread.Sleep(new Random().Next(200, 800));
i++;
}
}
}
و در نهایت کلاس DevTo
:
namespace TemplateMethod.SocialNetworks;
public class DevTo : SocialNetwork
{
public DevTo(string username, string password)
{
base.username = username;
base.password = password;
}
protected override bool LogIn(string username, string password)
{
Console.WriteLine("\nChecking user's credentials");
Console.WriteLine($"Name: {username}");
Console.Write("Password: ");
for(var i = 0; i < password.Length; i++)
Console.Write("*");
SimulateLatency();
Console.WriteLine("\n\nDevTo login successful");
return true;
}
protected override bool SendData(byte[] data)
{
Console.WriteLine($"Message was posted successfully on DevTo");
return true;
}
protected override void LogOut()
{
Console.WriteLine($"User '{username}' was logged out from DevTo");
}
private void SimulateLatency()
{
var i = 0;
var end = new Random().Next(5, 15);
Console.WriteLine();
while (i < end)
{
Console.Write(".");
Thread.Sleep(new Random().Next(200, 800));
i++;
}
}
}
حالا که شرکتکنندگان ConcreteClass خود را داریم، کلاس اصلی خود را ایجاد کرده و برنامه را اجرا میکنیم:
using TemplateMethod.SocialNetworks;
SocialNetwork network = null;
Console.Write("Enter your username: ");
var username = Console.ReadLine();
Console.Write("Enter your password: ");
var password = Console.ReadLine();
Console.Write("Enter your post message: ");
var message = Console.ReadLine();
Console.WriteLine(@"Choose the social network you want to post to:
1. Facebook\n
2. Twitter\n
3. Dev.to");
var choice = int.Parse(Console.ReadLine());
network = choice switch
{
1 => new Facebook(username, password),
2 => new Twitter(username, password),
3 => new DevTo(username, password),
_ => throw new Exception()
};
network.Post(message);
برنامه ابتدا از کاربر میخواهد نام کاربری، رمز عبور و پیامی که میخواهد ارسال کند را وارد کند. سپس یک منو نمایش داده میشود که شامل سه شبکه اجتماعی است: Facebook، Twitter و Dev.to. انتخاب کاربر از ورودی کنسول خوانده شده و با استفاده از متد int.Parse
، ورودی کاربر از رشته به عدد صحیح تبدیل میشود.
سپس یک نمونه جدید از کلاس SocialNetwork
بر اساس انتخاب کاربر با استفاده از یک عبارت switch ایجاد میشود. بسته به ورودی کاربر، متغیر network
به یک نمونه جدید از کلاس Facebook
، Twitter
یا DevTo
تنظیم میشود و username
و password
واردشده توسط کاربر به آن ارسال میشود.
حالا به یاد داشته باشید که هر یک از کلاسهای مشخص (Concrete Classes) از کلاس SocialNetwork مشتق شدهاند، بنابراین همه آنها متد Post
را در اختیار دارند. تنها تفاوت در نحوه برخورد الگوریتم متد Post
با APIهای مختلف شبکه است. بسته به شبکه انتخابشده، متد Post
پیادهسازیهای مناسب Login
، SendData
و Logout
را فراخوانی خواهد کرد.
خروجی برنامه میتواند چیزی شبیه به این باشد:
مزایا و معایب الگوی طراحی Template Method
مزایا
- با اجازه دادن به مشتریان برای بازنویسی فقط قسمتهای خاصی از یک الگوریتم پیچیده، میتوان تاثیر تغییرات در بخشهای دیگر الگوریتم بر مشتریان را به حداقل رساند.
- کد تکراری را میتوان در یک کلاس والد متمرکز کرد.
معایب
- یک پیادهسازی پیشفرض برای یک مرحله در کلاس والد میتواند توسط یک زیرکلاس نادیده گرفته شود و منجر به نقض اصل جایگزینی لیسکوف (Liskov Substitution Principle) شود.
- با افزایش تعداد مراحل در یک متد قالب، دشواری نگهداری آن نیز بیشتر میشود.
- چارچوب ارائهشده برای الگوریتم ممکن است برخی از مشتریان را محدود کند.
ارتباط با دیگر الگوها
الگوی طراحی Factory Method شکل خاصی از الگوی طراحی Template Method است. با این حال، یک Factory Method میتواند بهعنوان یکی از مراحل در یک متد قالب بزرگتر عمل کند.
الگوی Template Method برای اجازه به زیرکلاسها برای تغییر بخشهای خاصی از الگوریتم به ارثبری متکی است. در مقابل، الگوی Strategy از ترکیب استفاده میکند تا رفتار یک شیء را با ارائه استراتژیهای مختلف تغییر دهد. Template Method در سطح کلاس پیادهسازی میشود و ایستا است، به این معنی که ساختار آن در زمان کامپایل تعیین میشود. از سوی دیگر، الگوی Strategy در سطح شیء عمل میکند و امکان تغییر رفتارها در زمان اجرا را فراهم میکند.
نتیجهگیری
در این مقاله، الگوی Template Method، زمان استفاده از آن و مزایا و معایب این الگوی طراحی را بررسی کردیم. سپس برخی از موارد استفاده از این الگو و ارتباط آن با دیگر الگوهای طراحی کلاسیک را مرور کردیم.
شایان ذکر است که الگوی Template Method، همراه با سایر الگوهای طراحی ارائهشده توسط Gang of Four، یک راهحل جهانی یا همهجانبه برای طراحی یک برنامه نیست. مهندسان باید تصمیم بگیرند که چه زمانی از یک الگوی خاص استفاده کنند. در نهایت این الگوها زمانی مفید هستند که بهعنوان یک ابزار دقیق استفاده شوند، نه یک ابزار سنگین و کلی.