مهندس نرم افزار و کارشناس ارشد مدیریت IT (کسب و کار الکترونیک)
یک فنجان جاوا - دیزاین پترن ها - Object Pool
سلام. میتونید برای آشنایی با الگوهای طراحی (یا همون دیزاین پترن های) زبان جاوا و دیدن لیست مقالاتِ آموزشیشون، به مقاله ی یک فنجان جاوا - دیزاین پترن ها Design Patterns مراجعه کنید.
همونطور که گفته شده این الگو (استخر اشیا) زیرمجموعه ی الگوهای تکوینی یا ساختنی هست و لیست مقالات آموزشیِ من در مورد الگوهای Creational عبارت هستند از:
Creational Design Patterns
الگوی Object Pool
خب رسیدیم به آخرین مورد از الگوهای Creational توی لیستمون، یعنی استخر اشیاء. احتمال زیاد تا حالا به این موضوع برخوردین که داخل کد، نیاز باشه تعداد خیلی زیادی از یک یا چندین شئ ایجاد کنین. گاهی اوقات مشکلی وجود نداره، ولی زمانهایی هست که ممکنه نیاز باشه مثلاً یعالمه شئ ای ایجاد کنین که مشابه هم هستن ولی مدت زمان مصرف کوتاهی دارن (مثلاً توی بازی هایی که آبجکتهای زیادی مثل توپ داره از بالا میاد و از پایین صفحه میره بیرون). یا مثلاً شاید لازم باشه اشیائی رو ایجاد کنیم که یه مدتی باهاشون کار نداریم به همین خاطر از بین میبریمشون و بعداً مجدد میسازیمشون و اگه این قضیه خیلی زیاد اتفاق بیفته، عملیات شئ ساختن ممکنه برامون سنگین تموم بشه (مثلاً جایی که لازمه برای ساخت یک شئ، ریکوئست هایی بزنیم به دیتابیس). گاهی اوقات هم لازمه یه استِیت، حالت، تنظیمات یا هر مورد دیگه ای رو جایی توی حافظه نگه داریم و بعداً مجدد بیایم سراغش (مثل اطلاعات یا تنظیماتِ کاربرِ فعلی). توی این موارد و حتی موارد دیگه ای مثل زمانهایی که نرخ تکرارِ ایجاد اشیا خیلی زیاده (مثلاً کاربر تعداد تاچ های زیادی داره هر بار شئ جدیدی ایجاد میشه)، یا برعکس، تعداد خیلی کمی شئ توی برنامه لازم داریم (مواردی که برنامه کُلٌَن چند تا شئ بیشتر نداره)، میتونیم از الگوی استخر اشیاء یا همون Object Pool استفاده کنیم.
یه نکته ی دیگه در مورد این الگو، اینه که اگه بصورت Singleton پیاده سازی بشه، جاهایی مثل ارتباط و کانکشن با دیتابیس یا مثلاً سوکت ها و غیره میتونه توی برنامه هایی که بصورت چندنخی (multithread) نوشته میشن هم خیلی کمک کننده باشه (که طبیعتاً اغلب توی برنامه های چندنخی بدلیل ماهیت این الگو و توجه به این مورد که میخوایم از اشیاءِ قبلی مجدد استفاده کنیم، مجبوریم استخرمون رو بصورت سینگلتون پیاده سازی بکنیم، مگر اینکه بخوایم این اشیاء توی نخ های مختلف، متفاوت باشن!).
خب حالا که با کلیت موضوع آشنا شدیم، قبل از ادامه بصورت کلی بگیم که این الگو هدفش اینه که دستاوردهایی مثل افزایش پرفورمنس و بازدهی، قابلیت استفاده مجدد از اشیا در صورت نیاز، و حتی اشتراک گذاریِ اشیا ایجاد شده حین اجرا رو بهمون بده.
در ادامه اول یه نمودار دیاگرام میبینیم، بعد یه مثال ساده از پیاده سازیِ این الگو رو بررسی میکنیم، بعدش با یه مثال یکم بهتر آشنا میشیم.
خب بریم سراغ پیاده سازی، معمولاً پیاده سازی این الگو، با uml دیاگرام پایین انجام میشه:
توضیح مختصر دیاگرام بالا اینه که به کمک getInstance یه اینستنس از استخرمون میگیریم، با صدا زدنِ acquireReusable یه شئ میسازیم و میندازیمش داخل استخر، با استفاده از releaseReusable یه شئ از استخر رو میتونیم حذف کنیم، و در صورت نیاز میتونیم توابع کنترلی دیگه ای هم داشته باشیم مثل setMaxPoolSize (همونطور که از اسمش پیداس سایز حدأکثر استخرمون رو مشخص میکنه).
طبیعتاً این دیاگرام، کلیتِ ساختار و سازوکار رو نشون میده و توی پیاده سازی میشه توابع مختلف یا نام های دیگه ای رو در نظر گرفت.
بیاین یه مثال دیگه ببینیم تا بهتر متوجه این الگو بشیم:
import java.util.HashSet;
import java.util.Set;
abstract class ObjectPool<T> {
private final Set<T> available = new HashSet<>();
private final Set<T> inUse = new HashSet<>();
protected abstract T create();
public synchronized T checkOut() {
if (available.isEmpty()) {
available.add(create());
}
T instance = available.iterator().next();
available.remove(instance);
inUse.add(instance);
return instance;
}
public synchronized void checkIn(T instance) {
inUse.remove(instance);
available.add(instance);
}
@Override
public synchronized String toString() {
return String.format("Pool available=%d inUse=%d", available.size(), inUse.size());
}
}
این کلاس دو تا آرایه به اسامی available و inUse داره، اولی اشیائی که در دسترس هستن رو نگه میداره، و دومی اشیائی که در حال استفاده هستن. تابع abstract به اسم create هم برای ایجاد شئ جدید هست. توابع checkIn و checkOut هم رفتاری مشابه توابع acquireReusable و releaseReusable توی مثال دیاگرام قبلی دارن با این تفاوت که checkIn شئ رو اگه داخل inUse باشه ازش حذف و داخل available اضافه میکنه، و تابع checkOut یک شئ از داخل available حذف و برمیگردونه (اگه شئ ای نباشه یدونه جدید میسازه) و اون رو به inUse اضافه میکنه تا مشخص باشه که شئ در حال استفاده هست. تابع toString هم که جزئیات استخرمون رو نشون میده. دقت کنیم که از synchronized استفاده شده که توی استفاده ی چندنخی مشکلی پیش نیاد.
خب حالا که استخرمون رو ساختیم، میتونیم بصورت زیر یه استخر دلخواه از یه شئ داشته باشیم:
public class MyPool extends ObjectPool<ObjectClassName> {
@Override
protected ObjectClassName create() {
return new ObjectClassName();
}
}
طبیعتاً ObjectClassName یه کلاسی هست که خودتون پیاده سازی کردین و هر کلاسی میتونه باشه. براث مثال کلاس زیر:
class ObjectClassName{
void sayHello(){
System.out.println("Hello");
}
}
نکته ی کلاس MyPool اینه که هر بار یه ObjectClassName جدید ایجاد میکنه و در صورت لزوم به کمک الگوی سینگلتون میتونیم طوری پیاده سازی بکنیم که کلن یه استخر از یه شئ بیشتر نداشته باشیم (دقت کنیم که این کلاس چون از کلاس ObjectPool ارثبری کرده باید تابع create رو پیاده سازی کنه تا یه شئِ استخر برگردونه و مدیریتش با خودمونه که چطور باشه عملکردش).
بصورت زیر هم میتونیم از این استخر استفاده کنیم:
MyPool pool = new MyPool();
System.out.println(pool.toString());
ObjectClassName p1 = pool.checkOut();
System.out.println(pool.toString());
ObjectClassName p2 = pool.checkOut();
System.out.println(pool.toString());
pool.checkIn(p1);
System.out.println(pool.toString());
خروجی کد بالا هم جیزی شبیه این خواهد بود:
Pool available=0 inUse=0
Pool available=0 inUse=1
Pool available=0 inUse=2
Pool available=1 inUse=1
خب حالا که تا اینجا با کلیتِ این الگو آشنا شدیم و فهمیدیم که به چه دردی میخوره، و یه مثال ساده ازش دیدیم. بریم سراغ یه مثال یکم پیشرفته تر. اول کلاس رو ببینیم و بعد بریم سراغ توضیحش:
abstract class ObjectPool <T> {
long deadTime;
Hashtable <T, Long> lock, unlock;
ObjectPool() {
deadTime = 60000; // 1 Minute ( 60 seconds )
lock = new Hashtable <T, Long>();
unlock = new Hashtable <T, Long>();
}
abstract T create();
abstract boolean validate(T o);
abstract void dead(T o);
synchronized T takeOut() {
long now = System.currentTimeMillis();
T t;
if (unlock.size() > 0) {
Enumeration <T> e = unlock.keys();
while (e.hasMoreElements()) {
t = e.nextElement();
if ((now - unlock.get(t)) > deadTime) {
// object has deadd
unlock.remove(t);
dead(t);
t = null;
}
else {
if (validate(t)) {
unlock.remove(t);
lock.put(t, now);
return (t);
}
else {
// object failed validation
unlock.remove(t);
dead(t);
t = null;
}
}
}
}
// no objects available, create a new one
t = create();
lock.put(t, now);
return (t);
}
synchronized void takeIn(T t) {
lock.remove(t);
unlock.put(t, System.currentTimeMillis());
}
}
خب شاید این کد توی دید اول یکم گنگ باشه، ولی چیز خاصی نداره. کلاسِ ObjectPool اول از همه یه deadTime داریم که زمان از بین رفتن رو مشخص میکنه، دو تا Hashtable داریم که مشابه available و inUse توی مثال قبل عمل میکنن (شامل دو مقدار هستن این هش تیبل ها، یکی شئ، دومی زمان انقضاش یا همون وقتی که بعد از اون عملاً اکسپایر میشه). تابع create هم که دقیقاً مثل مثال قبل هست. توابع validate و dead توسط کلاس هایی که میخوان استخر باشن باید پیاده سازی بشه با توجه به ماهیت خودشون (validate برای اعتبارسنجی و dead برای اطلاع رسانی از منقضی شدنِ شئ به کار میره).
طولانی ترین تابع این مثال، takeOut هست. این تابع اگه شئ ای موجود نباشه، یه شئ جدید ایجاد میکنه، و در صورتی که شئ موجود باشه، به کمک خط زیر
(now - unlock.get(t)) > deadTime
بررسی میکنیم که شئ منقضی شده یا نه،اگه منقضی شده بود، شئ از تیبلِ unlock حذف میشه و تابع dead هم فراخوانی میشه تا به کلاس زیریِ خودش اطلاع بده که این شئ منقضی شده. اگه منقضی نشده باشه هم اول به کمک فراخوانی تابع validate اطمینان حاصل کنه که شئ، توسط کلاسِ زیری (منظور کلاسی هست که از این کلاس ارثبری کرده یا همون فرزندش) تأیید بشه، در صورت عدم تأیید رفتاری مشابه زمانی که منقضی شده طی میشه، و در صورت تأیید شده، شئ با موفقیت برگردونده میشه.
تابع بعدی هم takeIn هست که به راحتی هرچه تمام، اگه شئ توی lock موجود باشه، از این تیبل حذف میشه و به تیبل unlock اضافه میشه.
خب تا اینجا کلاس اصلیمون (استخر) رو ایجاد کردیم. الان وقتِ اون رسیده که یه استخر برای شئ ای که میخوایم ایجاد کنیم. اینجا برای مثال ارتباط با دیتایس رو در نظر میگیریم:
class JDBCConnectionPool extends ObjectPool<Connection> {
String dsn, usr, pwd;
JDBCConnectionPool(String driver, String dsn, String usr, String pwd) {
super();
try {
Class.forName(driver).newInstance();
}
catch(Exception e) {
e.printStackTrace();
}
this.dsn = dsn;
this.usr = usr;
this.pwd = pwd;
}
Connection create() {
try {
return (DriverManager.getConnection(dsn, usr, pwd));
}
catch(SQLException e) {
e.printStackTrace();
return (null);
}
}
void dead(Connection o) {
try { ((Connection) o).close();
}
catch(SQLException e) {
e.printStackTrace();
}
}
boolean validate(Connection o) {
try {
return (! ((Connection) o).isClosed());
}
catch(SQLException e) {
e.printStackTrace();
return (false);
}
}
}
کلاسِ JDBCConnectionPool چون از ObjectPool ارثبری کرده، باید توابع create و dead و validate رو پیاده سازی بکنه. توی این مثال، تابع create یه کانکشن بر میگردونه، تابع dead کانکشن رو میبنده (زمانی صدا زده میشه که از طرف کلاس ObjectPool شئ منقضی شده باشه)، و تابع validate هم چک میکنه که کانکشن بسته نشده باشه. تابع سازنده JDBCConnectionPool هم که اطلاعات مورد نیاز برای ایجاد اینستنس جدید برای کانکش زدن به دیتابیس رو میگیره.
خب برای تست کد بالا، میتونیم به شکل زیر عمل کنیم:
// Create the ConnectionPool:
JDBCConnectionPool pool = new JDBCConnectionPool(
"org.hsqldb.jdbcDriver", "jdbc:hsqldb: //localhost/mydb", "sa", "password");
ما یه شئ از JDBCConnectionPool ایجاد کردیم، به کمک دستور زیر:
// Get a connection:
Connection con = pool.takeOut();
میتونیم یه کانکشن از دیتابیسمون بگیریم. و به کمک دستور زیر:
// Return the connection:
pool.takeIn(con);
میتونیم کانکش رو به استخر برگردونیم.
خب این الگو رو هم بررسی کردیم، طبیعتاً کد زدن و تغییر دادن لازمه ی یادگیری هست. برای نمونه توی مثال آخر، میتونیم زمانِ اکسپایر شدن رو توی تابع ورودی بدیم که هر استخر بتونه تایم اوتِ خودش برای اشیاءِ درونش رو داشته باشه، یا حتی طوری پیاده سازی کنیم که هر شئ تایم اوتِ خودش رو داشته باشه. یا مثلاً میتونیم برای اشیائمون تگ یا شناسه (id) در نظر بگیریم که برای دسترسی بهشون گاهی اوقات پرفورمنسِ بهتری داشته باشیم.
خلاصه این که کد بزنیم، تمرین کنیم، سعی کنیم از الگوهای استاندارد و تمیز تر استفاده کنیم. تمیز کد زدن حتی اگه وقتِ بیشتری ازمون بگیره، از بیشتر کد زدن توی زمان کمتر خیلی بهتره.
منتشر شده در ویرگول توسط محمد قدسیان https://virgool.io/@mohammad.ghodsian
مطلبی دیگر از این انتشارات
گیتلب و CI/CD: یک راهنمای ساده - قسمت اول
مطلبی دیگر از این انتشارات
بهترین راه بهبود مهارت برنامه نویسی با سایت ها
مطلبی دیگر از این انتشارات
9 فریمورک برتر برای ساخت اپلیکیشن اندرویدی