امید زاهد
امید زاهد
خواندن ۹ دقیقه·۳ سال پیش

الگوهای طراحی- بخش۲ singleton, Repository, UOW

پیش نوشت:‌ این نوشته نقش تبلیغ برای برنامه نویسان جوان رو داره. برای یادگیری دلایل استفاده از الگوهای طراحیه و هدفش توضیح محل استفاده از دیزاین پترن‌هاست

توی بخش قبلی میخواستم نشون بدم برای استفاده از دیزاین پترن‌های مدیتور و کامن کجا استفاده مشن. که به وضوح در رسیدن به این هدف شکست خوردم.

حالا میخوام یه راه دیگه رو پیش بگیرم برای توضیح دیزاین پترن‌ها. اول توضیح بدم مشکلات از چه جنسی هستند و راه حل‌هایی که باید تو بخش الگوریتم‌ها یاد می‌گرفتیم، چطور در حل مشکلات غیر ریاضی کار به ما کمک میکنند. مثلا اینکه وقتی میخوایم فیچر جدید اضافه کنیم، خیلی طول میکشه و خیلی فایل ها رو باید عوض کنیم. اینجا مساله بهینه سازی از زمان اجرای کد توسط کامپیوتر در الگوریتم‌ها، به زمان اجرای تسک توسط انسان‌ها تغییر کرد. ولی بازم مساله همونه و روش حل مساله و حمله کردن به مشکل همون روش الگوریتیمه پارامترهای بهینه سازی، بجای اینکه برای عملیات‌هایی که سیستم کامپیوتری باید انجام بده تا به جواب برسه برای تعداد عملیات ها یا تعداد خط کدی که سیستم انسانی باید انجام بده تا به هدفش برسه تعریف مشن. مثلا مساله میتونه این باشه که:

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




بیاید به عنوان یه دولوپر دوتا شرایط زیر رو در نظر بگیرید:

1. شما دارید برنامه فرانت اندی می‌نویسید، که باید تم داشته باشه و کاربر میتونه بین تم دارک و لایت یکی رو انتخاب کنه

2. شما دارید یه برنامه وب مینویسید و نیاز دارید اطلاعات رو توی دیتا بیس نگه دارید.

خب حالا شما مساله اول رو با درست کردن کلاس dark-… , light-… برای کامپوننت هاتون حل میکنید و مساله دوم رو هم هر بار بخشی از برنامه نیاز داشت به دیتا بیس درخواستی بده یه کانکشن باز میکنید.

فعلا برنامه شما کار میکنه ولی مشکلاتی هست:

درباره مساله اول وقتی کاربر خواست تم تاریک بشه باید یه کدی بنویسید که بره کلاس تمام کامپوننت ‌ها رو یکی یکی عوض کنه، برای این کار، جداگانه برای هر کامپوننت توی فانکشن مربوطه، باید مقدار یه چیزی(حالا یا اینپوت یا چک باکس یا ...) رو هر بار بخونیم و از اول مقدار تم (true, false) یا (dark, light) رو از ورودی مربوطه دریافت کنید

درباره مساله دیتابیس هم تا زمانی که شما مثلا ۴۰ ۵۰ تا درخواست در ثانیه دارید مشکلی نیست ولی بعدش متوجه میشید که سیستم خیلی کند شده و خیلی سخت میتونه اسکیل بشه. چون هر بار باید کل فرایند هزینه بر ساختن کلاس ابسترکت دیتابیس و ایجاد کانکشن TCP بین برنامه و دیتا بیس رو اجرا کنید

حالا شما میخوای بری دنبال راه حل

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

هر دوتا مساله رو میشه به دو مرحله تبدیل کرد ، به دوبخش، اول گرفتن ورودی‌ها (خوندن مقدار تم و یا درست کردن کانکشن‌ دیتابیس). دوم اجرای بخش مربوط به لاجیک (عوض کردن کلاس رنگ یا دستور ثبت اطلاعات در دریتابیس)

اجرای مرحله دوم وابسطه به مرحله قبلی هست پس ما میایم و جواب مرحله اول رو که حساب کردیم نگهش میداریم،

قبلا آدم‌ها با این مشکل برخوردن و برای راه حلش یه اسم گذاشتن،‌ که بش میگیم

سینگلتون:

UML سینگلتون
UML سینگلتون


سینگلتون یه مدل کلاس یا فانکشنه (بستگی داره oopبنویسید یا فانکشنال) که هر بار کسی کانستراکتور رو صدا میکنه اگه اون آبجکت وجود داشته باشه، بجای ساختن یک اینستنس (فارسیش رو نمی‌دونم) جدید اون اینستنس موجود رو بر میگردونه و اگه اینستنسی نباشه اونو میسازه و مقدارش رو برای دفعات بعدی که نیاز میشه نگه میداره

توی جاوا این مدلی میشه (البته thread safeکردنش یکم داستان lock و اینا داره که فعلا حوصله نوشتنش رو ندارم)

public final class Singleton { private static Singleton instance = null; private Singleton() { // private to prevent anyone from creating one // initiation logic this.isDark = input.value; } public Singleton GetInstance () { if (instance == null) instance = new Singleton (); return instance ; } public boolean GetIsDark () { return this.isDark; } public void setIsDark (Boolean dark) { this.isDark = dark; } }


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

یا مثلا در مورد دیتا بیس، تو زبان GOمیتونیم توی main.go یا توی database.go از تابع init استفاده کنیم

type Singleton struct { connStr string } var st *Singleton func init (){ // initialize the Variables of single tone st = &Singleton { connStr : os.Getenv(&quotConnection_String&quot)} } func GetDBSingleTon () *Singleton { return st }



معایب سینگلتون

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

فقط چیزایی سینگلتون بشن که واقعا لازمه مثل کانکشن پول دیتابیس یا تم توی فرانت اند.

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


حالا خوشحال و راحت میریم خونه بعد از مدتی دوتا نیازمندی جدید میاد.

بدلیل حجم بالای ریکوئست‌ها حالا دیگه میخواین از دیتا بیس sqlite به دیتا بیس mongoDB تغییر بدیم سیستم رو

شما ریکوئست‌ها تون رو به دیتابیس رو توی فایل مدل نوشتین و توی سرویس‌ها اون متدهای مدل رو کال میکنید (این روش توی MVC خیلی مرسومه)

حالا اگر بخواین نیازمندی‌های جدید رو بسازیم باید مدل‌ها رو ریفکتور بکنیم.

این مساله رو هم مردم باش مشکل داشتن و قبلا براش اسم گذاشتن ولی این بار چندتا اسم داره

ریپوزیتوری، فاساد (facade ) کمی هم dependency inversion:


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

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

یکم پیچ در پیچ شد، ببخشید.

برنامه تون وقتی میخواد چند مدل پیاده سازی از وظیفه‌ای که باید انجام بشه ولی به چند روش باید انجامش بدیم، رو انجام بده، باید یک سری کلاس ابسترکت از خود کار و مراحلش بسازیم و بعد ساب کلاس‌ها بیان و اون رو پیاده سازی کنند.

مثلا توی بک اند بجای اینکه توی سرویس‌هاتون از کامندهای SQL استفاده کنید بیاید از یک اینترفیس یا کلاس ابسترکت(ریپوزیتوری) استفاده کنید که کلاس‌های persistent (دائمی) دیتابیس میان و اون رو پیاده سازی میکنند. می پذیره یا یه کلاس ابسترکن توی js برای فرانت که آدرس API رو میذیره یا اینترفیس تایپ اسکریپت

مثلا توی فایل repository برای جاوا میایم و اینطوری مینویسم

interface MyModelRepository{ public List<MyModel> GetModels () public MyModel FindById( UUID id) public void Add (MyModel model) ....
}

حالا توی فایل persistent میاید و این اینترفیس رو برای MongoDB پیاده میکنید (کوئری‌های مناسب رو مینویسید) بعد سرویساتون با اینتر فیس کار میکنن.

تا زمانی که کلاس مخصوص هر دیتا بیس، این اینتر فیس رو پیاده میکنه، برای تغییر دادن دیتا بیس نیاز نیست که استفاده کننده ‌هاشون (سرویس‌هامون) رو تغییری بدیم

calss MongoMyClassRepo implements MyModelRepository{ .... } calss PostgreMyClassRepo implements MyModelRepository{ } // for the sake of test calss ُTestMockMyClassRepo implements MyModelRepository{ }

حالا راحت میتونیم هم کد خوانا تری داشته باشیم هم کد تست پذیر و هم امکان راحت تر عوض کردن API ها و دیتابیس‌ها

معایب ریپوزیتوری و فاساد:‌

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


برای یحث دیتا بیس ریپوزیتوری‌ها برای افزایش پرفورمنس میتونن در کنار UOW بکار برن

خلاصه Unit Of Work یا persistent aggregate


وقتی توی برنامه میخوایم تعدادی کار روی داده ها مون انجام بدیم(CRUD) و اونا رو توی دیتابیس (یا هر جایی که نیاز داره از شبکه یا دیسک استفاده کنه) ذخیره کنیم میشه از دیزاین پترن unit of work استفاده کرد چون اینجا مساله در اصل مجموع دو مساله است، اول لاجیک ارتباط بین مدل‌ها و کلاس‌های برنامه و بعدی ارتباط با دیتا بیس و ذخیره کردن داده،‌ اینجا ارتباط با دیتابیس خیلی هزینه بره چون باید اول پروتوکل های شبکه اجرا بشه بعد منتظر تاخیر شبکه باشیم تا جوابش بیاد. حالا unit of work میاد و برای ما بخش رابطه کلاس ها و لاجیک مربوط به کلاس ها رو از بخش ارتباط با شبکه جدا میکنه تا بتونیم تقریبا همه عملیات‌ها رو فقط با یک بار انجام عملیات وصل شدن به دیتابیس هم زمان انجام بدیم (این روشیه که اکثر ORM ها برای کار کردن استفاده میکنن درباره اش بخونید جالبه) unit of work می‌تونه به عنوان یک persistent aggregate در مدل برنامه سازی دامنه محور (Domain driven design )استفاده بشه.

پیاده سازیش یکم طولانیه شدیدا توصیه میکنم این پست Microsoft رو ببینید

توی aggregate ها ما میدونم که چند بخش باید با هم ذخیره بشن (همه یا هیچی) مثلا برای اطلاعات سبد خرید کاربران،‌ ما باید اطلاعات کالاها و اطلاعات زمان خرید و پرداخت رو توی دیتابیس با هم ذخیره کنیم، و اگه پرداخت کنسل بشه باید لیست کالاهای خریداری شده رو هم حذف کنیم. پس اینجا دوتا ریپوزیتوری از OrderItemRepo و OrderInfoRepo میسازیم بعد توی اینترفیس unit of work مون اینو میسازیم

type UnitStruct{ name string //the name of struct we wanna work with value interface {} } type OrderUnitOfWork struct{ // injected repositories orderIrem *OrderItemRepo orderInfo *OrderInfoRepo // to track adds news []UnitStruct // to track Updates dirties []UnitStruct // to track Deletes removes []UnitStruct } // Inject the Repositories into builder function func NewOrderUnitOfWork (info *OrderInfoRepo , item *OrderItemRepo) *OrderUnitOfWork { return &OrderUnitOfWork { orderIrem: item, orderInfo info, } }


معایت UOW:

استفاده از UOW برای کارهای ساده و جاهایی که اصلا نیازی نیست چندتا چیز رو هم زمان با هم ذخیره کنیم یا با هم حذف کنیم، یا جاهایی که نمی‌خوایم لایبرری رو مون رو بدیم دیگران هم استفاده کنن،‌ UOW واقعا کد رو بی دلیل پیچیده میکنه.

اکثرا کسانی که تازه با دیزاین پترن‌ها آشنا شدن خیلی زیاده روی میکنن توی استفاده ازشون (خودمم این مدلی بودم).



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

خوشحال میشن نظراتتون رو بگید و اینکه بابت غلط املایی‌ها عذر میخوام من خیلی املام خوب نیست

دیزاین پترنبرنامه نویسیمهندسی نرم افزار
برنامه نویس،‌ علاقه مند به Go و C# , python
شاید از این پست‌ها خوشتان بیاید