به عنوان برنامه نویس، یکی از تصمیماتی که در نوشتن هر ماژول جدید با آن روبرو هستیم، این است که ماژول را به صورت عام منظوره (general-purpose) طراحی کنیم یا خاص منظوره (special-purpose). بعضی ها معتقد هستند، ماژول ها باید عام منظوره طراحی شوند، طوری که مکانیزمی را فراهم کنند که نه تنها پاسخگوی نیازهای امروز باشد، بلکه نیازهای بیشمار آینده را هم پیش بینی کند. این افراد معتقدند که با این روش، در وقت صرفه جویی خواهد شد، چرا که در آینده می توانیم به راحتی از ماژول هایی که قبلا نوشته شده اند، برای پاسخ به نیازهای جدید نرم افزار، استفاده کنیم.
از سوی دیگر، می دانیم که پیش بینی نیازهای آینده ی یک سیستم نرم افزاری، بسیار دشوار است، بنابراین یک راه حلِ عام منظوره، ممکن است شامل مواردی باشد، که هرگز استفاده نخواهند شد. علاوه بر این، ممکن است این راه حل ها روش خوبی برای حل مساله ای که امروز داریم نباشند و احتمال دارد که، کیفیت کار امروز را فدای پیش بینی های آینده کنیم. به همین دلیل، بعضی دیگر معتقدند، که بهتر است روی نیازهای امروز تمرکز کنیم، طراحی را بر مبنای آنچه امروز می دانیم انجام دهیم و "چو فردا رسد کار فردا کنیم". در آینده هر زمان که نیازهای دیگری کشف شد، می توان کد را refactor کرد و آن نیاز را پاسخ داد.
ولی شاید خیلی نباید به این مساله سیاه و سفید نگاه کرد. در این نوشته می خواهیم یک روشِ میانه برای طراحی ماژول ها را بررسی کنیم، به این صورت که عملکرد (functionality) ماژول، فقط منعکس کننده ی نیاز فعلی باشد، ولی واسط آن، به اندازه ای عام منظوره طراحی شود که موارد استفاده ی متعددی را پاسخگو باشد. در واقع واسط باید برای نیازهای فعلی، به راحتی قابل استفاده باشد، بدون این که به آن ها گره خورده باشد. یک نکته ی مهم این است که میزان عام منظوره بودن را به درستی تشخیص دهیم، آن قدر واسط را عام منظوره طراحی نکنیم که حتی به درد نیازهای فعلی هم نخورد.
مهم ترین فایده ی رویکرد عام منظوره این است که نتیجه ی آن، واسط های ساده تر و ماژول های عمیق تر خواهد بود. همچنین در آینده می توان از آن ماژول برای اهداف دیگر استفاده کرد و در زمان صرفه جویی کرد، ولی این نباید هدف ما از طراحی ماژول های عام منظوره باشد. یعنی حتی اگر نخواهیم در آینده از یک ماژول استفاده کنیم، طراحی عام منظوره ی آن، به خاطر کمک به سادگی، بهتر است.
شاید بررسی این مساله با یک مثال، به فهم بهتر آن کمک کند. فرض کنید می خواهیم یک نرم افزار ویرایشگر متن (مانند Notepad یا Word) طراحی کنیم. یک راه حل این است که، یک UI برای آن در نظر بگیریم و یک کلاس برای مدیریت متن. فرض کنیم نام کلاس را Text می گذاریم. این کلاس قرار است تمام متدهای مورد نیاز برای عملکرد نرم افزار را فراهم کند.
روش اول
قطعا هر کاربر که با نرم افزار ویرایشگر متن کار می کند، نیاز دارد که قسمتی از متن را پاک کند. فرض کنیم UI نرم افزار فرضی ما، دو دکمه برای پاک کردن متن دارد، یکی Delete و یکی Backspace (مانند صفحه کلید). عملکرد Delete به این صورت است که یک کاراکتر به سمت راست را حذف می کند و Backspace یک کاراکتر به سمت چپ.
در روش اول، کلاس Text شامل دو متد زیر است:
void Backspace(Cursor cursor); void Delete(Cursor cursor);
متد Backspace قرار است یک کاراکتر به سمت چپ را پاک کند و متد Delete، یک کاراکتر به سمت راست. در این متدها، ورودی، از نوع Cursor است، یعنی مکانی که نشانگر در آن قرار دارد (که از آن جا قرار است یک کاراکتر شمارش شود).
احتمالا زمان هایی هم هست که نیاز داریم یک بخش از متن را حذف کنیم نه فقط یک کاراکتر، پس متد زیر را هم به واسط کلاس Text اضافه می کنیم:
void DeleteSelection(Selection selection);
کاری که تا اینجا کردیم این است که، به ازای تمام نیازهای UI، یعنی تمام ویژگی هایی که کاربر قرار است ببیند، متدهایی در کلاس Text اضافه کرده ایم. اگر بخواهیم به همین صورت پیش برویم، کلاس Text، تبدیل به یک کلاس کم عمق خواهد شد، که فقط برای آن UI به خصوص قابل استفاده خواهد بود (یعنی خاص منظوره است). مشکل دیگر این کلاس این است که، برنامه نویسی که روی کلاس Text کار می کند، باید بار شناختی زیادی را تحمل کند، تا با متدهایی آشنا شود، که بنا بر نیازهای UI نوشته شده اند و این برنامه نویس هیچ دانشی از آنها ندارد. همچنین برنامه نویسی که روی UI کار می کند، باید تعداد زیادی متد را از کلاس Text بشناسند و بداند هر کدام را کجا استفاده کند.
این رویکرد، باعث نشت اطلاعات، بین UI و کلاس Text می شود. مفاهیمی که کاملا مرتبط با UI هستند، مثل Delete و Backspace و تفاوت این دو، در کلاس Text منعکس شده اند. هر تغییری در UI، منتج به تغییراتی روی کلاس Text خواهد شد و توسعه ی یکی، بدون تغییر دیگری امکان پذیر نخواهد بود.
یک روش بهتر
روش دوم این است که کلاس Text را عام منظوره بنویسیم. واسط آن، باید شامل عملکردهایِ ابتداییِ مرتبط با یک متن باشد و اصلا دغدغه ی این که UI چه عملیاتی قرار است انجام دهد نداشته باشد.
برای مثال فرض کنید فقط دو متد زیر را در کلاس Text داشته باشیم:
Position changePosition(Position position, int numChars); void Delete(Position start, Position end);
متد اول، یک position جدید، که به اندازه ی تعداد مشخصی کاراکتر با position ورودی فرق دارد را به عنوان خروجی بر می گرداند. منفی یا مثبت بودن مقدار numChars مشخص کننده ی جهت حرکت position است. متد دوم، کاراکترهایی که بعد از پارامتر start و قبل از پارامتر end هستند را حذف می کند. همچنین در این متدها، از نوع Position به جای Cursor استفاده شده، که به یک UI خاص اشاره نمی کند (چرا که ممکن است یک UI وجود داشته باشد که cursor در آن معنی ندارد).
با این دو متد که تعریف کردیم، هر UI می تواند به روش زیر، Delete را پیاده سازی کند:
text.delete(cursor, text.changePosition(cursor, 1));
و Backspace را به روش زیر:
text.delete(text.changePosition(cursor, -1), cursor);
درست است که کدها طولانی تر به نظر می رسند، ولی با روش جدید، برنامه نویسی که روی UI کار می کند، با توجه به عملکردی که قرار است برای کاربر طراحی شود، می تواند تصمیم بگیرد که چگونه از ویژگی های کلاس Text استفاده کند. در روش قبلی، برنامه نویس UI، برای این که بداند متد Backspace دقیقا چه می کند، باید به کلاس Text مراجعه می کرد و اسناد یا کد آن را مطالعه می کرد تا رفتار آن را بفهمد. همچنین در روش جدید، کلاس Text تعداد کمی متد خواهد داشت که می توانند تعداد زیادی از نیازهای UI های مختلف را پاسخ بدهند.
در این مثال، با استفاده از رویکرد عام منظوره، توانستیم بین کلاس Text و UI، جداسازی انجام دهیم، و این به پنهان سازی اطلاعات کمک می کند. کلاس Text نیازی ندارد جزئیات UI را بداند، مثلا این که تفاوت کلید Delete و BackSpace چیست. همچنین، می توانیم بدون این که کلاس Text را تغییر دهیم، به UI ویژگی های جدید اضافه کنیم، برای مثال، کاربر بتواند با یک دستور، 4 کاراکتر را به سمت راست حذف کند. برای پاسخ دادن به این نیاز UI، برنامه نویس UI بدون نیاز به دانش جدیدی از کلاس Text ، می تواند این بخش از UI را توسعه دهد.
یکی از مهم ترین عناصر طراحی نرم افزار، این است که تشخیص دهیم، چه کسی، چه چیزی را باید در چه زمانی بداند. وقتی جزئیات مهم هستند، بهتر است تا جای ممکن آن ها را شفاف کنیم. مثلا در روش اول، پیاده سازیِ متد Backspace، یک اشتباه در طراحی است. در نگاه اول، به نظر می رسد این متد برای این به وجود آمده، که اطلاعاتی را پنهان کند، اطلاعات درباره ی این که چه کاراکترهایی قرار است حذف شوند، ولی در واقع UI برای استفاده از این متد، باید دقیقا همان اطلاعاتی که قرار است پنهان شود را بداند! برنامه نویس UI، باید کد مربوط به این متد را ببیند، تا بداند کجا باید از آن استفاده کند.
چه سوال هایی بپرسیم؟
برای پیدا کردن یک تعادل بین ماژول های عام منظوره یا خاص منظوره، سوال های زیر را از خودتان بپرسید:
اگر بتوانیم تعداد متدهای یک ماژول را، بدون کم کردن قابلیت های آن ماژول کم کنیم، احتمالا یک ماژول عام منظوره تر داریم. در مثال قبلی، رویکرد خاص منظوره، شامل سه متد برای حذف متن بود: Backspace و Delete و DeleteSelection. رویکرد عام منظوره تر، فقط یک متد برای حذف متن دارد که هر سه نیاز قبلی را پاسخ می دهد. دقت کنید که کم کردن تعداد متدها تا زمانی معنی دارد که واسط متد ساده بماند. اگر برای کاهش متدها مجبور شدیم تعداد زیادی argument به آن ها اضافه کنیم، احتمالا به جای ساده تر کردن کارها، در حال افزودن پیچیدگی هستیم.
2. یک متد در چه تعداد موقعیت استفاده می شود؟
اگر یک متد فقط برای استفاده ی مشخص طراحی می شود (مانند متد backspace)، احتمالا خیلی خاص منظوره است. پس شاید بتوان آن را به همراه چند متد خاص منظوره ی دیگه ادغام کرد و یک متد عام منظوره تر نوشت.
3. آیا استفاده از این ماژول برای استفاده ی فعلی نرم افزار، ساده است؟
اگر برای استفاده از یک کلاس، مجبور شویم مقدار زیادی کد بنویسیم، احتمالا واسط مناسبی طراحی نکرده ایم. در مثال خودمان، اگر فقط متد delete وجود داشت، استفاده کنندگان از این کلاس باید جهت حذف تعداد زیادی کاراکتر، loop های زیادی داشته باشند. بنابراین بهتر است این کلاس، متدهایی برای عملیات روی تعداد بیشتری از 1 کاراکتر داشته باشد.
واسط های عام منظوره، فوایدی نسبت به واسط های خاص منظوره دارند. آن ها ساده تر هستند، تعداد اجزای کمتری دارند و عمیق ترند. آن ها از نشت اطلاعات بین ماژول ها جلوگیری می کنند و به مدیریت پیچیدگی سیستم کمک خواهند کرد.