راه کار هایی اثبات شده برای بهتر کردن و خوانا کد زدن

از کجا میشه فهمید کدام دیزاین پترن مال کدوم دسته است

  • creatiural

برای ساخت پکیج ، ساخت پلتفرم و ایمپورت و استفاده از پکیج

  • structural

درگیر ساختار و dto ها میشیم ، نحوه ی ورودی و خروجی برنامه یا فانکشن (io)

  • behavioral

زمانی که ورودی یا ساختار رو گرفتیم ، حالا می خوایم تصمیم بگیریم چجوری باهاش کار کنیم ، مثلن توی runtime چجوریکار کنیم

Creational Patterns

بر مکانیسم های ایجاد شی تمرکز می کنند و سعی می کنند اشیا را به شیوه ای مناسب با موقعیت ایجاد کنند

Abstract Factory

یک رابط برای ایجاد خانواده های اشیاء مرتبط یا وابسته بدون مشخص کردن کلاس های مشخص آنها فراهم می کند.

در حقیقت زمانی استفاده میشه که پیاده سازی کلاس برامون مهم نیست و مستقله

در این دیزاین پترن ، نیازی نیست تمام اجزای برنامه را ( کلاس ها را ) پیاده سازی کنیم ، بلکه با تعریف اینترفیس ها تنها رابطه ی بین کلاس ها را تعریف می کنیم و پیاده سازی کلاس ها را مستقل از رابطه آنها در نظر می گیریم.

چهار بخش اصلی :

  • Abstract Factory

  • Concrete Factory

  • Abstract Products

  • Concrete Products

usage

زمانی استفاده میشه که مانند Builder بخوایم یه پکیج بسازیم ولی ورودی هامون اینترفیس باشه و با رفتار هایی که تعریف کردیم توی اینترفیس کار کنیم

مثلن شما orm جنگو رو دارید ولی می خوایید از mysql برید روی postgres پس نیاز دارید یه پکیج نصب کنید ، اون پکیج بدون این که ببینیدتمامی اینترفیس های فکتوری رو پیاده کرده

یا مثلن کسی که یه پلتفورم رو طراحی کرده و بقیه می تونن extention روش بنویسن از این استفاده می کنن

باید interaface در خروجی داد

برای نوشتن پلتفرمی که اکستنشن ها بتونن تو اون پلتفرم کار کنن از این الگو باید استفاده کرد

// ProductA is the interface for the first product type
type ProductA interface {
	Use() string
}
 
// ProductB is the interface for the second product type
type ProductB interface {
	Use() string
}
type AbstractFactory interface {
	CreateProductA() ProductA
	CreateProductB() ProductB
}
 
 
////////////////////
 
 
// ConcreteFactory1 is a concrete implementation of AbstractFactory
type ConcreteFactory1 struct{}
 
func (f *ConcreteFactory1) CreateProductA() ProductA {
	return &ConcreteProductA1{}
}
 
func (f *ConcreteFactory1) CreateProductB() ProductB {
	return &ConcreteProductB1{}
}
 
// ConcreteFactory2 is another concrete implementation of AbstractFactory
type ConcreteFactory2 struct{}
 
func (f *ConcreteFactory2) CreateProductA() ProductA {
	return &ConcreteProductA2{}
}
 
func (f *ConcreteFactory2) CreateProductB() ProductB {
	return &ConcreteProductB2{}
}
 
//////////////
func main() {
	var factory AbstractFactory
 
	// Use ConcreteFactory1
	factory = &ConcreteFactory1{}
	productA1 := factory.CreateProductA()
	productB1 := factory.CreateProductB()
	
	println(productA1.Use()) // Output: Using Product A1
	println(productB1.Use()) // Output: Using Product B1
 
	// Use ConcreteFactory2
	factory = &ConcreteFactory2{}
	productA2 := factory.CreateProductA()
	productB2 := factory.CreateProductB()
 
	println(productA2.Use()) // Output: Using Product A2
	println(productB2.Use()) // Output: Using Product B2
}

