یکی از مهم‌ترین تکنیک‌ها برای مدیریت پیچیدگی نرم‌افزار، طراحی سیستم‌ها به‌گونه‌ای است که توسعه‌دهندگان فقط در هر زمان با بخش کوچکی از پیچیدگی کلی روبه‌رو شوند. این رویکرد “طراحی ماژولار” (Modular Design) نام دارد و این فصل اصول پایه‌ای آن را معرفی می‌کند.

۴.۱ طراحی ماژولار

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

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

برای مدیریت وابستگی‌ها، هر ماژول را به دو بخش در نظر می‌گیریم: رابط (Interface) و پیاده‌سازی (Implementation). رابط شامل هر چیزی است که یک توسعه‌دهنده که در ماژولی دیگر کار می‌کند باید بداند تا بتواند از ماژول مورد نظر استفاده کند. معمولاً رابط، آنچه ماژول انجام می‌دهد را توصیف می‌کند، نه اینکه چگونه آن را انجام می‌دهد. پیاده‌سازی شامل کدی است که تعهدات اعلام‌شده توسط رابط را اجرا می‌کند. توسعه‌دهنده‌ای که در یک ماژول خاص کار می‌کند، باید رابط و پیاده‌سازی آن ماژول و همچنین رابط ماژول‌هایی که توسط آن فراخوانی می‌شوند را درک کند. اما نیازی نیست پیاده‌سازی ماژول‌های دیگر را بشناسد.

به ماژولی فکر کنید که درخت‌های متوازن (balanced trees) را پیاده‌سازی می‌کند. احتمالاً این ماژول شامل کدهای پیشرفته‌ای برای اطمینان از حفظ تعادل درخت است. اما این پیچیدگی برای کاربران ماژول قابل مشاهده نیست. کاربران فقط یک رابط نسبتاً ساده برای انجام عملیات‌هایی مانند درج، حذف و بازیابی گره‌ها می‌بینند. برای انجام عملیات درج، کاربر فقط نیاز دارد کلید و مقدار گره جدید را فراهم کند؛ مکانیزم‌های پیمایش درخت و تقسیم گره‌ها در رابط قابل مشاهده نیستند.

در چارچوب این کتاب، یک ماژول هر واحدی از کد است که دارای رابط و پیاده‌سازی باشد. هر کلاس در زبان‌های شی‌گرا یک ماژول محسوب می‌شود. متدها درون یک کلاس یا توابع در زبان‌هایی که شی‌گرا نیستند نیز می‌توانند ماژول تلقی شوند: هر یک از آن‌ها دارای رابط و پیاده‌سازی هستند و می‌توان اصول طراحی ماژولار را بر آن‌ها اعمال کرد. زیرسیستم‌ها و سرویس‌های سطح بالاتر نیز ماژول هستند؛ رابط‌های آن‌ها ممکن است شکل‌های مختلفی مانند فراخوانی‌های کرنل یا درخواست‌های HTTP داشته باشند. بخش عمده‌ای از بحث طراحی ماژولار در این کتاب روی طراحی کلاس‌ها متمرکز است، اما مفاهیم و تکنیک‌های مطرح‌شده برای سایر انواع ماژول‌ها نیز قابل استفاده هستند.

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

۴.۲ در یک رابط چه چیزی وجود دارد؟

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

هر رابط همچنین شامل بخش‌های غیررسمی است. این بخش‌ها به‌گونه‌ای مشخص نمی‌شوند که توسط زبان برنامه‌نویسی قابل درک یا اجرایی باشند. بخش‌های غیررسمی رابط شامل رفتار سطح بالای آن است، مانند این واقعیت که یک تابع فایل مشخص‌شده توسط یکی از آرگومان‌هایش را حذف می‌کند. اگر محدودیت‌هایی برای استفاده از یک کلاس وجود داشته باشد (مثلاً باید یک متد قبل از متد دیگری فراخوانی شود)، این‌ها نیز بخشی از رابط آن کلاس هستند. به‌طور کلی، اگر یک توسعه‌دهنده برای استفاده از یک ماژول به دانستن یک قطعه خاص از اطلاعات نیاز داشته باشد، آنگاه آن اطلاعات بخشی از رابط ماژول است. جنبه‌های غیررسمی رابط فقط می‌توانند از طریق توضیحات متنی (کامنت‌ها) توصیف شوند و زبان برنامه‌نویسی نمی‌تواند اطمینان حاصل کند که این توضیحات کامل یا دقیق هستند. برای بیشتر رابط‌ها، جنبه‌های غیررسمی بزرگ‌تر و پیچیده‌تر از جنبه‌های رسمی هستند.

یکی از مزایای رابط‌های مشخص‌شده به‌طور واضح این است که دقیقاً نشان می‌دهند که توسعه‌دهندگان برای استفاده از ماژول مربوطه به چه اطلاعاتی نیاز دارند. این کمک می‌کند تا مشکل «ناشناخته‌های ناشناخته» که در بخش ۲.۲ توضیح داده شده است، از بین برود.

۴.۳ انتزاع‌ها

واژه انتزاع ارتباط نزدیکی با ایده طراحی ماژولار دارد. انتزاع یک دیدگاه ساده‌شده از یک موجودیت است که جزئیات غیرضروری آن را حذف می‌کند. انتزاع‌ها مفید هستند زیرا آن‌ها فرآیند تفکر و دستکاری چیزهای پیچیده را برای ما آسان‌تر می‌کنند.

در برنامه‌نویسی ماژولار، هر ماژول یک انتزاع را به‌صورت رابط خود فراهم می‌کند. رابط یک دیدگاه ساده‌شده از عملکرد ماژول را ارائه می‌دهد؛ جزئیات پیاده‌سازی از منظر انتزاع ماژول اهمیت ندارند، بنابراین در رابط حذف می‌شوند.

در تعریف انتزاع، واژه «غیرضروری» بسیار مهم است. هرچه جزئیات غیرضروری بیشتری از یک انتزاع حذف شود، بهتر است. با این حال، یک جزئیه فقط زمانی می‌تواند از یک انتزاع حذف شود که غیرضروری باشد. انتزاع می‌تواند به دو صورت اشتباه باشد. اول، می‌تواند جزئیاتی را شامل شود که واقعاً مهم نیستند؛ وقتی این اتفاق می‌افتد، انتزاع پیچیده‌تر از آنچه که لازم است می‌شود که بار شناختی بیشتری بر دوش توسعه‌دهندگان می‌گذارد. اشتباه دوم زمانی است که یک انتزاع جزئیات مهمی را که واقعاً ضروری هستند، حذف کند. این باعث ایجاد ابهام می‌شود: توسعه‌دهندگانی که فقط به انتزاع نگاه می‌کنند، تمام اطلاعات لازم برای استفاده درست از آن را نخواهند داشت. یک انتزاع که جزئیات مهم را حذف می‌کند، یک انتزاع نادرست است: ممکن است ساده به نظر برسد، اما در واقع این‌طور نیست. کلید طراحی انتزاع‌ها درک این است که چه چیزی مهم است و جستجو برای طراحی‌هایی که مقدار اطلاعات مهم را به حداقل برسانند.

به‌عنوان یک مثال، سیستم فایل را در نظر بگیرید. انتزاعی که توسط سیستم فایل فراهم می‌شود، جزئیات زیادی را حذف می‌کند، مانند مکانیسم انتخاب بلاک‌ها در یک دستگاه ذخیره‌سازی برای ذخیره داده‌های یک فایل خاص. این جزئیات برای کاربران سیستم فایل غیرضروری هستند (تا زمانی که سیستم عملکرد کافی ارائه دهد). اما برخی از جزئیات پیاده‌سازی سیستم فایل برای کاربران مهم هستند. بیشتر سیستم‌های فایل داده‌ها را در حافظه اصلی کش می‌کنند و ممکن است نوشتن داده‌های جدید را به تأخیر بیندازند تا عملکرد بهتری داشته باشند. برخی از برنامه‌ها، مانند پایگاه‌های داده، نیاز دارند تا دقیقاً بدانند که چه زمانی داده‌ها به ذخیره‌سازی نوشته می‌شوند تا بتوانند اطمینان حاصل کنند که داده‌ها پس از خرابی سیستم حفظ خواهند شد. بنابراین، قوانین مربوط به نوشتن داده‌ها به ذخیره‌سازی ثانویه باید در رابط سیستم فایل قابل مشاهده باشد.

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

 

۴.۴ ماژول‌های عمیق

