حالت تاریک
خمشان یا بشکن
زندگی جا نمیزند.
کدی که مینویسیم هم نمیتواند. برای همگام ماندن با ریتم تقریباً دیوانهوار تغییر امروز، باید هر تلاشی کنیم کدی بنویسیم که تا حد ممکن شل — انعطافپذیر — باشد. وگرنه ممکن است ببینیم کدمان بهسرعت کهنه میشود، یا آنقدر شکننده میشود که نتوان آن را تعمیر کرد، و در نهایت در هجوم دیوانهوار به سوی آینده عقب بمانیم.
در قابلیت بازگشت (صفحه ۴۴) درباره خطرات تصمیمهای غیرقابل بازگشت صحبت کردیم. در این فصل میگوییم چگونه تصمیمهای قابل بازگشت بگیرید تا کدتان در برابر دنیای نامطمئن انعطافپذیر و سازگار بماند.
ابتدا باید به جفتشدگی — وابستگیهای میان ماژولهای کد — نگاه کنیم. در جداسازی و قانون دمتر نشان میدهیم چگونه مفاهیم جدا را جدا نگه دارید و جفتشدگی را کاهش دهید.
راه خوبی برای انعطافپذیر ماندن این است که کمتر کد بنویسید. تغییر کد شما را در معرض احتمال معرفی باگهای جدید قرار میدهد. متاپروگرامنویسی توضیح میدهد چگونه جزئیات را کاملاً از کد بیرون ببرید، جایی که میتوان آنها را ایمنتر و آسانتر تغییر داد.
در جفتشدگی زمانی، دو جنبه زمان را در ارتباط با جفتشدگی بررسی میکنیم. آیا به «تیک» قبل از «تاک» وابستهاید؟ نه اگر میخواهید انعطافپذیر بمانید.
مفهوم کلیدی در ساخت کد انعطافپذیر، جداسازی مدل داده از نما، یا ارائه، آن مدل است. در فقط یک نماست مدلها را از نماها جدا میکنیم.
در نهایت، تکنیکی برای جداسازی بیشتر ماژولها با فراهم کردن محلی برای تبادل ناشناس و ناهمگام داده وجود دارد. موضوع تختهسیاهها همین است.
با این تکنیکها میتوانید کدی بنویسید که «با ضربهها همراه شود».
۲۶ — جداسازی و قانون دمتر
دیوار خوب، همسایه خوب. — رابرت فراست، «دیوار مرمت»
در ارتگونالیتی (صفحه ۳۴) و طراحی قراردادمحور (صفحه ۱۰۹) پیشنهاد کردیم نوشتن کد «خجالتی» مفید است. اما «خجالتی» دو طرفه است: خودتان را به دیگران نشان ندهید، و با افراد زیاد تعامل نکنید.
جاسوسها، مخالفان، انقلابیون و مانند آنها اغلب در گروههای کوچکی به نام سلول سازماندهی میشوند. اگرچه افراد هر سلول ممکن است یکدیگر را بشناسند، از کسانی در سلولهای دیگر خبر ندارند. اگر یک سلول کشف شود، هیچ مقدار سرم حقیقتیاب نام دیگران خارج از سلول را فاش نمیکند. حذف تعامل بین سلولها از همه محافظت میکند.
احساس میکنیم این اصل خوبی برای کدنویسی هم است. کد را در سلولها (ماژولها) سازماندهی کنید و تعامل بین آنها را محدود کنید. اگر یک ماژول به خطر بیفتد و باید جایگزین شود، ماژولهای دیگر باید بتوانند کار را ادامه دهند.
کمینه کردن جفتشدگی
مشکل داشتن ماژولهایی که از یکدیگر خبر دارند چیست؟ در اصل هیچ — لازم نیست به اندازه جاسوسها یا مخالفان پارانوئید باشیم. با این حال باید مراقب باشید با چند ماژول دیگر تعامل دارید و مهمتر، چگونه به این تعامل رسیدهاید.
فرض کنید خانهتان را بازسازی میکنید یا از صفر میسازید. ترتیب معمول شامل «پیمانکار عمومی» است. پیمانکار را استخدام میکنید تا کار انجام شود، اما پیمانکار ممکن است شخصاً ساخت را انجام ندهد؛ کار ممکن است به پیمانکاران فرعی مختلف واگذار شود. اما بهعنوان مشتری، مستقیماً با پیمانکاران فرعی سر و کار ندارید — پیمانکار عمومی آن مجموعه دردسرها را از طرف شما بر عهده میگیرد.
میخواهیم همین الگو را در نرمافزار دنبال کنیم. وقتی از شیئی خدمت خاصی میخواهیم، میخواهیم خدمت از طرف ما انجام شود. نمیخواهیم شیء شیء شخص ثالثی بدهد که برای گرفتن خدمت مورد نیاز باید با آن سر و کار داشته باشیم.
مثلاً فرض کنید کلاسی مینویسید که نموداری از دادههای ضبطکننده علمی تولید میکند. ضبطکنندههای داده در سراسر جهان پخشاند؛ هر شیء ضبطکننده شامل شیء مکانی است که موقعیت و منطقه زمانی را میدهد. میخواهید کاربران ضبطکنندهای انتخاب کنند و دادهاش را با منطقه زمانی درست رسم کنید. ممکن است بنویسید:
java
public void plotDate(Date aDate, Selection aSelection) {
TimeZone tz =
aSelection.getRecorder().getLocation().getTimeZone();
...
}اما حالا روتین رسم بهطور غیرضروری به سه کلاس — Selection، Recorder و Location — جفت شده است. این سبک کدنویسی تعداد کلاسهایی که کلاس ما به آنها وابسته است را بهشدت افزایش میدهد. چرا بد است؟ خطر را افزایش میدهد که تغییر نامرتبطی جای دیگر سیستم روی کد شما اثر بگذارد. مثلاً اگر فردی Location را طوری تغییر دهد که دیگر مستقیماً TimeZone نداشته باشد، شما هم باید کدتان را عوض کنید.
بهجای خودتان کندن در سلسلهمراتب، مستقیماً آنچه نیاز دارید بخواهید:
java
public void plotDate(Date aDate, TimeZone aTz) {
...
}
plotDate(someDate, someSelection.getTimeZone());متدی به Selection اضافه کردیم تا منطقه زمانی را از طرف ما بگیرد: روتین رسم مهم نیست منطقه زمانی مستقیماً از Recorder بیاید، از شیء محتوایی درون Recorder، یا Selection منطقه زمانی کاملاً متفاوتی بسازد. روتین انتخاب هم بهتر است فقط از ضبطکننده منطقه زمانی بخواهد و به خود ضبطکننده واگذار کند آن را از شیء Location محتوایی بگیرد.
عبور مستقیم از روابط بین اشیاء میتواند بهسرعت به انفجار ترکیبی¹ روابط وابستگی منجر شود. علائم این پدیده را به روشهای مختلف میبینید:
- پروژههای بزرگ C یا C++ که دستور لینک یک تست واحد از خود برنامه تست طولانیتر است
- تغییرات «ساده» در یک ماژول که در ماژولهای نامرتبط سیستم پخش میشود
- توسعهدهندگانی که از تغییر کد میترسند چون مطمئن نیستند چه چیزی تحت تأثیر قرار میگیرد
سیستمهایی با وابستگیهای غیرضروری زیاد بسیار سخت (و پرهزینه) نگهداری میشوند و تمایل به ناپایداری بالا دارند. برای کمینه نگه داشتن وابستگیها، از قانون دمتر برای طراحی متدها و توابع استفاده میکنیم.
یادداشت ۱: اگر همه اشیاء از یکدیگر خبر داشته باشند، تغییر در یک شیء میتواند باعث نیاز به تغییر اشیاء دیگر شود.
قانون دمتر برای توابع
قانون دمتر برای توابع [LH89] تلاش میکند جفتشدگی بین ماژولها در هر برنامهای را کمینه کند. سعی میکند مانع شود به درون شیئی بروید تا به متدهای شیء سوم دسترسی پیدا کنید. قانون در شکل ۵.۱ خلاصه شده است.
شکل ۵.۱. قانون دمتر برای توابع
هر متد شیء فقط باید متدهای متعلق به موارد زیر را فراخوانی کند:
- خودش
- هر پارامتری که به متد داده شده
- هر شیئی که ساخته
- هر شیء مؤلفهای که مستقیماً نگه داشته
cppclass Demeter { private: A *a; int func(); public: //... void example(B& b); } void Demeter::example(B& b) { C c; int f = func(); // خودش b.invert(); // پارامتر ورودی a = new A(); a->setActive(); // شیء ساختهشده c.print(); // مؤلفه مستقیم }
با نوشتن کد «خجالتی» که تا حد ممکن قانون دمتر را رعایت میکند، به هدفمان میرسیم:
TIP 36
جفتشدگی بین ماژولها را کمینه کنید
آیا واقعاً تفاوت میسازد؟
اگرچه در تئوری خوب به نظر میرسد، آیا رعایت قانون دمتر واقعاً به کد قابلنگهداریتر کمک میکند؟
مطالعات نشان داده [BBM96] کلاسهای C++ با مجموعه پاسخ بزرگتر بیشتر مستعد خطا هستند تا کلاسهایی با مجموعه پاسخ کوچکتر (مجموعه پاسخ تعداد توابعی است که مستقیماً توسط متدهای کلاس فراخوانی میشوند).
چون رعایت قانون دمتر اندازه مجموعه پاسخ در کلاس فراخواننده را کاهش میدهد، کلاسهایی که اینگونه طراحی شدهاند تمایل به خطای کمتر دارند ([URL 56] را برای مقالات و اطلاعات بیشتر درباره پروژه Demeter ببینید).
استفاده از قانون دمتر کد را سازگارتر و مقاومتر میکند، اما بهایی دارد: بهعنوان «پیمانکار عمومی»، ماژول شما باید همه پیمانکاران فرعی را مستقیماً واگذاری و مدیریت کند، بدون درگیر کردن مشتریان ماژول. در عمل یعنی تعداد زیادی متد wrapper مینویسید که فقط درخواست را به نماینده هدایت میکنند. این متدهای wrapper هم هزینه زمان اجرا و هم سربار فضایی دارند که ممکن است قابل توجه — حتی ممنوعکننده — در برخی کاربردها باشد.
مثل هر تکنیک، باید مزایا و معایب را برای کاربرد خاص خود متوازن کنید. در طراحی طرح پایگاه داده، «غیرنرمالسازی» طرح برای بهبود کارایی رایج است: نقض قوانین نرمالسازی در ازای سرعت.
جداسازی فیزیکی
در این بخش عمدتاً به طراحی برای نگه داشتن منطقی جدا بودن چیزها درون سیستمها میپردازیم. با این حال نوع دیگری از وابستگی متقابل وجود دارد که با بزرگتر شدن سیستمها بسیار مهم میشود. در کتاب طراحی نرمافزار C++ در مقیاس بزرگ [Lak96]، جان لاکوس مسائل مربوط به روابط میان فایلها، پوشهها و کتابخانههایی که سیستم را میسازند را بررسی میکند. پروژههای بزرگی که این مسائل طراحی فیزیکی را نادیده میگیرند با چرخههای build اندازهگیریشده به روز و تستهای واحدی که ممکن است کل سیستم را بهعنوان کد پشتیبان بکشند، مواجه میشوند. آقای لاکوس قانعکننده استدلال میکند طراحی منطقی و فیزیکی باید همزمان پیش برود — واگرداندن آسیب واردشده به بدنه بزرگ کد توسط وابستگیهای حلقوی بسیار دشوار است. اگر در توسعههای مقیاس بزرگ هستید این کتاب را توصیه میکنیم، حتی اگر C++ زبان پیادهسازی شما نباشد.
معامله مشابهی اینجا هم ممکن است. در واقع با معکوس کردن قانون دمتر و جفتکردن محکم چند ماژول، ممکن است بهبود کارایی مهمی به دست آورید. تا وقتی شناختهشده و قابل قبول است که آن ماژولها جفت شدهاند، طراحی شما خوب است.
وگرنه ممکن است خود را در جادهای به سوی آیندهای شکننده و غیرانعطافپذیر — یا اصلاً بدون آینده — بیابید.
بخشهای مرتبط:
- ارتگونالیتی، صفحه ۳۴
- قابلیت بازگشت، صفحه ۴۴
- طراحی قراردادمحور، صفحه ۱۰۹
- چگونه منابع را متوازن کنیم، صفحه ۱۲۹
- فقط یک نماست، صفحه ۱۵۷
- تیمهای عملگرا، صفحه ۲۲۴
- تست بیرحمانه، صفحه ۲۳۷
Challenges
بحث کردیم چگونه استفاده از واگذاری رعایت قانون دمتر و در نتیجه کاهش جفتشدگی را آسانتر میکند. با این حال نوشتن همه متدهای لازم برای هدایت فراخوانیها به کلاسهای واگذارشده خستهکننده و مستعد خطاست. مزایا و معایب نوشتن پیشپردازندهای که این فراخوانیها را خودکار تولید کند چیست؟ آیا این پیشپردازنده فقط یکبار اجرا شود یا بخشی از build باشد؟
Exercises
۲۴. مفهوم جداسازی فیزیکی را در جعبه صفحه مقابل بحث کردیم. کدام یک از فایلهای هدر C++ زیر به بقیه سیستم محکمتر جفت شده است؟ پاسخ در ص. ۲۹۳
person1.h:
cpp
#include "date.h"
class Person1 {
private:
Date myBirthdate;
public:
Person1(Date &birthDate);
// ...person2.h:
cpp
class Date;
class Person2 {
private:
Date *myBirthdate;
public:
Person2(Date &birthDate);
// ...۲۵. برای مثال زیر و مثالهای تمرینهای ۲۶ و ۲۷، مشخص کنید آیا فراخوانیهای متد نشاندادهشده طبق قانون دمتر مجازند. این اولی در Java است. پاسخ در ص. ۲۹۳
java
public void showBalance(BankAccount acct) {
Money amt = acct.getBalance();
printToScreen(amt.printFormat());
}۲۶. این مثال هم در Java است. پاسخ در ص. ۲۹۴
java
public class Colada {
private Blender myBlender;
private Vector myStuff;
public Colada() {
myBlender = new Blender();
myStuff = new Vector();
}
private void doSomething() {
myBlender.addIngredients(myStuff.elements());
}
}۲۷. این مثال در C++ است. پاسخ در ص. ۲۹۴
cpp
void processTransaction(BankAccount acct, int) {
Person *who;
Money amt;
amt.setValue(123.45);
acct.setBalance(amt);
who = acct.getOwner();
markWorkflow(who->name(), SET_BALANCE);
}۲۷ — متاپروگرامنویسی
هیچ مقدار نبوغ نمیتواند بر وسواس به جزئیات غلبه کند. — قانون هشتم لوی
جزئیات کد تمیز ما را بههم میریزند — بهویژه اگر مرتب عوض شوند. هر بار برای سازگاری با تغییر منطق کسبوکار، یا قانون، یا سلیقه روزانه مدیریت، باید کد را عوض کنیم، در معرض شکستن سیستم — معرفی باگ جدید — هستیم.
پس میگوییم «جزئیات بیرون!» آنها را از کد بیرون ببرید. در همین حال میتوانیم کد را بسیار قابل پیکربندی و «نرم» — یعنی بهراحتی سازگار با تغییر — کنیم.
پیکربندی پویا
ابتدا میخواهیم سیستمها را بسیار قابل پیکربندی کنیم. نه فقط چیزهایی مثل رنگ صفحه و متن اعلان، بلکه موارد عمیقاً نهادینهشده مثل انتخاب الگوریتمها، محصولات پایگاه داده، فناوری میانافزار و سبک رابط کاربری. این موارد باید بهعنوان گزینههای پیکربندی پیاده شوند، نه از طریق یکپارچهسازی یا مهندسی.
TIP 37
پیکربندی کنید، یکپارچه نکنید
از متادیتا برای توصیف گزینههای پیکربندی برنامه استفاده کنید: پارامترهای تنظیم، ترجیحات کاربر، پوشه نصب و مانند آن.
متادیتا دقیقاً چیست؟ بهطور دقیق، متادیتا داده درباره داده است. شاید رایجترین مثال طرح پایگاه داده یا فرهنگ داده باشد. طرح شامل دادهای است که فیلدها (ستونها) را از نظر نام، طول ذخیرهسازی و سایر ویژگیها توصیف میکند. باید بتوانید به این اطلاعات مثل هر داده دیگری در پایگاه داده دسترسی و آن را دستکاری کنید.
اصطلاح را در گستردهترین معنا به کار میبریم. متادیتا هر دادهای است که برنامه را توصیف میکند — چگونه باید اجرا شود، چه منابعی باید مصرف کند و مانند آن. معمولاً متادیتا در زمان اجرا دسترسی و استفاده میشود، نه زمان کامپایل. مدام از متادیتا استفاده میکنید — حداقل برنامههایتان. فرض کنید گزینهای میزنید تا نوار ابزار مرورگر وب پنهان شود. مرورگر آن ترجیح را بهعنوان متادیتا در نوعی پایگاه داده داخلی ذخیره میکند.
این پایگاه داده ممکن است قالب اختصاصی داشته باشد یا از مکانیزم استاندارد استفاده کند. در ویندوز، فایل مقداردهی اولیه (با پسوند .ini) یا ورودیهای رجیستری سیستم معمول است. در یونیکس، X Window System با فایلهای Application Default عملکرد مشابه میدهد. Java از فایل Property استفاده میکند. در همه این محیطها کلیدی برای گرفتن مقدار مشخص میکنید. یا پیادهسازیهای قویتر و انعطافپذیرتر متادیتا از زبان اسکریپت جاسازیشده استفاده میکنند (زبانهای دامنه، صفحه ۵۷ را ببینید).
مرورگر Netscape در واقع ترجیحات را با هر دو تکنیک پیاده کرده است. در نسخه ۳، ترجیحات بهصورت جفت کلید/مقدار ساده ذخیره میشد:
SHOW_TOOLBAR: Falseبعداً، ترجیحات نسخه ۴ بیشتر شبیه JavaScript بود:
javascript
user_pref("custtoolbar.Browser.Navigation_Toolbar.open", false);برنامههای متادیتامحور
اما میخواهیم فراتر از متادیتا برای ترجیحات ساده برویم. میخواهیم برنامه را تا حد امکان از طریق متادیتا پیکربندی و هدایت کنیم. هدف ما تفکر اعلانی (مشخص کردن چه انجام شود، نه چگونه) و ساخت برنامههای بسیار پویا و سازگار است. با پذیرش قاعده کلی این کار را میکنیم: برای حالت عمومی برنامهنویسی کنید و جزئیات را جای دیگری بگذارید — خارج از پایگاه کد کامپایلشده.
TIP 38
انتزاعها را در کد بگذارید، جزئیات را در متادیتا
چند مزیت برای این رویکرد وجود دارد:
- شما را مجبور میکند طراحی را جدا کنید که برنامه انعطافپذیرتر و سازگارتر میشود.
- شما را مجبور میکند طراحی مقاومتر و انتزاعیتر بسازید با به تعویق انداختن جزئیات — تا بیرون از برنامه.
- میتوانید برنامه را بدون کامپایل مجدد سفارشی کنید. میتوانید از این سطح سفارشیسازی برای راهحلهای موقت آسان باگهای بحرانی در سیستمهای تولید زنده هم استفاده کنید.
- متادیتا میتواند به شکلی بسیار نزدیکتر به دامنه مسئله از زبان برنامهنویسی عمومی بیان شود (زبانهای دامنه، صفحه ۵۷ را ببینید).
- شاید بتوانید چند پروژه مختلف را با همان موتور برنامه و متادیتای متفاوت پیاده کنید.
میخواهیم تعریف بیشتر جزئیات را تا آخرین لحظه به تعویق بیندازیم و جزئیات را تا حد ممکن نرم — یعنی آسان برای تغییر — نگه داریم. با ساخت راهحلی که تغییرات سریع را ممکن کند، شانس بهتری برای مقابله با سیل تغییر جهتگیری که بسیاری پروژهها را غرق میکند داریم (قابلیت بازگشت، صفحه ۴۴ را ببینید).
منطق کسبوکار
پس انتخاب موتور پایگاه داده را گزینه پیکربندی کردید و متادیتا برای تعیین سبک رابط کاربری فراهم کردید. میتوانیم بیشتر کنیم؟ قطعاً.
چون سیاست و قوانین کسبوکار بیشتر از هر جنبه دیگری پروژه احتمال تغییر دارند، منطقی است آنها را در قالبی بسیار انعطافپذیر نگه داریم.
مثلاً برنامه خرید شما ممکن است سیاستهای شرکتی مختلفی داشته باشد. شاید به تأمینکنندگان کوچک در ۴۵ روز و بزرگ در ۹۰ روز پرداخت کنید. تعریف انواع تأمینکننده و خود بازههای زمانی را قابل پیکربندی کنید. از فرصت تعمیم استفاده کنید.
شاید سیستمی با نیازمندیهای گردشکار وحشتناک مینویسید. اقدامات طبق قوانین کسبوکار پیچیده (و متغیر) شروع و متوقف میشوند. آنها را در نوعی سیستم مبتنی بر قانون (یا خبره) جاسازیشده در برنامه رمزگذاری کنید. اینطور با نوشتن قانون پیکربندی میکنید، نه برش کد.
منطق کمپیچیدهتر را میتوان با زبان کوچک بیان کرد و نیاز به کامپایل و استقرار مجدد هنگام تغییر محیط را حذف کرد. صفحه ۵۸ را برای مثال ببینید.
چه زمانی پیکربندی کنیم
همانطور که در قدرت متن ساده (صفحه ۷۳) گفتیم، توصیه میکنیم متادیتای پیکربندی را بهصورت متن ساده بیان کنید — زندگی را آسانتر میکند.
اما برنامه چه زمانی این پیکربندی را بخواند؟ بسیاری از برنامهها چنین چیزها را فقط در راهاندازی اسکن میکنند که بد است. اگر باید پیکربندی را عوض کنید، مجبورید برنامه را دوباره راه بیندازید. رویکرد انعطافپذیرتر نوشتن برنامههایی است که بتوانند پیکربندی را در حین اجرا دوباره بارگذاری کنند. این انعطافپذیری هزینه دارد: پیادهسازی پیچیدهتر است.
پس در نظر بگیرید برنامه چگونه استفاده میشود: اگر فرایند سرور بلندمدت است، راهی برای خواندن مجدد و اعمال متادیتا در حین اجرا بدهید. برای برنامه GUI کوچک مشتری که سریع دوباره راه میافتد، شاید لازم نباشد.
این پدیده محدود به کد برنامه نیست. همه ما از سیستمعاملهایی که برای نصب برنامه ساده یا تغییر پارامتر بیضرر مجبورمان به راهاندازی مجدد میکنند آزردهایم.
مثال: Enterprise Java Beans
Enterprise Java Beans (EJB) چارچوبی برای سادهسازی برنامهنویسی در محیط توزیعشده مبتنی بر تراکنش است. اینجا ذکر میکنیم چون EJB نشان میدهد چگونه متادیتا هم برای پیکربندی برنامه و هم برای کاهش پیچیدگی نوشتن کد به کار میرود.
فرض کنید میخواهید نرمافزار Java بسازید که در تراکنشها بین ماشینهای مختلف، بین فروشندگان پایگاه داده مختلف و با مدلهای نخ و توازن بار مختلف شرکت کند.
خبر خوب این است که لازم نیست نگران همه آنها باشید. یک bean — شیء خودکفا که قراردادهای خاصی را دنبال میکند — مینویسید و در ظرف bean قرار میدهید که بخش زیادی از جزئیات سطح پایین را از طرف شما مدیریت میکند. میتوانید کد bean را بدون عملیات تراکنش یا مدیریت نخ بنویسید؛ EJB از متادیتا برای مشخص کردن نحوه مدیریت تراکنشها استفاده میکند.
تخصیص نخ و توازن بار بهعنوان متادیتا به سرویس تراکنش زیرین که ظرف استفاده میکند مشخص میشود. این جداسازی انعطافپذیری زیادی برای پیکربندی پویای محیط در زمان اجرا میدهد.
ظرف bean میتواند تراکنشها را از طرف bean به چند سبک مختلف مدیریت کند (از جمله گزینهای که خودتان commit و rollback را کنترل کنید). همه پارامترهایی که رفتار bean را تحت تأثیر قرار میدهند در توصیفگر استقرار bean — شیء سریالشدهای که متادیتای لازم را دارد — مشخص میشوند.
سیستمهای توزیعشده مثل EJB راه را به دنیای جدید سیستمهای پویا و قابل پیکربندی هدایت میکنند.
پیکربندی همکارانه
درباره پیکربندی برنامههای پویا توسط کاربران و توسعهدهندگان صحبت کردیم. اما اگر بگذارید برنامهها یکدیگر را پیکربندی کنند — نرمافزاری که خود را با محیطش سازگار کند؟ پیکربندی برنامهریزینشده و لحظهای نرمافزار موجود مفهوم قدرتمندی است.
سیستمعاملها هنگام راهاندازی خود را با سختافزار پیکربندی میکنند و مرورگرهای وب خود را با مؤلفههای جدید بهطور خودکار بهروز میکنند.
برنامههای بزرگتر شما احتمالاً الان با نسخههای مختلف داده و انتشارهای مختلف کتابخانه و سیستمعامل مسئله دارند. شاید رویکرد پویاتر کمک کند.
کد دودو ننویسید
بدون متادیتا، کد به اندازه ممکن سازگار و انعطافپذیر نیست. آیا بد است؟ در دنیای واقعی، گونههایی که سازگار نمیشوند میمیرند.
دودو به حضور انسانها و دام آنها در جزیره موریس سازگار نشد و بهسرعت منقرض شد.² اولین انقراض مستند گونه به دست انسان بود.
نگذارید پروژه (یا حرفه) شما سرنوشت دودو را پیدا کند.
یادداشت ۲: کمک نکرد که مستعمرهنشینان پرندگان آرام (بخوانید: احمق) را برای تفریح با چوب کشتند.
بخشهای مرتبط:
- ارتگونالیتی، صفحه ۳۴
- قابلیت بازگشت، صفحه ۴۴
- زبانهای دامنه، صفحه ۵۷
- قدرت متن ساده، صفحه ۷۳
Challenges
برای پروژه فعلیتان در نظر بگیرید چه بخشی از برنامه میتواند از خود برنامه به متادیتا منتقل شود. «موتور» حاصل چه شکلی میشد؟ آیا میتوانستید آن موتور را در زمینه برنامه دیگری بازاستفاده کنید؟
Exercises
۲۸. کدام یک از موارد زیر بهتر است درون برنامه بهعنوان کد باشد و کدام خارجاً بهعنوان متادیتا؟ پاسخ در ص. ۲۹۵
- تخصیص پورت ارتباطی
- پشتیبانی ویرایشگر از برجستهسازی نحو زبانهای مختلف
- پشتیبانی ویرایشگر از دستگاههای گرافیکی مختلف
- ماشین حالت برای parser یا scanner
- مقادیر و نتایج نمونه برای استفاده در تست واحد
۲۸ — جفتشدگی زمانی
جفتشدگی زمانی درباره چیست، ممکن است بپرسید. درباره زمان است.
زمان جنبهای اغلب نادیدهگرفتهشده در معماریهای نرمافزار است. تنها زمانی که ذهن ما را مشغول میکند زمان روی برنامه زمانبندی، زمان باقیمانده تا تحویل است — اما منظور ما این نیست. در عوض، درباره نقش زمان بهعنوان عنصر طراحی خود نرمافزار صحبت میکنیم. دو جنبه زمان برای ما مهم است: همزمانی (اتفاق همزمان چیزها) و ترتیب (موقعیت نسبی چیزها در زمان).
معمولاً با هیچکدام از این جنبهها به طراحی برنامهنویسی نزدیک نمیشویم. وقتی مردم برای اولین بار مینشینند معماری طراحی کنند یا برنامه بنویسند، چیزها خطی به نظر میرسند. اینگونه بیشتر مردم فکر میکنند — این را انجام بده و بعد همیشه آن. اما این تفکر به جفتشدگی زمانی منجر میشود: جفتشدگی در زمان. متد A همیشه باید قبل از متد B فراخوانی شود؛ فقط یک گزارش میتواند همزمان اجرا شود؛ باید منتظر بازرسم صفحه بمانید تا کلیک دکمه دریافت شود. تیک باید قبل از تاک بیاید.
این رویکرد خیلی انعطافپذیر و واقعگرایانه نیست.
باید برای همزمانی³ فضا بگذاریم و به جداسازی هر وابستگی زمانی یا ترتیبی فکر کنیم. با این کار انعطافپذیری میگیریم و وابستگیهای زمانی را در بسیاری از حوزههای توسعه کاهش میدهیم: تحلیل گردشکار، معماری، طراحی و استقرار.
یادداشت ۳: اینجا به جزئیات برنامهنویسی همزمان یا موازی نمیپردازیم؛ یک کتاب درسی علوم کامپیوتر خوب باید مبانی از جمله زمانبندی، بنبست، گرسنگی، حذف متقابل/سمافور و مانند آن را پوشش دهد.
گردشکار
در بسیاری پروژهها باید گردشکار کاربران را بهعنوان بخشی از تحلیل نیازمندیها مدل و تحلیل کنیم. میخواهیم بفهمیم چه چیزی میتواند همزمان اتفاق بیفتد و چه چیزی باید به ترتیب سخت رخ دهد. یکی از راهها ثبت توصیف گردشکار با نمادگذاری مثل نمودار فعالیت UML⁴ است.
یادداشت ۴: برای اطلاعات بیشتر درباره همه انواع نمودار UML، [FS97] را ببینید.
نمودار فعالیت شامل مجموعهای از اقدامات بهصورت جعبههای گرد است. پیکان خروج از اقدام به اقدام دیگر (که پس از اتمام اقدام اول میتواند شروع شود) یا به خط ضخیم به نام میله همگامسازی منتهی میشود. وقتی همه اقدامات ورودی به میله همگامسازی تمام شدند، میتوانید در امتداد پیکانهای خروجی از میله پیش بروید. اقدامی که پیکان ورودی ندارد میتواند هر زمان شروع شود.
میتوانید از نمودار فعالیت برای بیشینه کردن موازیسازی با شناسایی فعالیتهایی که میتوانستند موازی انجام شوند اما نمیشوند استفاده کنید.
TIP 39
گردشکار را برای بهبود همزمانی تحلیل کنید
مثلاً در پروژه مخلوطکن ما (تمرین ۱۷، صفحه ۱۱۹)، کاربران ممکن است ابتدا گردشکار فعلی را اینگونه توصیف کنند:
- باز کردن مخلوطکن
- باز کردن مخلوط پینا کولادا
- ریختن مخلوط در مخلوطکن
- اندازهگیری نیم فنجان رام سفید
- ریختن رام
- افزودن ۲ فنجان یخ
- بستن مخلوطکن
- مایعکردن به مدت ۲ دقیقه
- باز کردن مخلوطکن
- برداشتن لیوانها
- برداشتن چترهای صورتی
- سرو کردن
اگرچه این اقدامات را پشتسرهم توصیف میکنند و شاید پشتسرهم انجام دهند، میبینیم بسیاری میتوانند موازی انجام شوند، همانطور که در نمودار فعالیت شکل ۵.۲ نشان دادهایم.
میتواند چشمگشا باشد ببینید وابستگیها واقعاً کجاست. در این مورد، وظایف سطح بالا (۱، ۲، ۴، ۱۰ و ۱۱) میتوانند همزمان و از ابتدا انجام شوند. وظایف ۳، ۵ و ۶ بعداً میتوانند موازی انجام شوند.
اگر در مسابقه پینا کولادا بودید، این بهینهسازیها میتواند همه تفاوت را بسازد.
شکل ۵.۲. نمودار فعالیت UML: درست کردن پینا کولادا
2. باز کردن مخلوط ──┐ 1. باز کردن مخلوطکن ──┐ 4. اندازهگیری رام ──┐ │ │ │ ▼ ▼ ▼ 3. ریختن مخلوط 6. افزودن ۲ فنجان یخ 5. ریختن رام │ │ │ └────────────┬───────────────┴────────────────────────┘ ▼ 7. بستن مخلوطکن │ 11. برداشتن چترهای صورتی ──┐ 10. برداشتن لیوانها ──┐ │ │ ▼ ▼ 8. مایعکردن ۲ دقیقه │ ▼ 9. باز کردن مخلوطکن │ ▼ 12. سرو کردن
معماری
چند سال پیش سیستم پردازش تراکنش آنلاین (OLTP) نوشتیم. در سادهترین حالت، سیستم فقط باید درخواست میخواند و تراکنش را روی پایگاه داده پردازش میکرد. اما برنامه توزیعشده سهلایه چندپردازشی نوشتیم: هر مؤلفه موجودیت مستقلی بود که همزمان با همه مؤلفههای دیگر اجرا میشد. اگرچه بیشتر به نظر میرسد، نبود: استفاده از جداسازی زمانی نوشتن را آسانتر کرد. نگاه دقیقتری به این پروژه بیندازیم.
سیستم درخواستها را از تعداد زیادی خط ارتباط داده میگیرد و تراکنشها را روی پایگاه داده پشتیبان پردازش میکند.
طراحی این محدودیتها را پوشش میدهد:
- عملیات پایگاه داده نسبتاً زمانبر است.
- برای هر تراکنش، نباید هنگام پردازش تراکنش پایگاه داده سرویسهای ارتباطی را مسدود کنیم.
- کارایی پایگاه داده با جلسات همزمان زیاد افت میکند.
- چند تراکنش همزمان روی هر خط داده در جریان است.
راهحلی که بهترین کارایی و تمیزترین معماری را داد شبیه شکل ۵.۳ بود.
شکل ۵.۳. نمای کلی معماری OLTP
Input task #1 ──► App'n Queue ──► App logic #1 ──► Database Queue ──► Database handler Input task #2 ──► │ App logic #n ──► │ Database Input task #n ──► │ │
هر جعبه فرایند جداگانهای را نشان میدهد؛ فرایندها از طریق صف کار ارتباط دارند. هر فرایند ورودی یک خط ورودی را پایش میکند و به سرور برنامه درخواست میدهد. همه درخواستها ناهمگاماند: بهمحض درخواست فعلی، به پایش خط برای ترافیک بیشتر برمیگردد. بههمین ترتیب، سرور برنامه از فرایند پایگاه داده درخواست میدهد⁵ و هنگام اتمام تراکنش فردی مطلع میشود.
این مثال هم راهی برای توازن بار سریع و ساده بین چند فرایند مصرفکننده نشان میدهد: مدل مصرفکننده گرسنه.
یادداشت ۵: اگرچه پایگاه داده را بهصورت موجودیت واحد و یکپارچه نشان میدهیم، نیست. نرمافزار پایگاه داده به چند فرایند و نخ مشتری تقسیم شده، اما این داخلاً توسط نرمافزار پایگاه داده مدیریت میشود و بخشی از مثال ما نیست.
در مدل مصرفکننده گرسنه، زمانبند مرکزی را با چند وظیفه مصرفکننده مستقل و صف کار مرکزی جایگزین میکنید. هر وظیفه مصرفکننده قطعهای از صف کار برمیدارد و پردازش آن را انجام میدهد. وقتی کار تمام شد، برای کار بیشتر به صف برمیگردد. اینطور اگر یک وظیفه گیر کند، بقیه کمبود را جبران میکنند و هر مؤلفه با سرعت خود پیش میرود. هر مؤلفه از بقیه در زمان جدا شده است.
TIP 40
با سرویسها طراحی کنید
بهجای مؤلفه، واقعاً سرویس ساختهایم — اشیاء مستقل و همزمان پشت رابطهای مشخص و سازگار.
طراحی برای همزمانی
پذیرش رو به رشد Java بهعنوان بستره، توسعهدهندگان بیشتری را در معرض برنامهنویسی چندنخی قرار داده است. اما برنامهنویسی با نخها محدودیتهای طراحی تحمیل میکند — و این خوب است. آن محدودیتها آنقدر مفیدند که میخواهیم هر وقت برنامه مینویسیم رعایتشان کنیم. به جداسازی کد و مبارزه با برنامهنویسی تصادفی (صفحه ۱۷۲ را ببینید) کمک میکند.
با کد خطی، آسان است فرضیاتی بگیرید که به برنامهنویسی شل منجر میشود. اما همزمانی مجبورتان میکند کمی دقیقتر فکر کنید — دیگر تنها در مهمانی نیستید. چون چیزها میتوانند «همزمان» اتفاق بیفتند، ناگهان وابستگیهای زمانی میبینید.
ابتدا، هر متغیر سراسری یا ایستا باید از دسترسی همزمان محافظت شود. شاید وقت خوبی باشد بپرسید چرا اصلاً متغیر سراسری میخواهید. علاوه بر این، باید مطمئن شوید اطلاعات حالت سازگار ارائه میدهید، صرفنظر از ترتیب فراخوانیها. مثلاً چه زمانی پرسوجوی حالت شیء معتبر است؟ اگر شیء بین برخی فراخوانیها در حالت نامعتبر است، شاید به تصادفی تکیه میکنید که کسی در آن لحظه شیء را فراخوانی نکند.
فرض کنید زیرسیستم پنجرهای دارید که ابتدا ویجتها ساخته و بعد در دو مرحله جدا روی نمایشگر نشان داده میشوند. تا نشان داده نشود نمیتوانید حالت ویجت را تنظیم کنید. بسته به چیدمان کد، شاید به این واقعیت تکیه میکنید که هیچ شیء دیگری نمیتواند از ویجت ساختهشده استفاده کند تا آن را روی صفحه نشان دهید.
اما در سیستم همزمان این شاید درست نباشد. اشیاء هنگام فراخوانی همیشه باید در حالت معتبر باشند و میتوانند در ناجورترین زمانها فراخوانی شوند. باید مطمئن شوید شیء هر زمان که ممکن است فراخوانی شود در حالت معتبر است. اغلب این مسئله با کلاسهایی که سازنده و روتین مقداردهی اولیه جدا تعریف میکنند (که سازنده شیء را مقداردهیشده برنمیگرداند) خود را نشان میدهد. استفاده از ناوردهای کلاس، بحثشده در طراحی قراردادمحور (صفحه ۱۰۹)، از این تله کمک میگیرد.
رابطهای تمیزتر
فکر کردن به همزمانی و وابستگیهای زمانیترتیبی میتواند به طراحی رابطهای تمیزتر هم منجر شود. روتین کتابخانه C یعنی strtok را در نظر بگیرید که رشته را به توکن میشکند.
طراحی strtok thread-safe نیست⁶، اما بدترین بخش آن نیست: به وابستگی زمانی نگاه کنید. باید اولین فراخوانی strtok را با متغیری که میخواهید parse کنید بدهید و همه فراخوانیهای بعدی با NULL. اگر مقدار غیر-NULL بدهید، parse را روی آن بافر از نو شروع میکند. حتی بدون در نظر گرفتن نخها، فرض کنید میخواهید با strtok دو رشته جدا را همزمان parse کنید:
c
char buf1[BUFSIZ];
char buf2[BUFSIZ];
char *p, *q;
strcpy(buf1, "this is a test");
strcpy(buf2, "this ain't gonna work");
p = strtok(buf1, " ");
q = strtok(buf2, " ");
while (p && q) {
printf("%s %s\n", p, q);
p = strtok(NULL, " ");
q = strtok(NULL, " ");
}کد نشاندادهشده کار نمیکند: حالت ضمنی بین فراخوانیها در strtok نگه داشته میشود. باید strtok را فقط روی یک بافر در هر زمان استفاده کنید.
حالا در Java، طراحی parser رشته باید متفاوت باشد. باید thread-safe باشد و حالت سازگار ارائه دهد.
java
StringTokenizer st1 = new StringTokenizer("this is a test");
StringTokenizer st2 = new StringTokenizer("this test will work");
while (st1.hasMoreTokens() && st2.hasMoreTokens()) {
System.out.println(st1.nextToken());
System.out.println(st2.nextToken());
}StringTokenizer رابط بسیار تمیزتر و قابلنگهداریتری است. غافلگیرکننده ندارد و باگهای مرموز در آینده ایجاد نمیکند، برخلاف strtok.
یادداشت ۶: از داده ایستا برای نگه داشتن موقعیت فعلی در بافر استفاده میکند. داده ایستا در برابر دسترسی همزمان محافظت نشده، پس thread-safe نیست. علاوه بر این، اولین آرگومانی که میدهید را خراب میکند که میتواند شگفتیهای ناخوشایندی ایجاد کند.
TIP 41
همیشه برای همزمانی طراحی کنید
استقرار
وقتی معماری با عنصر همزمانی طراحی کردید، فکر کردن به مدیریت سرویسهای همزمان زیاد آسانتر میشود: مدل همهجا میشود.
حالا میتوانید در نحوه استقرار برنامه انعطافپذیر باشید: مستقل، کلاینت-سرور یا n-tier. با معماری سیستم بهعنوان سرویسهای مستقل، پیکربندی را هم پویا میکنید. با برنامهریزی برای همزمانی و جداسازی عملیات در زمان، همه این گزینهها را دارید — از جمله گزینه مستقل که میتوانید همزمان نباشید.
مسیر معکوس (تلاش برای افزودن همزمانی به برنامه غیرهمزمان) بسیار سختتر است. اگر برای همزمانی طراحی کنیم، وقتی زمان برسد — و اگر هرگز نرسد — نیازمندیهای مقیاسپذیری یا کارایی را راحتتر برآورده میکنیم و در هر صورت از طراحی تمیزتر سود میبریم.
دیگر وقتش نیست؟
بخشهای مرتبط:
- طراحی قراردادمحور، صفحه ۱۰۹
- برنامهنویسی تصادفی، صفحه ۱۷۲
Challenges
چند وظیفه را هنگام آماده شدن برای کار صبح بهطور موازی انجام میدهید؟ آیا میتوانید این را در نمودار فعالیت UML بیان کنید؟ راهی برای آماده شدن سریعتر با افزایش همزمانی پیدا میکنید؟
۲۹ — فقط یک نماست
با این حال مردی میشنود آنچه میخواهد بشنود و بقیه را نادیده میگیرد لا لا لا... — سیمون و گارفانکل، «بوکسر»
از ابتدا به ما یاد میدهند برنامه را یک تکه بزرگ ننویسیم، بلکه «تقسیم و فتح» کنیم و برنامه را به ماژولها جدا کنیم. هر ماژول مسئولیتهای خودش را دارد؛ در واقع تعریف خوب ماژول (یا کلاس) این است که مسئولیت واحد و مشخص داشته باشد.
اما وقتی برنامه را بر اساس مسئولیت به ماژولهای مختلف جدا کردید، مسئله جدیدی دارید. در زمان اجرا اشیاء چگونه با هم صحبت میکنند؟ چگونه وابستگیهای منطقی بین آنها را مدیریت میکنید؟ یعنی چگونه تغییرات حالت (یا بهروزرسانی مقادیر داده) در این اشیاء مختلف را به شکل تمیز و انعطافپذیر همگام میکنید؟ نمیخواهیم از یکدیگر زیاد خبر داشته باشند. میخواهیم هر ماژول مثل مرد آهنگ فقط آنچه میخواهد بشنود را بشنود.
با مفهوم رویداد شروع میکنیم. رویداد پیام ویژهای است که میگوید «چیز جالبی تازه اتفاق افتاد» (جالب البته به چشم بیننده است). میتوانیم از رویدادها برای علامت دادن تغییر در یک شیء که شیء دیگری ممکن است به آن علاقه داشته باشد استفاده کنیم.
استفاده از رویدادها اینگونه جفتشدگی بین آن اشیاء را کمینه میکند — فرستنده رویداد نیازی به دانش صریح از گیرنده ندارد. در واقع ممکن است چند گیرنده باشد، هر کدام روی دستور کار خود متمرکز (که فرستنده از آن بیخبر است).
با این حال باید در استفاده از رویدادها مراقب باشیم. در نسخه اولیه Java، مثلاً یک روتین همه رویدادهای مقصد یک برنامه را دریافت میکرد. دقیقاً راه نگهداری یا تکامل آسان نیست.
انتشار/اشتراک
چرا همه رویدادها را از یک روتین عبور دادن بد است؟ کپسولهسازی شیء را نقض میکند — آن یک روتین حالا باید دانش صمیمی از تعامل بین اشیاء زیاد داشته باشد. جفتشدگی را هم افزایش میدهد — در حالی که میخواهیم کاهش دهیم. چون خود اشیاء هم باید از این رویدادها خبر داشته باشند، احتمالاً اصل DRY، ارتگونالیتی و شاید بخشهایی از کنوانسیون ژنو را نقض میکنید. شاید این نوع کد را دیدهاید — معمولاً با case بزرگ یا if-then چندشاخهای. میتوانیم بهتر کنیم.
اشیاء باید بتوانند ثبتنام کنند تا فقط رویدادهایی که نیاز دارند را بگیرند و هرگز رویدادی که نمیخواهند دریافت نکنند. نمیخواهیم اشیاء را اسپم کنیم! در عوض میتوانیم از پروتکل انتشار/اشتراک استفاده کنیم، با نمودار توالی UML در شکل ۵.۴ نشاندادهشده.⁷
یادداشت ۷: الگوی Observer در [GHJV95] را هم برای اطلاعات بیشتر ببینید.
نمودار توالی جریان پیامها بین چند شیء را نشان میدهد، با اشیاء در ستونها چیدهشده. هر پیام بهصورت پیکان برچسبخورده از ستون فرستنده به ستون گیرنده است. ستاره در برچسب یعنی بیش از یک پیام از این نوع میتواند فرستاده شود.
اگر به رویدادهای خاصی که Publisher تولید میکند علاقه داریم، فقط کافی است ثبتنام کنیم. Publisher همه Subscriberهای علاقهمند را دنبال میکند؛ وقتی رویداد مورد علاقه تولید میکند، هر Subscriber را به نوبت فراخوانی و مطلع میکند که رویداد رخ داده است.
شکل ۵.۴. پروتکل انتشار/اشتراک
Subscriber one ──register──► Publisher ◄──register── Subscriber two Subscriber one ◄──notify*── Publisher ──notify*──► Subscriber two Subscriber one ──unsubscribe──► Publisher ──notify*──► Subscriber two
چند تنوع روی این تم وجود دارد — آینه سبکهای ارتباطی دیگر. اشیاء ممکن است انتشار/اشتراک را بهصورت همتا به همتا استفاده کنند (همانطور که بالا دیدیم)؛ ممکن است از «اتوبوس نرمافزاری» استفاده کنند که شیء مرکزی پایگاه شنوندگان را نگه میدارد و پیامها را مناسب پخش میکند. حتی میتوانید طرحی داشته باشید که رویدادهای بحرانی به همه شنوندگان — ثبتنامشده یا نه — پخش شود. یک پیادهسازی ممکن رویدادها در محیط توزیعشده توسط CORBA Event Service است، در جعبه صفحه بعد توصیف شده.
میتوانیم از این مکانیزم انتشار/اشتراک برای پیادهسازی مفهوم طراحی بسیار مهم استفاده کنیم: جداسازی مدل از نماهای مدل. با مثال مبتنی بر GUI شروع میکنیم، با طراحی Smalltalk که این مفهوم در آن متولد شد.
مدل-نما-کنترلکننده
فرض کنید برنامه صفحهگسترده دارید. علاوه بر اعداد خود صفحهگسترده، نموداری دارید که اعداد را بهصورت نمودار میلهای نشان میدهد و کادر گفتگوی جمع کل جاری که مجموع ستونی از صفحهگسترده را نشان میدهد.
واضح است نمیخواهیم سه نسخه جدا از داده داشته باشیم. پس مدلی میسازیم — خود داده، با عملیات مشترک برای دستکاری آن. بعد نماهای جدا میسازیم که داده را به روشهای مختلف نشان میدهند: بهصورت صفحهگسترده، نمودار یا کادر جمع. هر کدام از این نماها ممکن است کنترلکننده خودش را داشته باشد. مثلاً نمای نمودار ممکن است کنترلکنندهای داشته باشد که زوم یا پان روی داده را ممکن کند. هیچکدام روی خود داده اثر نمیگذارد، فقط روی آن نما.
این مفهوم کلیدی پشت اصطلاح Model-View-Controller (MVC) است: جداسازی مدل از هم GUI که آن را نمایش میدهد و هم کنترلهایی که نما را مدیریت میکنند.⁸
یادداشت ۸: نما و کنترلکننده محکم جفتاند و در برخی پیادهسازیهای MVC نما و کنترلکننده یک مؤلفه واحدند.
سرویس رویداد CORBA
CORBA Event Service به اشیاء شرکتکننده اجازه میدهد اعلانهای رویداد را از طریق اتوبوس مشترک، کانال رویداد، ارسال و دریافت کنند. کانال رویداد رسیدگی رویداد را میانجیگری میکند و تولیدکنندگان رویداد را از مصرفکنندگان جدا میکند. به دو روش اساسی کار میکند: push و pull.
در حالت push، تأمینکنندگان رویداد به کانال رویداد اطلاع میدهند رویدادی رخ داده است. کانال سپس خودکار آن رویداد را به همه اشیاء مشتری که علاقه ثبت کردهاند توزیع میکند.
در حالت pull، مشتریان دورهای کانال رویداد را poll میکنند که به نوبه خود تأمینکنندهای که داده رویداد متناظر با درخواست را ارائه میدهد poll میکند.
اگرچه CORBA Event Service میتواند همه مدلهای رویداد بحثشده در این بخش را پیاده کند، میتوانید آن را حیوان متفاوتی هم ببینید. CORBA ارتباط بین اشیاء نوشتهشده به زبانهای برنامهنویسی مختلف روی ماشینهای جغرافیایی پراکنده با معماریهای مختلف را تسهیل میکند. روی CORBA، سرویس رویداد راه جداشدهای برای تعامل با برنامههای سراسر جهان، نوشتهشده توسط افرادی که هرگز ندیدهاید، با زبانهایی که ترجیح میدهید ندانید، به شما میدهد.
با این کار از امکانات جالبی بهره میبرید. میتوانید چند نمای یک مدل داده را پشتیبانی کنید. میتوانید بینندههای مشترک را روی مدلهای داده مختلف استفاده کنید. حتی میتوانید چند کنترلکننده برای مکانیزمهای ورودی غیرسنتی پشتیبانی کنید.
TIP 42
نماها را از مدلها جدا کنید
با شل کردن جفتشدگی بین مدل و نما/کنترلکننده، انعطافپذیری زیادی با هزینه کم میخرید. در واقع این تکنیک یکی از مهمترین راههای حفظ قابلیت بازگشت است (قابلیت بازگشت، صفحه ۴۴ را ببینید).
نمای درختی Java
مثال خوبی از طراحی MVC را در ویجت درخت Java میتوان یافت. ویجت درخت (که درخت قابل کلیک و پیمایش نشان میدهد) در واقع مجموعهای از چند کلاس مختلف سازمانیافته در الگوی MVC است.
برای تولید ویجت درخت کاملاً کاربردی، فقط کافی است منبع دادهای بدهید که با رابط TreeModel سازگار باشد. کد شما حالا مدل درخت میشود.
نما توسط کلاسهای TreeCellRenderer و TreeCellEditor ساخته میشود که میتوان از آنها ارث برد و برای رنگها، فونتها و آیکونهای مختلف در ویجت سفارشی کرد. JTree بهعنوان کنترلکننده ویجت درخت عمل میکند و برخی قابلیتهای مشاهده عمومی میدهد.
چون مدل را از نما جدا کردیم، برنامهنویسی را بسیار سادهتر میکنیم. دیگر لازم نیست به برنامهنویسی ویجت درخت فکر کنید. در عوض فقط منبع داده میدهید.
فرض کنید معاون شرکت پیش شما میآید و برنامه سریعی میخواهد که نمودار سازمانی شرکت را — که در پایگاه داده قدیمی روی مینفریم است — پیمایش کند. فقط wrapperی بنویسید که داده مینفریم را بهعنوان TreeModel ارائه دهد و voilà: ویجت درخت کاملاً قابل پیمایش دارید.
حالا میتوانید از کلاسهای بیننده استفاده کنید؛ نحوه رندر گرهها را عوض کنید و از آیکونها، فونتها یا رنگهای ویژه استفاده کنید. وقتی معاون برمیگردد و میگوید استانداردهای جدید شرکت آیکون جمجمه و استخوان برای برخی کارمندان میخواهد، میتوانید تغییرات را در TreeCellRenderer بدون دست زدن به کد دیگر انجام دهید.
فراتر از GUI
اگرچه MVC معمولاً در زمینه توسعه GUI آموزش داده میشود، واقعاً تکنیک برنامهنویسی عمومی است. نما تفسیری از مدل است (شاید زیرمجموعه) — لازم نیست گرافیکی باشد. کنترلکننده بیشتر مکانیزم هماهنگی است و لازم نیست به دستگاه ورودی مرتبط باشد.
- مدل. مدل داده انتزاعی نماینده شیء هدف. مدل دانش مستقیم از هیچ نما یا کنترلکنندهای ندارد.
- نما. راهی برای تفسیر مدل. به تغییرات مدل و رویدادهای منطقی از کنترلکننده اشتراک میگیرد.
- کنترلکننده. راهی برای کنترل نما و دادن داده جدید به مدل. رویدادها را هم به مدل و هم به نما منتشر میکند.
مثال غیرگرافیکی ببینیم.
بیسبال نهادی منحصربهفرد است. جای دیگر کجا میتوانید گوهرهایی مثل «این بالاترین امتیاز بازی شده در سهشنبه، زیر باران، با نور مصنوعی، بین تیمهایی که نامشان با حرف صدادار شروع میشود» یاد بگیرید؟ فرض کنید مسئول توسعه نرمافزاری برای پشتیبانی گویندگان شجاعی هستیم که باید امتیازها، آمار و حقایق تافهای را گزارش کنند.
واضح است به اطلاعات بازی در جریان نیاز داریم — تیمهای بازیکننده، شرایط، بازیکن در خانه، امتیاز و مانند آن. این حقایق مدلهای ما را میسازند؛ با رسیدن اطلاعات جدید بهروز میشوند (تعویض پرتابکننده، strike out بازیکن، باران شروع میشود).
چند شیء نما خواهیم داشت که از این مدلها استفاده میکنند. یک نما ممکن است به دنبال run بگردد تا امتیاز جاری را بهروز کند. دیگری ممکن است اعلان بازیکنان جدید را بگیرد و خلاصه کوتاهی از آمار سالبهتاریخ آنها را بازیابی کند. سومی ممکن است داده را ببیند و رکوردهای جهانی جدید را بررسی کند. حتی ممکن است نمای تافهای داشته باشیم مسئول حقایق عجیب و بیفایدهای که مخاطب را هیجانزده میکند.
اما نمیخواهیم همه این نماها را مستقیماً به گوینده غرق کنیم. در عوض هر نما اعلانهای رویدادهای «جالب» تولید میکند و شیء سطح بالاتری تعیین میکند چه چیزی نشان داده شود.⁹
یادداشت ۹: اینکه هواپیمایی از بالا رد شود احتمالاً جالب نیست مگر اینکه صدمین هواپیمای آن شب باشد.
این اشیاء نما ناگهان برای شیء سطح بالاتر مدل شدهاند که خودش ممکن است مدل برای بینندههای قالببندی مختلف باشد. یک بیننده قالببندی ممکن است متن teleprompter برای گوینده بسازد، دیگری زیرنویس ویدیو را مستقیماً روی uplink ماهوارهای تولید کند، دیگری صفحات وب شبکه یا تیم را بهروز کند (شکل ۵.۵).
شکل ۵.۵. گزارش بیسبال. بینندهها به مدلها اشتراک میگیرند.
Conditions ──► Scores ──► Score collector ──► Display filter ──► TV feed generator Batter stats ──────────────────► Display filter ──► Web page formatter Records ───────────────────────► Display filter ──► Teleprompter Trivia ────────────────────────► Display filter (viewer subscribes to model)
این نوع شبکه مدل-بیننده تکنیک طراحی رایج (و ارزشمند) است. هر پیوند داده خام را از رویدادهایی که آن را ساخت جدا میکند — هر بیننده جدید یک انتزاع است. چون روابط شبکهاند (نه فقط زنجیره خطی)، انعطافپذیری زیادی داریم. هر مدل ممکن است چند بیننده داشته باشد و یک بیننده با چند مدل کار کند.
در سیستمهای پیشرفته مثل این، داشتن نماهای دیباگ — نماهای تخصصی که جزئیات عمیق مدل را نشان میدهند — مفید است. افزودن امکان ردیابی رویدادهای فردی هم صرفهجویی بزرگ در زمان است.
هنوز جفتشده (بعد از همه این سالها)
با وجود کاهش جفتشدگی، شنوندگان و تولیدکنندگان رویداد (مشترکین و ناشران) هنوز دانشی از یکدیگر دارند. در Java مثلاً باید روی تعاریف رابط مشترک و قراردادهای فراخوانی توافق کنند.
در بخش بعد، راههای کاهش بیشتر جفتشدگی را با شکلی از انتشار و اشتراک میبینیم که هیچ شرکتکنندهای نیازی به دانستن یا فراخوانی مستقیم یکدیگر ندارد.
بخشهای مرتبط:
- ارتگونالیتی، صفحه ۳۴
- قابلیت بازگشت، صفحه ۴۴
- جداسازی و قانون دمتر، صفحه ۱۳۸
- تختهسیاهها، صفحه ۱۶۵
- همه چیز نوشتن است، صفحه ۲۴۸
Exercises
۲۹. فرض کنید سیستم رزرو پرواز هواپیمایی دارید که مفهوم پرواز را شامل میشود. پاسخ در ص. ۲۹۶
java
public interface Flight {
// Return false if flight full.
public boolean addPassenger(Passenger p);
public void addToWaitList(Passenger p);
public int getFlightCapacity();
public int getNumPassengers();
}اگر مسافری به لیست انتظار اضافه شود، هنگام باز شدن جا بهطور خودکار روی پرواز قرار میگیرد.
کار گزارشدهی عظیمی وجود دارد که پروازهای overbook یا پر را برای پیشنهاد زمانبندی پروازهای اضافی جستوجو میکند. خوب کار میکند، اما ساعتها طول میکشد.
میخواهیم انعطافپذیری بیشتری در پردازش مسافران لیست انتظار داشته باشیم و باید کاری برای آن گزارش بزرگ بکنیم — اجرایش خیلی طول میکشد. از ایدههای این بخش برای بازطراحی این رابط استفاده کنید.
۳۰ — تختهسیاهها
نوشته روی دیوار است...
شاید معمولاً ظرافت را با کارآگاهان پلی مرتبط نکنید و بهجای آن تصویر کلیشهای دونات و قهوه ببینید. اما در نظر بگیرید کارآگاهان چگونه ممکن است از تختهسیاه برای هماهنگی و حل تحقیق قتل استفاده کنند.
فرض کنید بازرس ارشد با نصب تختهسیاه بزرگ در اتاق کنفرانس شروع میکند. روی آن یک سؤال مینویسد:
H. DUMPTY (MALE, EGG): ACCIDENT OR MURDER?آیا هامی واقعاً افتاد، یا هل داده شد؟ هر کارآگاه میتواند با افزودن حقایق، اظهارات شاهدان، هر مدرک پزشکی قانونی و مانند آن به این معمای قتل بالقوه کمک کند. با انباشته شدن داده، کارآگاهی ممکن است ارتباطی ببیند و آن مشاهده یا حدس را هم بگذارد. این فرایند در همه شیفتها، با افراد و عوامل مختلف، تا بسته شدن پرونده ادامه مییابد. نمونه تختهسیاه در شکل ۵.۶ نشان داده شده است.
شکل ۵.۶. کسی ارتباط بین بدهیهای قمار هامی و سوابق تلفن را پیدا کرد. شاید تهدید تلفنی میگرفت.
H. Dumpty (Male, Egg): Accident or Murder? Photos | Shell fragments | Phone logs King's men | Gambling debts | Graffiti | Wife's alibi Detective 1 Detective 2 Detective 3
برخی ویژگیهای کلیدی رویکرد تختهسیاه:
- هیچ کارآگاهی نیازی به دانستن وجود کارآگاه دیگر ندارد — تخته را برای اطلاعات جدید تماشا میکنند و یافتههایشان را اضافه میکنند.
- کارآگاهان ممکن است در رشتههای مختلف آموزش دیده باشند، سطح تحصیلات و تخصص متفاوت داشته باشند و شاید حتی در همان منطقه کار نکنند. همه تمایل به حل پرونده دارند، و بس.
- کارآگاهان مختلف ممکن است در طول فرایند بیایند و بروند و شیفتهای مختلف کار کنند.
- محدودیتی برای آنچه روی تختهسیاه گذاشته میشود نیست. ممکن است عکس، جمله، مدرک فیزیکی و مانند آن باشد.
روی چند پروژه با گردشکار یا فرایند جمعآوری داده توزیعشده کار کردهایم. با هر کدام، طراحی راهحل حول مدل ساده تختهسیاه به ما استعاره محکمی داد: همه ویژگیهای بالا با کارآگاهان به همان اندازه برای اشیاء و ماژولهای کد قابل استفادهاند.
سیستم تختهسیاه به ما اجازه میدهد اشیاء را کاملاً از یکدیگر جدا کنیم و محلی برای تبادل ناشناس و ناهمگام داده بین مصرفکنندگان و تولیدکنندگان دانش فراهم کنیم. همانطور که حدس میزنید، مقدار کدی که باید بنویسیم را هم کم میکند.
پیادهسازیهای تختهسیاه
سیستمهای تختهسیاه مبتنی بر کامپیوتر در ابتدا برای کاربردهای هوش مصنوعی اختراع شدند که مسائل حلشونده بزرگ و پیچیده بودند — تشخیص گفتار، سیستمهای استدلال مبتنی بر دانش و مانند آن.
سیستمهای توزیعشده مدرک شبیه تختهسیاه مثل JavaSpaces و T Spaces [URL 50, URL 25] بر مدل جفت کلید/مقدار مبتنیاند که ابتدا در Linda [CG90] محبوب شد، جایی که مفهوم بهنام فضای tuple شناخته میشد.
با این سیستمها میتوانید اشیاء Java فعال — نه فقط داده — روی تختهسیاه ذخیره کنید و با تطبیق جزئی فیلدها (از طریق قالب و wildcard) یا زیرنوع بازیابی کنید. مثلاً فرض کنید نوع Author دارید که زیرنوع Person است. میتوانید تختهسیاه حاوی اشیاء Person را با قالب Author با مقدار lastName برابر «Shakespeare» جستوجو کنید. بیل شکسپیر نویسنده را میگیرید، نه فرد شکسپیر باغبان را.
عملیات اصلی در JavaSpaces:
| نام | عملکرد |
|---|---|
read | جستوجو و بازیابی داده از فضا |
write | گذاشتن مورد در فضا |
take | شبیه read، اما مورد را از فضا هم حذف میکند |
notify | تنظیم اعلان برای هرگاه شیئی مطابق قالب نوشته شود |
T Spaces مجموعه مشابهی از عملیات را پشتیبانی میکند، اما با نامهای متفاوت و معناشناسی کمی متفاوت. هر دو سیستم مثل محصول پایگاه داده ساخته شدهاند؛ عملیات اتمی و تراکنشهای توزیعشده برای تضمین یکپارچگی داده میدهند.
چون میتوانیم اشیاء ذخیره کنیم، میتوانیم از تختهسیاه برای طراحی الگوریتمها بر اساس جریان اشیاء، نه فقط داده، استفاده کنیم. انگار کارآگاهان بتوانند افراد را به تختهسیاه سنجاق کنند — خود شاهدان، نه فقط اظهاراتشان. هر کسی میتواند از شاهد سؤال بپرسد، رونوشت بگذارد و شاهد را به ناحیه دیگر تختهسیاه منتقل کند که شاید متفاوت پاسخ دهد (اگر اجازه دهید شاهد هم تختهسیاه را بخواند).
مزیت بزرگ چنین سیستمهایی این است که رابط واحد و سازگاری به تختهسیاه دارید. هنگام ساخت برنامه توزیعشده معمولی، میتوانید زمان زیادی صرف ساخت فراخوانیهای API منحصربهفرد برای هر تراکنش و تعامل توزیعشده در سیستم کنید. با انفجار ترکیبی رابطها و تعاملات، پروژه بهسرعت کابوس میشود.
سازماندهی تختهسیاه شما
وقتی کارآگاهان روی پروندههای بزرگ کار میکنند، تختهسیاه شلوغ میشود و یافتن داده روی تخته دشوار میشود. راهحل تقسیمبندی تختهسیاه و شروع سازماندهی داده روی آن است.
سیستمهای نرمافزاری مختلف این تقسیمبندی را به روشهای مختلف انجام میدهند؛ برخی از منطقهها یا گروههای علاقه نسبتاً تخت استفاده میکنند، برخی ساختار درختی سلسلهمراتبیتر میگیرند.
سبک برنامهنویسی تختهسیاه نیاز به این همه رابط را حذف میکند و سیستمی ظریفتر و سازگارتر میسازد.
مثال کاربردی
فرض کنید برنامهای برای پذیرش و پردازش درخواستهای وام یا رهن مینویسیم. قوانین حاکم بر این حوزه بهطرز آزاردهندهای پیچیدهاند؛ دولت فدرال، ایالتی و محلی همه نظر دارند. وامدهنده باید ثابت کند برخی چیزها را افشا کرده و اطلاعات خاصی را پرسیده — اما نباید سؤالات خاص دیگری را پرسیده باشد و مانند آن.
فراتر از مه گرفتگی قانون قابل اجرا، مسائل زیر را هم داریم:
- تضمینی برای ترتیب رسیدن داده نیست. مثلاً پرسوجوهای بررسی اعتبار یا جستوجوی سند ممکن است زمان قابل توجهی ببرد، در حالی که مواردی مثل نام و آدرس فوراً در دسترساند.
- جمعآوری داده ممکن است توسط افراد مختلف، در دفاتر پراکنده، در مناطق زمانی مختلف انجام شود.
- برخی جمعآوری داده ممکن است خودکار توسط سیستمهای دیگر انجام شود. این داده هم ممکن است ناهمگام برسد.
- با این حال برخی دادهها هنوز به داده دیگر وابستهاند. مثلاً شاید نتوانید جستوجوی سند ماشین را شروع کنید تا مدرک مالکیت یا بیمه را نگیرید.
- رسیدن داده جدید ممکن است سؤالات و سیاستهای جدیدی مطرح کند. فرض کنید بررسی اعتبار گزارش کمدرخشانی میدهد؛ حالا به این پنج فرم اضافی و شاید نمونه خون نیاز دارید.
میتوانید هر ترکیب و شرایط ممکن را با سیستم گردشکار مدیریت کنید. چنین سیستمهایی وجود دارند، اما میتوانند پیچیده و پرهزینه برای برنامهنویس باشند. با تغییر مقررات، گردشکار باید بازسازماندهی شود: مردم شاید روشهایشان را عوض کنند و کد سختکدشده بازنویسی شود.
تختهسیاه، در ترکیب با موتور قوانین که الزامات قانونی را کپسوله میکند، راهحل ظریفی برای دشواریهای اینجاست. ترتیب رسیدن داده مهم نیست: وقتی واقعیتی گذاشته میشود، قوانین مناسب را فعال میکند. بازخورد هم بهراحتی مدیریت میشود: خروجی هر مجموعه قانون میتواند روی تختهسیاه گذاشته شود و قوانین بیشتر قابل اجرا را فعال کند.
TIP 43
از تختهسیاه برای هماهنگی گردشکار استفاده کنید
میتوانیم از تختهسیاه برای هماهنگی حقایق و عوامل پراکنده استفاده کنیم و در عین حال استقلال و حتی انزوا بین شرکتکنندگان را حفظ کنیم.
البته با روشهای زورگیرانهتر هم میتوان همان نتایج را گرفت، اما سیستم شکنندهتری خواهید داشت. وقتی بشکند، شاید همه اسبها و مردان پادشاه نتوانند برنامهتان را دوباره کار بیندازند.
بخشهای مرتبط:
- قدرت متن ساده، صفحه ۷۳
- فقط یک نماست، صفحه ۱۵۷
Challenges
آیا در دنیای واقعی از سیستمهای تختهسیاه استفاده میکنید — تابلو کنار یخچال، یا تختهسفید بزرگ سر کار؟ چه چیزی آنها را مؤثر میکند؟ آیا پیامها با قالب سازگار گذاشته میشوند؟ مهم است؟
Exercises
۳۰. برای هر یک از کاربردهای زیر، آیا سیستم تختهسیاه مناسب است یا نه؟ چرا؟ پاسخ در ص. ۲۹۷
پردازش تصویر. میخواهید چند فرایند موازی تکههایی از تصویر را بگیرند، پردازش کنند و تکه تکمیلشده را برگردانند.
تقویم گروهی. افرادی در سراسر جهان، در مناطق زمانی مختلف و با زبانهای مختلف، میخواهند جلسهای زمانبندی کنند.
ابزار پایش شبکه. سیستم آمار کارایی جمع میکند و گزارشهای مشکل را جمعآوری میکند. میخواهید چند عامل برای استفاده از این اطلاعات و جستوجوی مشکل در سیستم پیاده کنید.