factory method

این الگو به ما اجازه می دهد تا با استفاده از interface ها ، شی مورد نظر خود را بسازیم اما زیر کلاس ها بتونن و اجازه داشته باشن کلاس ساخته شده را تغییر دهند

معمولا زمانی استفاده میشود که یک شی فیلد هاش قابل پیش بینی نباشد

بخوایم به زبون ساده بگیم به فانکشنی که اینترفیس ریترن می کنه میگن فکتوری متود.

به دلیل این که گولنگ از وراثت پشتیبانی نمی کند ، نمی توان مثالی آورد ، اما میشه به روش زیر تلاش کنیم نیاز مندی هامون رو رفع کنیم

package main
 
import "fmt"
 
// Product Interface
type Vehicle interface {
	Drive() string
}
 
// Concrete Products
type Car struct{}
func (c Car) Drive() string { return "Driving a car 🚗" }
 
type Bike struct{}
func (b Bike) Drive() string { return "Riding a bike 🚴" }
 
// Factory Method
func GetVehicle(vehicleType string) Vehicle {
	switch vehicleType {
	case "car":
		return Car{}
	case "bike":
		return Bike{}
	default:
		panic("unknown vehicle")
	}
}
 
func main() {
	car := GetVehicle("car")
	fmt.Println(car.Drive()) // Driving a car 🚗
}

اما اونجا که سابکلاس ها می تونن سوپر کلاس رو اور رایت کنن رو ئایتون داره و گولنگ نداره و گولنگ باید با امبد و کامپوزیشن ئیاده سازی کنه :

 
from abc import ABC, abstractmethod
 
class VehicleFactory(ABC):  # Base Creator
    def __init__(self, base_color="red"):
        self.base_color = base_color  # Shared state for all factories
    
    def prepare_vehicle(self):  # Shared logic for all factories
        print(f"Preparing {self.base_color} vehicle")
    
    @abstractmethod
    def create_vehicle(self) -> "Vehicle":  # Factory Method
        pass
 
# Concrete Creators (inherit shared logic)
class CarFactory(VehicleFactory):
    def create_vehicle(self):  # Override factory method
        self.prepare_vehicle()
        return Car()
 
class BikeFactory(VehicleFactory):
    def create_vehicle(self):
        self.prepare_vehicle()
        return Bike()

به لاجیک شیرد ها توجه شود

type VehicleFactory struct {
    baseColor string
}
 
func (f *VehicleFactory) prepareVehicle() {
    fmt.Printf("Preparing %s vehicle\n", f.baseColor)
}
 
// No way to force implementation in "subclasses"
type Creator interface {
    CreateVehicle() Vehicle
}
 
// Must reimplement shared logic per factory
type CarFactory struct {
    VehicleFactory // Embedding (composition)
}
 
func (f *CarFactory) CreateVehicle() Vehicle {
    f.prepareVehicle() // Manual call to shared logic
    return Car{}
}
 
type BikeFactory struct {
    VehicleFactory
}
 
func (f *BikeFactory) CreateVehicle() Vehicle {
    f.prepareVehicle() // Duplicated boilerplate
    return Bike{}
}

factory method vs Abstract Factory

شباهت

ساختار هر دو بسیار شبیه است

و باید interface در خروجی باشد

نکته جز معدود جاهایی است که در سیگنیچر این ها ، اینترفیس باید ریترن کرد

بیش از حد شبیه Abstract Factory هست تنها تفاوتشون نحوه ی استفادشون هست ولی ساختارشون یکیه

توی Abstract Factory می تونیم چندین شی شبیه به هم کانکریت کنیم

type AbstractFactory interface {
	CreateProductA() ProductA
	CreateProductB() ProductB
}

اما توی factory method نیازه تنها یک شی کانکریت بشه

type Creator interface {
	FactoryMethod() Product
}

همچنین client یا کاربر تنها در main.go از اشیا استفده می کنند و خود آن ها را کانکریت نمی کنند

کانکریت کردن وظیفه ی کاربر نیست و subclass باید superclass را کانکریت کند

Builder

ساخت یک شی پیچیده را از نمایش آن جدا می کند و به فرآیند ساخت یکسان اجازه می دهد تا نمایش های متفاوتی ایجاد کند.

این دیزاین پترن زمانی استفاده می شه که بخوایم یک استراکت که تعداد زیادی متغیر داره رو مقدار دهی کنیم

بجای اینکه بخوایم تمام مقادیر رو یگ جا بدیم، می تونیم از الگوی بیلدر استفاده کنیم و تیکه تیکه به اون مقدار بدیم

و یکی از بهترین مثال هاش ، او آر ام جنگو هست که می شه تیکه تیکه آبجکت رو ساخت و در نهایت اون رو اگزکیوت کرد

singleton

اطمینان می دهد که یک کلاس فقط یک نمونه دارد و یک نقطه دسترسی جهانی به آن فراهم می کند.

این الگو می گوید بهتر است در صورتی که شی داریم که چند جا استفاده می شود با استفاده از الگوی سینگل تن یک بار آن شی را بسازیم و بار ها از آن شی استفاده کنیم . همچنین دیگر کاربر نمی تواند داده را تغییر دهد و نیاز به تخصیص حافظه بیش از حد به این شی نیست ( در صورتی که از این الگو استفاده نکنیم )

برای این کار باید ابتدا تایپ مورد نظر را تعریف کنیم ( کلاس را بنویسیم ) سپس یک اینستنس از آن بسازیم و با متد گیرنده ، آن را در اختیار تمام پروژه قرار دهیم

نکته : به هیچ وجه در ساختار سینگلتون نباید اشاره گر استفاده شود زیرا با استفاده از آن می توان اینستنس را تغییر داد

نکته : دقت شود بعضی از آموزش ها ، در کنار اینستنس ، تایپ هم تعریف می کنند . در صورتی که کلاس سینگلتن را درون تایپ تعریفی خود بریزیم و آن تایپ را ریترن کنیم ، ریسیور های آن منتقل نخواهند شد . باید خروجی خود استراکت پکیج باشد

Prototype

این الگو ما را ملزم می کند که هر شی که ایجاد می کنیم ، بتوان آن را با تمام ویژگی ها ( پراپرتی ها و متد ها ) بتوان کپی کرد.

درحقیق یک اینترفیس وجود دارد که باید متد clone را پیاده سازی کرد که خروجی این متد ، تایپ استراکت است .

Structural Patterns

“چگونه اشیاء/کلاس‌ها ترکیب یا سازماندهی می‌شوند؟”

اگر مشکل ساختار اشیاء است (مثلاً تبدیل رابط‌ها یا ترکیب اشیاء) → ساختاری.

Proxy

این دیزاین پترن میگه فرض کنیم سرویس یا کلاسی داریم که شاید نتونه همه ی درخواست ها رو هندل کنه ، شاید حجم درخواست ها زیاد باشه ، شاید نیاز باشه درخواست ها رو چک یا کنترل کرده به همین دلیل سرویس میانی میسازیم که خیلی شبیه سرویس اصلی است و کلاینت دچار مشکل نمیشه اگه بجای ارسال به سرویس اصلی ، به این بفرسته .

و برای پیاده سازیش نیاز داریم یه اینترفیس با سیگنیچر های سرویس اصلی بسازیم ، و در نهایت سرویس میانی رو مینویسیم و بعد از کنترل در خواست ها رو به سرویس اصلی میفرستیم

میشه گفت پروکسی خیلی شبیه یک رپر است ، کلاینت نباید متوجه بشود که با کلاس اصلی در تعامل است یا پروکسی

