برنامهنویسی شیگرا (Object-Oriented Programming یا OOP) یکی از پایههای اصلی توسعهی نرمافزار مدرن است. با گسترش سیستمهای پیچیده و نیاز به ساختاردهی بهتر به کد، تنها یادگیری اصول ابتدایی OOP کافی نیست؛ بلکه برای تولید نرمافزارهایی که هم قابل نگهداری باشند و هم توسعهپذیر، استفاده از الگوهای طراحی (Design Patterns) امری ضروری بهنظر میرسد.
الگوهای طراحی شیگرا، راهحلهایی تکرارشونده و آزمودهشده برای مسائل رایجی هستند که در طراحی نرمافزار با آنها مواجه میشویم. این الگوها نهتنها به ما کمک میکنند تا از تکرار اشتباهات گذشته اجتناب کنیم، بلکه باعث افزایش خوانایی، انعطافپذیری و قابلیت استفادهی مجدد از کد میشوند.
در این مطلب قصد داریم با تمرکز بر «الگوهای طراحی شیگرا» به معرفی جامعترین دستهبندیها و نمونههای کاربردی این الگوها بپردازیم. ابتدا با اصول طراحی شیگرا و بهترین شیوههای کدنویسی در این سبک آشنا میشویم و سپس به سراغ الگوهای اصلی طراحی (الگوهای GoF) میرویم. در ادامه، هر دسته (ایجادکننده، ساختاری، رفتاری) بههمراه مهمترین مثالهایش بررسی خواهد شد.
اصول طراحی شیگرا و بهترین شیوههای برنامهنویسی شیگرا
درک و رعایت اصول طراحی شیگرا، پیشنیاز بهرهگیری مؤثر از الگوهای طراحی است. بدون شناخت این اصول، هر الگوی طراحی ممکن است به ابزاری نامناسب در موقعیتی نادرست تبدیل شود. در این بخش، ابتدا به اصول بنیادین طراحی شیگرا میپردازیم و سپس مهمترین «best practices» را مرور میکنیم.
اصول طراحی شیگرا (SOLID Principles)
مجموعهای از پنج اصل کلیدی که توسط رابرت سی. مارتین (Robert C. Martin) معرفی شدهاند و به شکل مخفف SOLID شناخته میشوند، بهعنوان ستون فقرات طراحی شیگرا مدرن محسوب میشوند:
- Single Responsibility Principle (SRP): هر کلاس باید تنها یک وظیفه داشته باشد و فقط به یک دلیل تغییر کند.
- Open/Closed Principle (OCP): نرمافزار باید برای توسعه باز و برای تغییر بسته باشد؛ یعنی امکان افزودن قابلیتهای جدید بدون تغییر در کدهای موجود.
- Liskov Substitution Principle (LSP): کلاسهای فرزند باید بتوانند جایگزین کلاسهای والد خود شوند بدون آنکه رفتار برنامه تغییر کند.
- Interface Segregation Principle (ISP): رابطها (interfaces) نباید بیش از حد بزرگ و سنگین باشند؛ هر کلاس باید فقط با متدهایی سروکار داشته باشد که واقعاً به آنها نیاز دارد.
- Dependency Inversion Principle (DIP): وابستگی به پیادهسازی (implementation) باید به نفع وابستگی به انتزاع (abstraction) کاهش یابد.
بهترین شیوههای برنامهنویسی شیگرا (Best Practices)
علاوه بر اصول SOLID، رعایت برخی شیوههای عملی دیگر نیز به کیفیت طراحی کمک میکنند:
- ترکیب به جای وراثت (Composition over Inheritance): استفاده از ترکیب اشیاء بهجای سلسلهمراتب پیچیدهی ارثبری برای افزایش انعطافپذیری.
- جداسازی concerns: تفکیک واضح منطق تجاری، لایهی داده و رابط کاربری.
- پرهیز از استفادهی نادرست از الگوها: فقط زمانی از الگوهای طراحی استفاده کنید که نیاز واقعی وجود دارد، نه صرفاً برای زیبایی یا مد.
- کپسولهسازی دقیق: مخفیسازی اطلاعات داخلی کلاس و ارائهی تنها آنچه بیرون لازم دارد.
- یادداشتگذاری (Documentation): مستندسازی واضح کلاسها و متدها برای کمک به توسعهدهندگان دیگر (و خودتان در آینده).
الگوهای طراحی GoF (Gang of Four)
در سال ۱۹۹۴، چهار مهندس نرمافزار به نامهای Erich Gamma ،Richard Helm ،Ralph Johnson و John Vlissides کتابی منتشر کردند با عنوان Design Patterns: Elements of Reusable Object-Oriented Software. این چهار نفر که بعدها بهعنوان «گنگ چهار نفره» یا Gang of Four (GoF) شناخته شدند، پایهگذار سیستممندترین رویکرد به الگوهای طراحی در برنامهنویسی شیگرا هستند.
در این کتاب، آنها ۲۳ الگوی طراحی شیگرا را طبقهبندی و معرفی کردند که امروزه نیز مرجع اصلی بسیاری از برنامهنویسان و معماران نرمافزار محسوب میشود. این الگوها به سه دستهی کلی تقسیم میشوند:
- الگوهای ایجادکننده (Creational Patterns): تمرکز این الگوها بر نحوهی ایجاد اشیاء است؛ بهگونهای که فرایند ساخت از ساختار سیستم جدا شود.
- الگوهای ساختاری (Structural Patterns): این الگوها به نحوهی ترکیب کلاسها و اشیاء برای ایجاد ساختارهای بزرگتر و منعطفتر میپردازند.
- الگوهای رفتاری (Behavioral Patterns): رفتار و تعامل بین اشیاء را مدلسازی میکنند؛ با هدف افزایش انعطافپذیری و کاهش وابستگی.
این سه دسته، ساختاری ذهنی برای درک و استفادهی صحیح از الگوهای طراحی فراهم میکنند. در ادامه، هرکدام از این دستهها را همراه با نمونههای کلیدی بررسی خواهیم کرد.
الگوهای ایجادکننده (Creational Patterns)
الگوهای ایجادکننده به مسألهی «چگونگی ساخت اشیاء» در برنامهنویسی شیگرا میپردازند. در شرایطی که فرایند ایجاد اشیاء پیچیده یا به پیکربندیهای مختلف نیاز دارد، این الگوها به ما اجازه میدهند که از ایجاد مستقیم اشیاء خودداری کرده و ساخت آنها را به الگوهایی انعطافپذیر بسپاریم. در ادامه، سه الگوی کلیدی از این دسته را بررسی میکنیم:
الگوی سینگلتون (Singleton Pattern)
این الگو زمانی بهکار میرود که فقط یک شیء از یک کلاس باید وجود داشته باشد؛ مانند کلاس تنظیمات، لاگ سیستم، یا مدیریت اتصال به دیتابیس. هدف، تضمین ایجاد فقط یک نمونه از کلاس و فراهمکردن دسترسی سراسری به آن است. ایدهی اصلی این است که کلاس خودش وضعیت ایجاد شیء را کنترل کند و در صورتی که نمونهای قبلاً ساخته شده باشد، همان را بازگرداند.
نمونه کد (JavaScript):
class Singleton {
constructor() {
if (Singleton.instance) {
return Singleton.instance;
}
Singleton.instance = this;
this.config = {};
}
}
const a = new Singleton();
const b = new Singleton();
console.log(a === b); // true → هر دو به یک نمونه اشاره دارند
الگوی فکتوری متد (Factory Method Pattern)
هنگامیکه بخواهیم کلاسهای مختلفی را بدون وابستگی مستقیم بسازیم، یا زمانیکه کلاس والد میخواهد ایجاد شیء را به زیرکلاسها واگذار کند. کلاس پایه یک متد abstract یا virtual تعریف میکند که زیرکلاسها موظف به پیادهسازی آن هستند. این روش ایجاد اشیاء را قابل توسعه و مستقل از کلاسهای خاص میسازد.
نمونه کد (Java):
abstract class Dialog {
public void renderWindow() {
Button okButton = createButton();
okButton.render();
}
public abstract Button createButton();
}
class WindowsDialog extends Dialog {
public Button createButton() {
return new WindowsButton();
}
}
در این مثال، کلاس Dialog
نمیداند چه نوع دکمهای (Button) باید ایجاد شود، اما زیرکلاس WindowsDialog
تعیین میکند که WindowsButton
باید ساخته شود.
الگوی آبسترکت فکتوری (Abstract Factory Pattern)
برای زمانی مناسب است که بخواهید مجموعهای از اشیاء مرتبط یا سازگار را با هم بسازید، بدون آنکه به کلاسهای مشخص وابسته باشید. مثلاً ساخت ویجتهای UI برای سیستمعاملهای مختلف مثل macOS یا Windows. یک «کارخانهی انتزاعی» مجموعهای از متدها دارد که هرکدام یک نوع شیء خاص را برمیگردانند. پیادهسازیهای مختلف این کارخانه برای پلتفرمها یا حالتهای مختلف تولید میشوند.
نمونه کد (TypeScript):
interface Button {
render(): void;
}
class MacButton implements Button {
render() { console.log("Mac Button"); }
}
class WinButton implements Button {
render() { console.log("Windows Button"); }
}
interface GUIFactory {
createButton(): Button;
}
class MacFactory implements GUIFactory {
createButton(): Button {
return new MacButton();
}
}
class WinFactory implements GUIFactory {
createButton(): Button {
return new WinButton();
}
}
// استفاده از کارخانه
const factory: GUIFactory = new MacFactory();
const button = factory.createButton();
button.render(); // خروجی: Mac Button
در این سه الگو، هدف کلی جداسازی فرایند «ساخت» از «استفاده» است؛ امری که در پروژههای بزرگ و چندپلتفرمی به طرز چشمگیری از پیچیدگیها میکاهد.
الگوهای ساختاری (Structural Patterns)
الگوهای ساختاری بر نحوهی ترکیب کلاسها و اشیاء برای ایجاد ساختارهای بزرگتر و انعطافپذیر تمرکز دارند. هدف این الگوها، سادهسازی طراحی معماری نرمافزار از طریق تعریف روابط مؤثر بین اجزای مختلف است. این روابط ممکن است برای افزودن قابلیت، سازگار کردن اینترفیسها یا ترکیب رفتارها بهکار روند.
در ادامه، دو الگوی پرکاربرد در این دسته را معرفی میکنیم: Adapter و Decorator.
الگوی آداپتور (Adapter Pattern)
وقتی نیاز داریم یک کلاس موجود را با اینترفیس مورد انتظارمان سازگار کنیم، بدون آنکه در کد اصلی آن تغییری ایجاد کنیم. مثل آداپتور برق که دستگاههایی با دوشاخهی متفاوت را به پریز استاندارد متصل میکند. این الگو بهعنوان واسط بین دو کلاس ناسازگار عمل میکند و به ما اجازه میدهد از کلاسهایی استفاده کنیم که با اینترفیس موردنظرمان سازگار نیستند.
نمونه کد (JavaScript):
// کلاس موجود که اینترفیس ناسازگار دارد
class OldPrinter {
printText(text) {
console.log("چاپ با پرینتر قدیمی:", text);
}
}
// کلاس جدید که نیاز به متد print دارد
class PrinterAdapter {
constructor(oldPrinter) {
this.oldPrinter = oldPrinter;
}
print(text) {
this.oldPrinter.printText(text);
}
}
// استفاده از آداپتور
const old = new OldPrinter();
const adapter = new PrinterAdapter(old);
adapter.print("Hello"); // خروجی: چاپ با پرینتر قدیمی: Hello
الگوی دکوریتور (Decorator Pattern)
وقتی میخواهیم قابلیتهای جدیدی را بهصورت پویا (runtime) به شیء اضافه کنیم، بدون آنکه کلاس پایه را تغییر دهیم یا از ارثبری استفاده کنیم. دکوریتور شیء را در شیء دیگری قرار میدهد و قابلیتهایی به آن اضافه میکند، درحالیکه این قابلیتها برای شیء اصلی ناشناختهاند. این الگو برخلاف وراثت، انعطاف بیشتری برای ترکیب قابلیتها فراهم میکند.
نمونه کد (TypeScript):
interface Coffee {
cost(): number;
}
class SimpleCoffee implements Coffee {
cost(): number {
return 5;
}
}
class MilkDecorator implements Coffee {
constructor(private coffee: Coffee) {}
cost(): number {
return this.coffee.cost() + 2;
}
}
class SugarDecorator implements Coffee {
constructor(private coffee: Coffee) {}
cost(): number {
return this.coffee.cost() + 1;
}
}
// استفاده:
let coffee: Coffee = new SimpleCoffee();
coffee = new MilkDecorator(coffee);
coffee = new SugarDecorator(coffee);
console.log(coffee.cost()); // خروجی: 8
این دو الگو نمونههایی از قدرت طراحی ساختاری هستند؛ با استفاده از آنها میتوان بهجای تغییر کلاسها، رفتارها را در قالب شیءهای ترکیبی و انعطافپذیر تعریف کرد.
الگوهای رفتاری (Behavioral Patterns)
الگوهای رفتاری به شیوهی تعامل و ارتباط میان اشیاء در یک سیستم نرمافزاری میپردازند. این الگوها تلاش میکنند تا وابستگی بین اشیاء را کاهش دهند، وظایف را بهشکل مؤثرتری بین اجزا توزیع کنند و ارتباطات را ساختاریافتهتر کنند.
در این بخش، دو الگوی مهم و پرکاربرد را بررسی میکنیم: Observer و Strategy.
الگوی آبزرور (Observer Pattern)
مناسب برای زمانیکه تغییر در یک شیء باید بهصورت خودکار به چند شیء دیگر اطلاع داده شود. مثلاً در سیستمهای اعلان (notification)، یا ارتباط بین UI و مدل داده در معماری MVC. در این الگو، شیء subject لیستی از observerها (ناظران) را نگه میدارد و هنگام تغییر وضعیت، آنها را آگاه میکند. این جداسازی به ما امکان میدهد تا بدون وابستگی سخت، ارتباطی دینامیک و واکنشمحور بین اشیاء برقرار کنیم.
نمونه کد (JavaScript):
class Subject {
constructor() {
this.observers = [];
}
subscribe(observer) {
this.observers.push(observer);
}
unsubscribe(observer) {
this.observers = this.observers.filter(obs => obs !== observer);
}
notify(data) {
this.observers.forEach(observer => observer.update(data));
}
}
class ConcreteObserver {
constructor(name) {
this.name = name;
}
update(data) {
console.log(`${this.name} دریافت کرد:`, data);
}
}
// استفاده:
const subject = new Subject();
const obs1 = new ConcreteObserver("کاربر A");
const obs2 = new ConcreteObserver("کاربر B");
subject.subscribe(obs1);
subject.subscribe(obs2);
subject.notify("پیامی جدید");
// خروجی:
// کاربر A دریافت کرد: پیامی جدید
// کاربر B دریافت کرد: پیامی جدید
الگوی استراتژی (Strategy Pattern)
برای زمانی مناسب است که بخواهید رفتارهای متفاوت را در زمان اجرا قابل تعویض کنید، بدون آنکه کلاس اصلی تغییر یابد. مثلاً الگوریتمهای مختلف مرتبسازی یا سیاستهای متفاوت قیمتگذاری. این الگو اجازه میدهد تا الگوریتمها را بهصورت شیءهای جداگانه پیادهسازی و آنها را در زمان اجرا تزریق کنیم. کلاس کلاینت صرفاً با یک واسط کار میکند و از جزئیات پیادهسازی بیخبر است.
نمونه کد (TypeScript):
interface Strategy {
execute(a: number, b: number): number;
}
class AddStrategy implements Strategy {
execute(a: number, b: number): number {
return a + b;
}
}
class MultiplyStrategy implements Strategy {
execute(a: number, b: number): number {
return a * b;
}
}
class Context {
constructor(private strategy: Strategy) {}
setStrategy(strategy: Strategy) {
this.strategy = strategy;
}
calculate(a: number, b: number): number {
return this.strategy.execute(a, b);
}
}
// استفاده:
const context = new Context(new AddStrategy());
console.log(context.calculate(3, 4)); // 7
context.setStrategy(new MultiplyStrategy());
console.log(context.calculate(3, 4)); // 12
الگوهای رفتاری با جدا کردن رفتار از ساختار، موجب انعطاف در منطق برنامه میشوند. این جدایی نهتنها وابستگیها را کم میکند، بلکه امکان توسعه، تست و نگهداری آسانتر را فراهم میسازد.
کاربردها و مزایای الگوهای طراحی شیگرا
الگوهای طراحی شیگرا نه فقط راهحلهایی انتزاعی برای مسائل تکراری در طراحی نرمافزار هستند، بلکه در عمل نیز بخشی جداییناپذیر از توسعهی نرمافزارهای مقیاسپذیر و قابلنگهداری محسوب میشوند. در این بخش به صورت خلاصه به کاربردها و مزایای آنها میپردازیم.
کاربردهای الگوهای طراحی
- معماری ماژولار و قابل توسعه: با استفاده از الگوهای طراحی، اجزای سیستم میتوانند بهصورت مستقل توسعه، تست و بازطراحی شوند، بدون اینکه کل سیستم تحت تأثیر قرار گیرد.
- افزایش قابلیت نگهداری کد (Maintainability): طراحی مبتنی بر الگوهای شناختهشده باعث میشود که دیگر برنامهنویسان بتوانند منطق کد را سریعتر درک کنند و راحتتر آن را تغییر دهند.
- تسهیل تست و دیباگ: چون الگوها معمولاً وابستگیها را از طریق انتزاع کاهش میدهند، تست کردن قسمتهای مختلف سیستم بهصورت مستقل سادهتر میشود.
- هماهنگی با معماریهای نرمافزاری مدرن: بیشتر الگوهای مورد استفاده در معماریهایی نظیر MVC، MVVM، Microservices، Clean Architecture یا Hexagonal Architecture، ریشه در الگوهای طراحی کلاسیک دارند.
- استفاده در فریمورکها و کتابخانههای استاندارد: اغلب فریمورکهای مطرح (مثل Angular ،Spring ،React ،Django) بهصورت درونی از الگوهایی مانند Singleton ،Observer ،Decorator و Factory بهره میبرند.
در پایان
الگوهای طراحی شیگرا ابزاری قدرتمند برای توسعهدهندگان نرمافزار هستند که به کمک آنها میتوان ساختار و رفتار برنامهها را به شکل مؤثر، منعطف و قابل نگهداری سازماندهی کرد. با پیروی از اصول طراحی شیگرا و استفاده هوشمندانه از الگوهایی مانند سینگلتون، فکتوری متد، آبسترکت فکتوری، آداپتور، دکوریتور، آبزرور و استراتژی، میتوان از پیچیدگیهای غیرضروری جلوگیری کرد و کدی خواناتر، مقیاسپذیرتر و قابل استفاده مجدد ایجاد نمود.
یادگیری و بهکارگیری این الگوها نهتنها به بهبود کیفیت نرمافزار کمک میکند، بلکه مهارتهای تفکر طراحی و معماری نرمافزار را نیز در برنامهنویسان تقویت میکند.
دیدگاه و پرسش
در حال دریافت نظرات از سرور، لطفا منتظر بمانید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید