Baron
Baron
خواندن ۱۱ دقیقه·۵ ماه پیش

الگو طراحی factory

چند وقت پیش یه api واسه پردازش اسلایس های go ساختم. این بخشش که میخوام توضیح بدم یکمی object oriented نوشته شده که خیلی با مذاق go سازگار نیست. خلاصه اگه یکم زشت شده شما ببخشید.

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

کد نهایی
کد نهایی

ماجرا اینکه ما یه اینترفیس Stream داریم و دوتا struct که این اینترفیس رو پیاده سازی کردن(در oop میگیم اینترفیس Stream دوتا زیر کلاس داره)

اسم این struct ها:

sequentialStream

concurrentStream

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

حالا اگر کلاینت(کسی که قراره از کد ما استفاده کنه) بخواد یه استریم ترتیبی بسازه باید اینکارو کنه:

s := []int{1, 5, 16, 13, 71, 22}

seqStream := sequentialStream{slice: s}

و یا همروند:

conStream := concurrentStream{slice: s}

اینکه کلاینت ما میدونه دقیقا چه struct هایی با چه اسم هایی وجود داره مشکل حساب میشه

کلاینت صرفا نیاز داره بدونه که دو "نوع" استریم داریم که یکی با قابلیت ترتیبی و یکی همروند. حالا اینکه اسم هرکدوم struct چیه نیاز نیست

پس از factory method استفاده میکنیم

مرحله اول ساخت تابع فکتوری هست

اسمشو New میذاریم

خروجی از نوع اینترفیس Stream هست

و ورودی هم فیلد های struct هامون هست که اینجا فقط اسلایس هستش

و یه ورودی دیگه هم که مشخص کنه کدوم "نوع" میخوایم ساخته بشه(ترتیبی یا همروند) که من اینجا از int استفاده میکنم: اگر 0 بود یعنی ترتیبی میخوایم اگر 1 بود یعنی همروند میخوایم ساخته شه.

پس تابع New یه همچین چیزی میشه:

همینجا بگم که بخاطر اینکه کد الکی پیچبده نشه از اسلایس int استفاده کردم و مثال ها generic نیستن

بلاک تابع New ساده هستش:

خب حالا کلاینت برای اینکه استریم بسازه باید اینکارو کنه:

s := []int{1, 5, 16, 13, 71, 22}

seqStream := New(s, 0)

conStream := New(s, 1)

خب این سه تا مشکل داره(شاید شما بیشتر از سه تا پیدا کنید)

یکی اینکه 0 و 1 خیلی تجربه جالبی ایجاد نمیکنه

بهتره تو هر package ای که این تابع New رو گذاشتیم یه همچین چیزی هم اضافه کنیم:

const (

SequentialType = 0

ConcurrentType = 1

)


حالا شرط ایف تو New میشه این:

if t == SequentialType

و ساخت استریم ترتیبی:

s := []int{1, 5, 16, 13, 71, 22}
seqStream := New(s, SequentialType)

مشکل دوم اینکه New به تنهایی منظور خاصی رو نمیرسونه. اصلا چی رو new میکنه؟ بهتر نبود اسمش NewStream می بود؟

جواب سوال آخر اینکه نه نیاز نیست. چرا؟

چون در حقیقت ما داریم تمام این استراکت ها اینتفرس و متد New رو در یک پکیج جدا مینویسیم

مثلا فرض کنید اسم پکیج رو میذاریم gostream

در این صورت کلاینت ما باید برای ساخت استریم ترتیبی همچین چیزی بنویسه:

s := []int{1, 5, 16, 13, 71, 22}

seqStream := gostream.New(s, gostream.SequentialType)

اینجوری کلاینت دیگه میدونه که تابع New ای که در پکیج gostream هست احتمالا کارش ساخت استریم هستش

اگر هم حس میکنید gostream برای چند بار نوشتن زیادی طولانیه، میشه در بخش ایمپورت ها gostream رو تبدیل کنه به یه کلمه کوچیکتر(مثلا gs) و حالا اینجوری بنویسه:

s := []int{1, 5, 16, 13, 71, 22}

seqStream := gs.New(s, gs.SequentialType)

مشکل سوم چیه؟ اینکه برای ورودی دوم New که تایپ استریم رو مشخص میکنه میشه اعداد غیر از 0 و 1 هم داد که این فعلا برامون مشکل ایجاد نمیکنه چون که گفتیم اگر 0 نبود (حالا یا میخواد 1 باشه یا 13 یا 100) استریم همروند بساز. خلاصه اگه 0 بود ترتیبی بساز و هرچی جز 0 بود همروند. البته بعدا مشکل ایجاد میشه ولی فعلا راحتیم. خداروشکر :)

خب ازینجا به بعد دیگه کاری به کد کلاینت نداریم یعنی کد کلاینت برای ساخت استریم ازینجا به بعد کلا همینه:

gostream.New(s, gostream.SequentialType)

ازینجا به بعد میخوایم کد پکیج خودمون رو بهتر کنیم

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

خب برگردیم به تابع New. یک بار دیگه ببینیدش:

فرض کنید بعد ها خواستید یه نوع جدید از Stream داشته باشید( استراکت سوم)

اینجا باید در if else برای ساختنش لحاظش کنید که این با قانون open closed در تضاده

خب چیکار کنیم؟ از abstract factory استفاده میکنیم :) سعی میکنم به ساده ترین روش توضیح بدم

اول از همه یه اینترفیس می‌سازیم (بهش میگیم streamFactory)

type streamFactory interface {

}

این اینترفیس یه متد داره که اسمشو میذاریم Create. اگرم با حرف کوچیک شروع میشد فرقی نمیکرد چون متد Create قرار نیست خارج از پکیج فراخوانی بشه.

ورودی این متد هم فیلد های struct هامون هستن که اینجا فقط اسلایس هستش و خروجی هم Stream. پس:

type streamFactory intrrface {

Create(s []int) Stream
}

حالا میایم برای هرکدوم از struct هامون(sequentialStream و concurrentStream) یه struct میسازیم که هیچ فیلدی ندارن و اینترفیس streamFactory رو پیاده سازی میکنن(اسمشون رو میذاریم sequentialStreamFactory و concurrentStreamFactory)

هرکدوم ازین استراکت های Factory یه متد Create دارن طبیعتا.

متد Create ای که sequentialStreamFactory داره کارش اینکه یه شی از جنس sequentialStream ریترن کنه و متد Create ای که concurrentStreamFactory داره کارش اینکه به شی از جنس concurrentStream ریترن کنه:

دید کلی ای که نسبت به abstract factory وجود داره هم این شکلیه:

ن
ن

ایده اینکه ما یه سلسه مراتب داریم با اینترفیسی به اسم Result و زیرکلاس هاش. حالا میایم یه سلسه مراتب دیگه می‌سازیم به اسم Factory که یه متد به اسم Create داره و واسه هر کدوم از زیر کلاس های Result یه کلاس(استراکت) میسازیم که اینترفیس Factory رو پیاده سازی میکنه و متد Create ای که داره یه شی از جنس کلاس متناظر با خودش رو ریترن میکنه

اینجا Result همون Stream هستش

حالا بیاین ببینیم چطور با همچین چیزی میتونیم یه استریم همروند بسازیم:

s := []int{1, 5, 16, 13, 71, 22}

خب این از اسلایس.

حالا باید یه شی از concurrentStreamFactory بسازیم:

f := concurrentStreamFactory{}

که طبیعتا هیچ فیلدی نداره اما یه متد Create داره که اون استریم همروند ریترن میکنه:

conStream := f.Create(s)

پس برای ساخت استریم همروند:

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

بعد از متد Create اون فکتوری استفاده میکنیم

یه سوال میپرسم قبل از اینکه ادامه بدید روش فکر کنید: ما برای ساخت شی از یه سلسله مراتب(Stream و زیر کلاس هاش) اومدیم به سلسله مراتب دیگه(Factory و زیر کلاس هاش) ساختیم. خب این سلسله مراتب دومی خودش برای ساخت شی ازش نیاز به یه سلسله مراتب دیگه نیاز نداره؟

خب طبیعتا و منطقا نباید نیازی باشه چون این کار رو میشه زنجیر وار تا بینهایت ادامه داد ولی بذارید بریم تابع New رو عوض کنیم و در کنارش به این سوال هم پاسخ بدیم:

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


خب ازتون میخوام این نسخه New رو با نسخه قبلی مقایسه کنید:

در نسخه قبلی با توجه به ورودی یه ایف الز میذاشتیم و دقیقا به صورت مستقیم یه شی از استراکت های Stream می‌ساختیم

در این نسخه با توجه به ورودی و با ایف الز، اول factory رو میسازیم بعد از متد Create ای که داره استفاده میکنیم که برای ساخت یه شی از استراکت Stream مد نظرش هستش.

تفاوتی که وجود داره اینکه ما نیاز نداریم برای هر بار فراخوانی New یه factory جدید بسازیم

همونطوری که گفتم استراکت های factory هیچ فیلدی ندارن (خلاصه که منو یاد singleton objects میندازه که از هر کلاس یا استراکت به یه شی ازش بیشتر نیاز نداریم)

اصلا واسه همینه که سلسله مراتب دومی(factory) خودش نیاز به یه سلسه مراتب دیگه نداره چون اصلا دغدغه ای واسه ساخت شی ازشون نداریم و یه بار ساخت شی ازشون بسه. پس از فرایندی استفاده میکنیم به اسم object caching:

const (
sequentialType = 0
concurrentType = 1
)

این کانست ها دقیقا بالای تابع New بودن

کاری که میکنیم اینکه یه ساختمان داده map میسازیم(اسمشو میذاریم factories) با کلید های int (همون sequentialType و concurrentType) و value هایی از جنس Factory:

یه [ بعد Factory اضافس
یه [ بعد Factory اضافس

خب حالا نسخه جدید تابع New رو ببینید:

منطقیه دیگه؟ اگه ورودی گفت استریم ترتیبی میخوام از کلید SequentialType برای مپ استفاده کن در غیر این صورت از کلید ConcurrentType.

حالا لطفا یبار دیگه به این New نگاه کنید :)

اگه t همون SequentialType بود پس از SequentialType به عنوان کلید استفاده کن(که اینجا چون if گذاشتیم پس t همون SequentialType هست پس میشه گفت از همون t به عنوان کلید استفاده کن)

برای ConcurrentType هم همینطور. خلاصه t چه 0 باشه چه 1 باشه به هر حال از همون t به عنوان کلید استفاده میشه

پس بجای اینکه من از if else استفاده کنم مستقیم از t به عنوان کلید مپ استفاده میکنیم:

و یا:

میبینید چه قشنگ؟ نه تنها از ایف الز دیگه استفاده نمیکنیم بلکه کل بلاک تابع New یه خط بیشتر نشد.

خب کد کلاینت طبیعتاً عوض نمیشه و برای ساخت استریم همچنان یه همچین کاری میکنه:

gostream.New(slc, gostream.SequentialType)

حالا اون مشکلی بود که گفتم فعلا از دستش راحتیم، اینجا گریبانگیر میشه :)

نکته اینجاس که کلاینت میتونه همچین چیزی بنویسه:

gostream.New(s, 10)

که از این 10 به عنوان کلید map استفاده میشه و ما میدونیم در map فقط کلید های 0 و 1 وجود دارن.

خب چیکار کنیم؟ راه حل های متفاوتی هست

اولین و ساده ترین راهش اینکه بگیم اگه همچین کلیدی در map وجود نداشت یه nil ریترن کن:

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

اول از همه یه استراکت جدید میسازیم به اسم inputType که یه فیلد از جنس int داره (اسم فیلدو میذاریم t):

type inputType struct {

t int

}

همین الان بگم این struct با i کوچیک شروع میشه.