facade

الگو بسیار ساده است و ما از آن استفاده می کینم ، زمانی که ما نیاز به چند متد از چندین کلاس متفاوت داریم ، می توان در یک قطعه کد بزرگ ، تمامی آن ها را فراخوانی کرد و استفاده کرد ، نیاز داریم تمامی کلاس ها رو ایمپورت کنیم ، اما راه بهتر این است که یک کلاس ایجاد کنیم که آن متد ها و کلاس ها رو فرا خوانی کنه ، و نیازی نباشه در کد اصلی همه ی جزییات متد ها رو بشناسیم

همچنین زمانی که یک قطعه کد قدیمی که کار با آن دشوار بوده و بهبود آن ناممکن است ، می توان از این دیزاین پترن استفاده کرد به عبارت دیگر میدونیم یه کد درست کار میکنه ولی کار با اون خیلی سخت است ، یه لایه روی این کد ها مینویسیم که کلاینت هم توانایی کار با متد های قدیمی و دشوار رو داشته باشه ، هم از متد های راحت ما استفاده کنه

و اولین الگو هست دیدم که نیاز به اینترفیس نداره

فرض کنید به جای اینکه کاربر از تک تک ریسیور های یه کلاس باید بلد باشه استفاده کنه و باید ترتیب اجرای چندین ریسیور از چندین کلاس رو توی برنامه اش بچینه ، ما بیایم این ها رو به ترتیب بچینیم براش و کلاینت فقط اون رو کال کنه

adaptor

در صورتی که بخواهیم دو ابجکت که اینترفیس جدا دارن رو اینتگریت کنیم همچنین در صورتی که از ترد پارتی استفاده بخوایم بکنیم و نیازه از ورژن های جدید استفاده کنیم

bridge between two incompatible interfaces

composite

این دیزاین پترن برای دیتا استراکچر درختی استفاده میشه و میاد داده رو به صورت سلسله مراتبی تو آبجکت ها میزاره و راحت تر میشه اطلاعات واکشی کرد

bridge

دقیقا همون مفهوم dependency inversion یا جدا سازی نیاز مندی با استفاده از اینترفیس هست . میگه وابستگی هاتو با اینترفیس بر طرف کن

decorator

افزودن قابلیت به کد با کمترین هزینه و بدون تغییر کلاینت

Behavioral Patterns

الگوهای رفتاری با الگوریتم ها و تخصیص مسئولیت ها بین اشیا سروکار دارند. آنها به مدیریت الگوریتم ها، روابط و مسئولیت ها در بین اشیا کمک می کنند.

command

این الگو میگه به جای این که پارامتر به فانکشن ها بدیم ، آبجکت بدیم ، به این صورت ، هر درخواست علاوه بر مقدار ، چندین متد هم داره

یک درخواست را به عنوان یک شی کپسوله می کند و در نتیجه امکان پارامترسازی کلاینت ها با صف ها، درخواست ها و عملیات را فراهم می کند.

همچنین برای پیاده سازی باید فانکشن ها ، اینترفیس بگیرند و ما درخواست ها را ابتدا باید با توجه به پروتوتایپ اینترفیس پیاده کنیم ، فانکشن هم به جای دریافت کلاس یا پارامتر عادی، اینترفیس گرفته و با سیگنیچر های اینترفیس کار میکند

این مفهوم رو توی گولنگ زمانی می بینیم که از io.Reader/Writer استفاغده می کنیم & ورودی مهم نیست چیه ، مهم اینه که چند تا سیگنیچر داشته باشه

mediator

مورد علاقه سپهر ، میاد لوس کاپل میکنه کامپوننت ها رو با استفاده از ارتباط مرکزی ، به جای این که کامپوننت ها به صورت مستقیم با هم در ارتباط باشن ، از طریق یک ابجکت مرکزی با هم در ارتباط هستند

خیلی خیلی شبیه مفهوم ایونت دیریون هست ، در اسکوپ فانکشن اگر بخواهیم فانکشن ها رو لوز کاپل کنیم ، از این مفهوم استفاده میکنیم

observer

اگر بخواهیم فانکشن یا کلاسی از تغییرات یا آپدیت شدن کلاسی دیگر مطلع شود از این دیزاین پترن استفاده می کنیم

به تعریف دیگر اگر بخواهیم بعد از آپدیت شدن یک تابع، پیام یا داده ای به دیگر کلاس ها داده شود از ابزرور استفاده می کنیم

مثلا سیگنال در جنگو از این دیزاین پترن استفاده می کند

در اینجا ۲ مفهوم داریم :

Subject یا پابلیشر

Observer یا سابسکرایبر

در سابجکت لیستی وجود دارد که هر ابزرور که بخواهد تغییرات را مشاهده کند ، در آن اضافه می کنیم

همچنیندر در ابزرور ها باید متدی مانند آپدیت باشد تا هر بار در سابجکت تغییر صورت گرفت ، این متد را صدا بزند و آبزرور متوجه تغییر بشود

strategy

زمانی که برای انجام کاری ، چند الگوریتم متفاوت داریم و با توجه به ورودی ، باید یکی از الگوریتم ها را انتخاب کنیم ، از دیزاین پترن استراتجی استفاده می کنیم .

فرض کنیم برای پرداخت الکترونیکی چند درگاه با متد های مختلف داریم و کلاینت می تواند یکی را انتخاب کند . شاید الگوریتم ها متفاوت باشد ولی روند کلی همه یکسان است.

ساختار :

یک اینترفس استراتجی داریم که امضای متد مشترک تمام الگوریتم ها را دارد

تمام متد ها باید اینترفیس را پیاده سازی کنند

کانتکست ، گرفتن اسم متد مشخص از کلاینت و اجرای متد های پیاده سازی شده

کلاینت یا متد اصلی

نکته ی مهم دیگر استراتجی پترن هم این است که به جای استفاده از if/else یا switch از map استفاده می کنیم به این صورت که کلید مپ نام آن استراتجی است و مقدارش FACTORY FUNCTION است

فرض کنیم به ازای هر استراتجی یک کانستراکتور باید داشته باشیم مانند کانستراکتور زیر :

NewShahinService(config config.App, bank string) (*shahinService, error) NewSadadService(config config.App, bank string) (*sadadService, error)

و وظیفه ی این است که استراکتی که اینترفیس را ایمپلمنت کرده بسازیم

اما مشکلی که وجود دارد این است که هارد کدی باید بگیم به ازای این استراتجی چه کانستراکتوری باید داشته باشیم همچنین تنها یک بار ایجاد میشود و هر بار از اون مجددا استفالده میشود

اما روش بهینه این است که تایپ اون فانکشن رو بسازیم و اون رو داخل MAP قرار بدیم

 
 
type BankServiceFactory func(config.App, string) (interfaces.BankService, error)
 
var registry = make(map[string]BankServiceFactory)
func RegisterBankServiceFactory(providerName string, factory BankServiceFactory) {
	registry[providerName] = factory
}
 

اگر توجه شود سیگنیچر BankServiceFactory شبیه کانستراکتور است

iterator

در صورتی که تعدادی داده داشته باشیم و بخواهیم آن را پیمایش کنیم از این دیزاین پترن استفاده می کنیم .

گاهی ساختار کالکشن داده ی ما به سادگی لیست نیست ، مثلا درختی است ، در این صورت می توانیم نحوه ی پیمایش درختی دلخواه خود را پیاده کنیم ( مثلا بر اساس عمق درخت پیمایش کند یا فرزند خوشه )

مثلا مجموعه ای سطر از دیتابیس گرفته ( به صورت پیور) و حال می خواهیم آن را پیمایش کنیم .