بهترین ماژول‌ها آن‌هایی هستند که عملکرد قدرتمندی دارند اما رابط‌های ساده‌ای ارائه می‌دهند. من از واژه “عمیق” برای توصیف چنین ماژول‌هایی استفاده می‌کنم. برای تجسم مفهوم عمق، تصور کنید که هر ماژول به‌صورت یک مستطیل نمایش داده می‌شود، همانطور که در شکل ۴.۱ نشان داده شده است. مساحت هر مستطیل متناسب با عملکرد پیاده‌سازی‌شده توسط ماژول است. لبه بالای هر مستطیل نمایانگر رابط ماژول است؛ طول این لبه نشان‌دهنده پیچیدگی رابط است. بهترین ماژول‌ها عمیق هستند: آن‌ها عملکرد زیادی را پشت یک رابط ساده پنهان می‌کنند. یک ماژول عمیق، یک انتزاع خوب است زیرا تنها بخش کوچکی از پیچیدگی داخلی آن برای کاربرانش قابل مشاهده است.

تصویر 4.1

تصویر 4.1

شکل ۴.۱: ماژول‌های عمیق و کم عمق. بهترین ماژول‌ها عمیق هستند: آن‌ها عملکرد زیادی را از طریق یک رابط ساده قابل دسترسی می‌کنند. یک ماژول کم عمق، ماژولی است با رابط نسبتاً پیچیده، اما عملکرد چندانی ندارد: این ماژول پیچیدگی زیادی را پنهان نمی‌کند.

عمق ماژول یک روش تفکر درباره هزینه در مقابل فایده است. فایده‌ای که یک ماژول فراهم می‌کند، عملکرد آن است. هزینه یک ماژول (از نظر پیچیدگی سیستم) رابط آن است. رابط ماژول نمایانگر پیچیدگی‌ای است که ماژول به سیستم وارد می‌کند: هرچه رابط کوچک‌تر و ساده‌تر باشد، پیچیدگی کمتری به سیستم تحمیل می‌کند. بهترین ماژول‌ها آن‌هایی هستند که بیشترین فایده و کمترین هزینه را دارند. رابط‌ها خوب هستند، اما رابط‌های بیشتر یا بزرگتر لزوماً بهتر نیستند!

مکانیزم I/O فایل ارائه‌شده توسط سیستم‌عامل Unix و نسل‌های آن مانند Linux، نمونه‌ای زیبا از یک رابط عمیق است. تنها پنج فراخوانی سیستم پایه برای I/O وجود دارد که امضای ساده‌ای دارند:

int open(const char* path, int flags, mode_t permissions); 
ssize_t read(int fd, void* buffer, size_t count);
ssize_t write(int fd, const void* buffer, size_t count); 
off_t lseek(int fd, off_t offset, int referencePosition); 
int close(int fd);

فراخوانی سیستم open یک نام فایل سلسله‌مراتبی مانند /a/b/c را می‌گیرد و یک شناسۀ فایل عددی برمی‌گرداند که برای ارجاع به فایل بازشده استفاده می‌شود. سایر آرگومان‌های open اطلاعات اختیاری مانند اینکه آیا فایل برای خواندن یا نوشتن باز می‌شود، آیا باید یک فایل جدید ایجاد شود اگر فایل موجود نباشد، و مجوزهای دسترسی به فایل در صورت ایجاد یک فایل جدید را فراهم می‌کنند. فراخوانی‌های سیستم read و write اطلاعات را بین نواحی بافر در حافظه برنامه و فایل منتقل می‌کنند؛ close دسترسی به فایل را خاتمه می‌دهد. بیشتر فایل‌ها به‌صورت ترتیبی دسترسی پیدا می‌کنند، بنابراین این حالت به‌طور پیش‌فرض است؛ با این حال، دسترسی تصادفی می‌تواند با فراخوانی سیستم lseek برای تغییر موقعیت دسترسی جاری به‌دست آید.