حالا ورودی دوم تابع New که مشخص میکرد از چه نوع استریم میخوایم ساخته بشه بجای t int یه inputType میگیره و داخل بلاک هم از فیلد t ای که inputType داره استفاده میکنیم:

خب حالا وقتشه برگردیم به const ها:

const(

SequentialType = 2

ConcurrentType = 1

...

)

نکته ای که هست اینکه به عنوان وروردی دوم تابع New نمیتونیم دیگه از SequentialType و یا ConcurrentType استفاده کنیم چون اینا از جنس int هستن ولی ورودی دوم دیگه از جنس inputType میگیره

غصه نداره. اینا رو بجای int به inputType تبدیل میکنیم:

خب کد کلاینت همچنان این شکلیه:

gostream.New(slc, gostream.SequentialType)

ولی حتی نمیدونه که جنس SequentialType از int به inputType عوض شده :))

خلاصه این روش این شد:

ما اومدیم یه استراکت به اسم inputType ساختیم و در ورودی دوم New به جای int یه inputType میگیریم. دوتا const از inputType ساختیم که کلاینت موقع فراخوانی New از اونا استفاده کنه. این نکته هم اضافه کنم که کلاینت نمیتونه هر شی دلخواه از inputType بسازه(بخاطر اینکه inputType با حرف کوچیک شروع شده نه بزرگ)و فقط مجبوره از اون دوتا SequentialType و ConcurrentType استفاده کنه. و اینجوری دیگه هیچوقت در map ای که داریم از کلیدی جز 0 و یا 1 استفاده نمیشه

و در اخر بگم که اینترفیس Stream چند تا متد داره که میتونید ازشون استفاده کنید(تمام این کارا رو کردیم که بتونیم از متد های استریم استفاده کنیم)مثلا قطعه کد زیر خونه هایی از اسلایس که زوج هستن رو چاپ میکنه:

و یا قطعه کد زیر مجموع مربع خونه های فرد رو حساب میکنه و در sum قرار میده:

اول اعداد فرد رو با متد Filter جدا می‌کنیم بعد با متد Map اونا  رو به توان دو میرسونیم بعد با Reduce جمعشون رو حساب میکنیم
اول اعداد فرد رو با متد Filter جدا می‌کنیم بعد با متد Map اونا رو به توان دو میرسونیم بعد با Reduce جمعشون رو حساب میکنیم


خب تموم شد:)

اون کدی که گفتم فعلا بهش نگاه نکنید و الان میگم که کلا نیازی نیست نگاهی بهش بندازید :)

ولی اگه میخواید یه تصویر کلی ازش داشته باشید میتونید یه نگاهی کنید فقط چند تا نکته:

۰. در کد اصلی به جای const از var استفاده کردم(که اشتباهه)

۱. در کد اصلی بجای اسم Create از New استفاده کردم. یعنی تو قطعه کد اصلی دوتا New داریم که در توضیحات بخاطر اینکه ابهامی پیش نیاد من اسم یکی رو Create گذاشتم(طبیعتا بهتر بود که در کد اصلی هم همینکارو باید میکردم)

۲. اینترفیس Stream و استراکت هاش و متد New و سلسله مراتب Factory همگی generic هستن. همون طور که گفتم برای راحتی از int استفاده کردم. در ادامه باید بگم طبیعتا const ها و var هایی که بیرون از توابع تعریف میکنیم نمیتونن generic باشن. پس در کد اصلی، مپ factories رو داخل تابع New تعریف کردم که البته این دیگه object caching حساب نمیشه.

۳. من این مشکل که map کلیدی با مقدار مثلا 10 نداره رو در کد اصلی لحاظ نکردم(باید اینکارو بکنم ولی چند وقتیه بخاطر افسردگی اصلا حوصله انجام دادن عملی به اسم برنامه نویسی ندارم)

یه خسته نباشید به خودم و شما میگم و ممنونم که تا اینجا همراهم اومدید

مراقب خودتون باشید



software designبرنامه نویسیگولنگgolangdesign pattern
In the trees
شاید از این پست‌ها خوشتان بیاید