یک پیاده‌سازی مدرن از رابط I/O Unix نیازمند صدها هزار خط کد است که مسائل پیچیده‌ای مانند موارد زیر را حل می‌کند:

  • فایل‌ها چگونه روی دیسک نمایان می‌شوند تا دسترسی کارآمد فراهم کنند؟
  • دایرکتوری‌ها چگونه ذخیره می‌شوند و نام‌های مسیر سلسله‌مراتبی چگونه پردازش می‌شوند تا فایل‌هایی که به آن‌ها اشاره می‌شود پیدا شوند؟
  • چگونه مجوزها اجرا می‌شوند، به‌گونه‌ای که یک کاربر نتواند فایل‌های کاربر دیگر را تغییر دهد یا حذف کند؟
  • دسترسی به فایل‌ها چگونه پیاده‌سازی می‌شود؟ به‌عنوان مثال، چطور عملکرد بین پردازش‌های وقفه و کد پس‌زمینه تقسیم می‌شود و چگونه این دو عنصر به‌طور ایمن با هم ارتباط برقرار می‌کنند؟
  • چه سیاست‌های زمان‌بندی برای دسترسی‌های همزمان به چندین فایل استفاده می‌شود؟
  • چگونه می‌توان داده‌های فایل که اخیراً به آن‌ها دسترسی پیدا شده را در حافظه کش کرد تا تعداد دسترسی‌ها به دیسک کاهش یابد؟
  • چگونه می‌توان انواع مختلفی از دستگاه‌های ذخیره‌سازی ثانویه، مانند دیسک‌ها و فلش درایوها، را در یک سیستم فایل واحد گنجاند؟

تمامی این مسائل و بسیاری دیگر توسط پیاده‌سازی سیستم فایل Unix مدیریت می‌شوند؛ این‌ها برای برنامه‌نویسانی که فراخوانی‌های سیستم را انجام می‌دهند، نامرئی هستند. پیاده‌سازی‌های رابط I/O Unix در طول سال‌ها به‌طور رادیکالی تغییر کرده‌اند، اما پنج فراخوانی اصلی هسته تغییر نکرده‌اند.

مثال دیگری از یک ماژول عمیق، جمع‌آور زباله (Garbage Collector) در زبان‌هایی مانند Go یا Java است. این ماژول هیچ رابطی ندارد؛ به‌طور نامرئی در پس‌زمینه کار می‌کند تا حافظه غیرقابل استفاده را بازیابی کند. اضافه کردن جمع‌آوری زباله به یک سیستم در واقع رابط کلی آن را کوچکتر می‌کند، زیرا رابط برای آزادسازی اشیاء حذف می‌شود. پیاده‌سازی جمع‌آور زباله بسیار پیچیده است، اما این پیچیدگی برای برنامه‌نویسانی که از زبان استفاده می‌کنند، پنهان است.

ماژول‌های عمیقی مانند I/O Unix و جمع‌آور زباله‌ها انتزاع‌های قدرتمندی فراهم می‌کنند زیرا استفاده از آن‌ها آسان است، در حالی که پیچیدگی‌های قابل‌توجه پیاده‌سازی را پنهان می‌کنند.

۴.۵ ماژول‌های کم عمق

از طرف دیگر، یک ماژول کم عمق ماژولی است که رابط آن نسبت به عملکردی که ارائه می‌دهد، پیچیدگی نسبتاً بیشتری دارد. به‌عنوان مثال، یک کلاس که لیست‌های پیوندی را پیاده‌سازی می‌کند، کم عمق است. تغییرات زیادی برای دستکاری یک لیست پیوندی (افزودن یا حذف یک عنصر فقط چند خط کد نیاز دارد)، بنابراین انتزاع لیست پیوندی جزئیات زیادی را پنهان نمی‌کند. پیچیدگی رابط لیست پیوندی تقریباً به اندازه پیچیدگی پیاده‌سازی آن است. کلاس‌های کم عمق گاهی اجتناب‌ناپذیر هستند، اما آن‌ها در مدیریت پیچیدگی چندان کمک نمی‌کنند.

در اینجا یک مثال افراطی از یک متد کم عمق است که از یک پروژه در کلاس طراحی نرم‌افزار گرفته شده است:

private void addNullValueForAttribute(String attribute) {
    data.put(attribute, null);
}

از منظر مدیریت پیچیدگی، این متد وضعیت را بدتر می‌کند، نه بهتر. این متد هیچ انتزاعی ارائه نمی‌دهد، زیرا تمام عملکرد آن از طریق رابطش قابل مشاهده است. به‌عنوان مثال، فراخوانی‌کنندگان احتمالاً باید بدانند که ویژگی در متغیر داده (data) ذخیره خواهد شد. فکر کردن به رابط آن به اندازه فکر کردن به پیاده‌سازی کامل آن ساده نیست. اگر این متد به‌درستی مستند شده باشد، مستندات آن طولانی‌تر از کد خود متد خواهد بود. حتی برای فراخوانی این متد، به تعداد کلیدهای بیشتری نیاز است تا زمانی که فراخوانی‌کننده بخواهد مستقیماً با متغیر داده کار کند. این متد پیچیدگی را اضافه می‌کند (در قالب یک رابط جدید که توسعه‌دهندگان باید یاد بگیرند) اما هیچ فایده‌ای جبران‌کننده ندارد.

پرچم قرمز: ماژول کم عمق

یک ماژول کم عمق ماژولی است که رابط آن نسبت به عملکردی که ارائه می‌دهد، پیچیده است. ماژول‌های کم عمق چندان در مبارزه با پیچیدگی کمک نمی‌کنند، زیرا فایده‌ای که ارائه می‌دهند (عدم نیاز به یادگیری نحوه عملکرد داخلی آن‌ها) با هزینه یادگیری و استفاده از رابط‌هایشان جبران می‌شود. ماژول‌های کوچک تمایل دارند که کم عمق باشند.

۴.۶ کلاسیتی (Classitis)

متأسفانه، ارزش کلاس‌های عمیق امروزه به‌طور گسترده‌ای درک نمی‌شود. خرد متداول در برنامه‌نویسی این است که کلاس‌ها باید کوچک باشند، نه عمیق. دانش‌آموزان معمولاً آموزش می‌بینند که مهم‌ترین جنبه در طراحی کلاس این است که کلاس‌های بزرگ‌تر به کلاس‌های کوچک‌تر تقسیم شوند. همین مشاوره اغلب درباره متدها نیز داده می‌شود: “هر متدی که بیش از N خط طول دارد باید به چند متد تقسیم شود” (N ممکن است حتی تا ۱۰ خط هم باشد). این رویکرد منجر به تعداد زیادی کلاس و متد کم عمق می‌شود که پیچیدگی کلی سیستم را افزایش می‌دهد.

افراط در رویکرد “کلاس‌ها باید کوچک باشند” به یک سندروم تبدیل می‌شود که من آن را “کلاسیتی” (Classitis) می‌نامم، که ناشی از دیدگاه اشتباهی است که “کلاس‌ها خوب هستند، پس تعداد بیشتر کلاس‌ها بهتر است.” در سیستم‌هایی که از کلاسیتی رنج می‌برند، توسعه‌دهندگان تشویق می‌شوند که مقدار عملکرد در هر کلاس جدید را به حداقل برسانند: اگر به عملکرد بیشتری نیاز دارید، کلاس‌های بیشتری معرفی کنید. کلاسیتی ممکن است منجر به کلاس‌هایی شود که به‌طور فردی ساده هستند، اما پیچیدگی سیستم کلی را افزایش می‌دهند. کلاس‌های کوچک عملکرد زیادی ارائه نمی‌دهند، بنابراین باید تعداد زیادی از آن‌ها وجود داشته باشد، هرکدام با رابط خود. این رابط‌ها تجمع می‌کنند و پیچیدگی عظیمی را در سطح سیستم ایجاد می‌کنند. کلاس‌های کوچک همچنین منجر به سبک برنامه‌نویسی پرحجم می‌شوند، به‌دلیل کدهای اضافی که برای هر کلاس مورد نیاز است.

۴.۷ مثال‌ها: Java و Unix I/O

یکی از قابل‌مشاهده‌ترین مثال‌ها از کلاسیتی امروز، کتابخانه کلاس Java است. زبان Java نیاز به تعداد زیادی کلاس کوچک ندارد، اما فرهنگ کلاسیتی به‌نظر می‌رسد که در جامعه برنامه‌نویسی Java ریشه دوانده است. به‌عنوان مثال، برای باز کردن یک فایل به‌منظور خواندن اشیاء سریال‌شده از آن، شما باید سه شیء مختلف ایجاد کنید:

FileInputStream fileStream = new FileInputStream(fileName);
BufferedInputStream bufferedStream = new BufferedInputStream(fileStream);
ObjectInputStream objectStream = new ObjectInputStream(bufferedStream);

یک شیء FileInputStream تنها I/O ابتدایی را ارائه می‌دهد: قادر به انجام I/O با بافر نیست و نمی‌تواند اشیاء سریال‌شده را بخواند یا بنویسد. شیء BufferedInputStream به یک FileInputStream بافر اضافه می‌کند و شیء ObjectInputStream قابلیت خواندن و نوشتن اشیاء سریال‌شده را اضافه می‌کند. دو شیء اول در کد بالا، یعنی fileStream و bufferedStream، پس از باز کردن فایل هیچ‌گاه استفاده نمی‌شوند؛ تمام عملیات‌های آینده از objectStream استفاده می‌کنند.

اینکه باید درخواست بافر کردن را به‌طور صریح با ایجاد یک شیء BufferedInputStream جداگانه مطرح کنید، به‌خصوص آزاردهنده است (و پر از خطا). اگر یک توسعه‌دهنده فراموش کند که این شیء را ایجاد کند، هیچ بافرینگی انجام نخواهد شد و I/O کند خواهد بود. شاید توسعه‌دهندگان Java استدلال کنند که همه نمی‌خواهند از بافر کردن برای I/O فایل استفاده کنند، بنابراین نباید این ویژگی به‌طور پیش‌فرض در مکانیزم اصلی گنجانده شود. آن‌ها ممکن است استدلال کنند که بهتر است بافر کردن جدا باشد تا مردم بتوانند انتخاب کنند که آیا از آن استفاده کنند یا نه. فراهم کردن انتخاب خوب است، اما رابط‌ها باید به‌گونه‌ای طراحی شوند که رایج‌ترین حالت ساده‌ترین حالت ممکن باشد (نگاه کنید به فرمول صفحه ۶). تقریباً هر کاربر I/O فایل به بافرینگ نیاز دارد، بنابراین باید به‌طور پیش‌فرض ارائه شود. برای آن دسته از موقعیت‌هایی که بافرینگ مطلوب نیست، کتابخانه می‌تواند یک مکانیزم برای غیرفعال کردن آن فراهم کند. هر مکانیزم برای غیرفعال کردن بافرینگ باید به‌طور واضح در رابط جدا شود (برای مثال، با فراهم کردن یک سازنده مختلف برای FileInputStream یا از طریق یک متد که بافرینگ را غیرفعال یا جایگزین می‌کند)، به‌طوری که بیشتر توسعه‌دهندگان حتی نیازی به آگاهی از وجود آن نداشته باشند.

در مقابل، طراحان فراخوانی‌های سیستم Unix حالت رایج را ساده کردند. به‌عنوان مثال، آن‌ها تشخیص دادند که I/O ترتیبی رایج‌ترین حالت است، بنابراین آن را به‌عنوان رفتار پیش‌فرض قرار دادند. دسترسی تصادفی هنوز نسبتاً آسان است، با استفاده از فراخوانی سیستم lseek، اما توسعه‌دهنده‌ای که فقط به دسترسی ترتیبی نیاز دارد، نیازی به آگاهی از آن مکانیزم ندارد. اگر یک رابط ویژگی‌های زیادی داشته باشد، اما بیشتر توسعه‌دهندگان فقط باید از چند مورد از آن‌ها آگاه باشند، پیچیدگی مؤثر آن رابط فقط پیچیدگی ویژگی‌های رایج استفاده‌شده است.

 

۴.۸ نتیجه‌گیری

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

  1. زبان‌هایی وجود دارند که عمدتاً در جامعه تحقیقاتی استفاده می‌شوند و در آن‌ها می‌توان رفتار کلی یک متد یا تابع را به‌طور رسمی با استفاده از یک زبان مشخصات توصیف کرد. این مشخصات می‌توانند به‌طور خودکار بررسی شوند تا اطمینان حاصل شود که با پیاده‌سازی مطابقت دارند. سوال جالب این است که آیا چنین مشخصات رسمی می‌توانند بخش‌های غیررسمی رابط را جایگزین کنند. نظر فعلی من این است که رابطی که به زبان انگلیسی توصیف شده باشد احتمالاً برای توسعه‌دهندگان شهودی‌تر و قابل‌فهم‌تر از رابطی است که با زبان مشخصات رسمی نوشته شده باشد.