یکی از رایجترین تصمیماتی که هنگام طراحی یک ماژول جدید با آن مواجه میشوید، این است که آیا آن را بهصورت همهمنظوره پیادهسازی کنید یا خاصمنظوره. برخی ممکن است استدلال کنند که باید رویکرد همهمنظوره را در پیش بگیرید، یعنی یک مکانیزم پیادهسازی کنید که بتواند طیف گستردهای از مشکلات را پوشش دهد، نه فقط آنهایی که در حال حاضر اهمیت دارند. در این حالت، مکانیزم جدید ممکن است در آینده استفادههایی غیرمنتظره پیدا کند و در نتیجه، باعث صرفهجویی در زمان شود. رویکرد همهمنظوره با ذهنیت سرمایهگذاری که در فصل سوم مطرح شد سازگار به نظر میرسد، جایی که مقداری زمان بیشتر در ابتدا صرف میکنید تا در آینده زمان صرفهجویی شود.
از سوی دیگر، میدانیم که پیشبینی نیازهای آینده یک سیستم نرمافزاری دشوار است، بنابراین ممکن است یک راهحل همهمنظوره شامل قابلیتهایی باشد که هرگز واقعاً مورد استفاده قرار نمیگیرند. علاوه بر این، اگر چیزی بیش از حد همهمنظوره پیادهسازی شود، ممکن است در حل مشکل خاص امروز کارآمد نباشد. در نتیجه، برخی ممکن است استدلال کنند که بهتر است روی نیازهای امروز تمرکز کنیم، فقط چیزی را بسازیم که میدانیم لازم داریم، و آن را برای نحوه استفادهای که امروز برنامهریزی کردهایم، تخصصی کنیم. اگر رویکرد خاصمنظوره را در پیش بگیرید و بعداً استفادههای بیشتری کشف کنید، همیشه میتوانید آن را بازآرایی (refactor) کنید تا همهمنظوره شود. رویکرد خاصمنظوره با توسعه نرمافزار به صورت تدریجی سازگار به نظر میرسد.
کلاسها را تا حدی همهمنظوره بسازید
از دید من، نقطه تعادل مناسب این است که ماژولهای جدید را به صورت «تا حدی» همهمنظوره پیادهسازی کنیم. عبارت «تا حدی همهمنظوره» به این معنی است که عملکرد ماژول باید منعکسکننده نیازهای فعلی شما باشد، اما رابط آن نه. در عوض، رابط باید به اندازهای عمومی باشد که از کاربردهای مختلف پشتیبانی کند. رابط باید برای نیازهای امروز آسان و قابل استفاده باشد، بدون آنکه مستقیماً به آنها گره خورده باشد. واژه «تا حدی» اهمیت دارد: نباید آنقدر پیش بروید که چیزی بیش از حد عمومی بسازید که استفاده از آن برای نیازهای فعلی سخت شود.
مهمترین (و شاید شگفتانگیزترین) مزیت رویکرد همهمنظوره این است که به رابطهایی سادهتر و عمیقتر نسبت به رویکرد خاصمنظوره منجر میشود. رویکرد همهمنظوره همچنین میتواند در آینده در صورتی که کلاس را برای اهداف دیگر استفاده کنید، در زمان صرفهجویی کند. اما حتی اگر ماژول فقط برای هدف اصلی خود استفاده شود، باز هم رویکرد همهمنظوره به دلیل سادگیاش بهتر است.
مثال: ذخیره متن برای یک ویرایشگر
بیایید مثالی از یک کلاس طراحی نرمافزار بررسی کنیم که در آن از دانشجویان خواسته شده بود ویرایشگرهای متنی گرافیکی سادهای بسازند. این ویرایشگرها باید یک فایل را نمایش میدادند و به کاربران اجازه میدادند تا با کلیک، اشاره و تایپ، فایل را ویرایش کنند. این ویرایشگرها باید از چندین نمای همزمان از یک فایل در پنجرههای مختلف پشتیبانی میکردند؛ همچنین باید از قابلیت بازگردانی و تکرار چندسطحی (undo/redo) برای تغییرات فایل پشتیبانی میکردند.
هر یک از پروژههای دانشجویان شامل کلاسی بود که متن زیربنایی فایل را مدیریت میکرد. کلاسهای متنی معمولاً متدهایی برای بارگذاری فایل در حافظه، خواندن و ویرایش متن فایل، و نوشتن مجدد متن ویرایششده به فایل ارائه میدادند.
بسیاری از تیمهای دانشجویی APIهایی خاصمنظوره برای کلاس متن پیادهسازی کرده بودند. آنها میدانستند که این کلاس قرار است در یک ویرایشگر تعاملی استفاده شود، بنابراین به قابلیتهایی که ویرایشگر باید ارائه دهد فکر کردند و API کلاس متن را برای آن ویژگیهای خاص طراحی کردند. برای مثال، اگر کاربر در ویرایشگر کلید backspace را فشار میداد، ویرایشگر کاراکتر بلافاصله سمت چپ مکاننما را حذف میکرد؛ اگر کلید delete فشار داده میشد، کاراکتر بلافاصله سمت راست مکاننما حذف میشد. با علم به این موضوع، برخی تیمها برای هر یک از این ویژگیهای خاص، متدی در کلاس متن ایجاد کردند:
void backspace(Cursor cursor);
void delete(Cursor cursor);
هر یک از این متدها موقعیت مکاننما را بهعنوان آرگومان میگرفتند؛ یک نوع خاص به نام Cursor
این موقعیت را نمایش میداد. همچنین ویرایشگر باید از یک انتخاب (selection) پشتیبانی میکرد که میتوانست کپی یا حذف شود. دانشجویان این را با تعریف یک کلاس Selection
و ارسال شیئی از این کلاس به کلاس متن هنگام حذف مدیریت کردند:
void deleteSelection(Selection selection);
احتمالاً دانشجویان فکر میکردند که پیادهسازی رابط کاربری آسانتر خواهد بود اگر متدهای کلاس متن با ویژگیهایی که برای کاربران قابل مشاهده است، تطابق داشته باشد. اما در واقع، این تخصصیسازی مزیت چندانی برای کد رابط کاربری نداشت و بار ذهنی بالایی برای توسعهدهندگانی که روی رابط کاربری یا کلاس متن کار میکردند ایجاد کرد. کلاس متن در نهایت شامل تعداد زیادی متد سطحی شد که هر یک فقط برای یک عملیات رابط کاربری مناسب بودند. بسیاری از متدها، مانند delete
، فقط در یک جای خاص فراخوانی میشدند. در نتیجه، توسعهدهندهای که روی رابط کاربری کار میکرد باید تعداد زیادی از متدهای کلاس متن را یاد میگرفت.
این رویکرد منجر به نشت اطلاعات بین رابط کاربری و کلاس متن شد. انتزاعهایی مرتبط با رابط کاربری، مانند انتخاب یا کلید backspace، در کلاس متن منعکس شده بودند؛ این موضوع بار ذهنی برای توسعهدهندگانی که روی کلاس متن کار میکردند را افزایش داد. هر عملیات جدید در رابط کاربری نیازمند تعریف متد جدیدی در کلاس متن بود، بنابراین توسعهدهندهای که روی رابط کاربری کار میکرد، به احتمال زیاد مجبور به کار روی کلاس متن نیز میشد. یکی از اهداف طراحی کلاس این است که هر کلاس بتواند بهطور مستقل توسعه یابد، اما رویکرد خاصمنظوره، کلاس رابط کاربری و کلاس متن را به یکدیگر وابسته کرده بود.
یک API همهمنظورهتر
رویکرد بهتر این است که کلاس متن را عمومیتر طراحی کنیم. API آن باید فقط بر اساس ویژگیهای پایهای متن تعریف شود، بدون اینکه عملیات سطح بالاتری که قرار است با آن پیادهسازی شوند را منعکس کند. برای مثال، تنها دو متد برای ویرایش متن کافی هستند:
void insert(Position position, String newText);
void delete(Position start, Position end);
متد اول یک رشته دلخواه را در یک موقعیت دلخواه از متن وارد میکند، و متد دوم تمام کاراکترهایی را که در موقعیتهایی بزرگتر یا مساوی start
و کوچکتر از end
هستند، حذف میکند. این API همچنین از نوع عمومیتری به نام Position
به جای Cursor
استفاده میکند، که بازتابدهندهی یک رابط کاربری خاص بود. کلاس متن همچنین باید قابلیتهای همهمنظورهای برای کار با موقعیتها درون متن فراهم کند، مانند:
Position changePosition(Position position, int numChars);
این متد یک موقعیت جدید را بازمیگرداند که به اندازه تعداد مشخصی کاراکتر با موقعیت اولیه فاصله دارد. اگر آرگومان numChars
مثبت باشد، موقعیت جدید جلوتر از موقعیت قبلی در فایل خواهد بود؛ اگر منفی باشد، عقبتر خواهد بود. این متد در صورت لزوم بهطور خودکار به خط بعد یا قبلی پرش میکند. با استفاده از این متدها، کلید delete میتواند با کد زیر پیادهسازی شود (با فرض اینکه متغیر cursor
موقعیت فعلی مکاننما را نگه میدارد):
text.delete(cursor, text.changePosition(cursor, 1));
به همین ترتیب، کلید backspace میتواند به شکل زیر پیادهسازی شود:
text.delete(text.changePosition(cursor, -1), cursor);
با استفاده از API متنی همهمنظوره، کدی که عملیات رابط کاربری مانند delete و backspace را پیادهسازی میکند، کمی طولانیتر از رویکرد اولیه با API خاصمنظوره خواهد بود. با این حال، کد جدید از نظر درک بسیار واضحتر از کد قدیمی است. یک توسعهدهنده که روی ماژول رابط کاربری کار میکند، احتمالاً برایش مهم است که بداند کدام کاراکترها توسط کلید backspace حذف میشوند. با کد جدید، این موضوع بهوضوح مشخص است. اما در کد قدیمی، توسعهدهنده باید به کلاس متن مراجعه میکرد و مستندات و/یا کد متد backspace
را میخواند تا از رفتار آن مطمئن شود. علاوه بر این، رویکرد همهمنظوره به طور کلی کد کمتری نسبت به رویکرد خاصمنظوره دارد، زیرا تعداد زیادی متد خاص را در کلاس متن با تعداد کمتری متد عمومی جایگزین میکند.
کلاس متنی که با رابط همهمنظوره پیادهسازی شده باشد، میتواند علاوه بر یک ویرایشگر تعاملی، برای اهداف دیگری نیز مورد استفاده قرار گیرد. برای مثال، فرض کنید در حال ساخت یک برنامه هستید که یک فایل مشخص را طوری ویرایش میکند که تمام وقوعهای یک رشته خاص را با رشتهای دیگر جایگزین کند. متدهای کلاس متن خاص، مانند backspace
و delete
، در این برنامه ارزشی نخواهند داشت. اما کلاس متن همهمنظوره، در حال حاضر اکثر قابلیتهای مورد نیاز این برنامه جدید را دارد. تنها چیزی که کم است، متدی برای جستجوی وقوع بعدی یک رشته خاص است، مانند:
Position findNext(Position start, String string);
البته، یک ویرایشگر متنی تعاملی نیز به احتمال زیاد مکانیزمی برای جستجو و جایگزینی خواهد داشت، که در این صورت کلاس متن از قبل شامل این متد نیز خواهد بود.
عمومی بودن باعث پنهانسازی بهتر اطلاعات میشود
رویکرد همهمنظوره جداسازی تمیزتری بین کلاس متن و کلاس رابط کاربری ایجاد میکند، که این به پنهانسازی بهتر اطلاعات منجر میشود. کلاس متن نیازی ندارد از جزئیات رابط کاربری آگاه باشد، مانند اینکه کلید backspace چگونه مدیریت میشود؛ این جزئیات اکنون در کلاس رابط کاربری کپسوله شدهاند. ویژگیهای جدید رابط کاربری را میتوان بدون نیاز به ایجاد توابع پشتیبان جدید در کلاس متن اضافه کرد. رابط همهمنظوره همچنین بار شناختی را کاهش میدهد: توسعهدهندهای که روی رابط کاربری کار میکند، تنها نیاز دارد چند متد ساده را یاد بگیرد که میتوان آنها را در کاربردهای مختلف استفاده کرد.
متد backspace
در نسخه اولیه کلاس متن یک انتزاع نادرست بود. وانمود میکرد که اطلاعات مربوط به کاراکترهایی که حذف میشوند را پنهان میکند، اما ماژول رابط کاربری واقعاً به دانستن این اطلاعات نیاز دارد؛ توسعهدهندگان رابط کاربری احتمالاً کد متد backspace
را خواهند خواند تا از رفتار دقیق آن مطمئن شوند. قرار دادن این متد در کلاس متن فقط باعث میشود توسعهدهندگان رابط کاربری برای دسترسی به اطلاعات مورد نیاز خود کار سختتری داشته باشند. یکی از مهمترین عناصر طراحی نرمافزار این است که مشخص کنیم چه کسی، چه چیزی را، و در چه زمانی باید بداند. وقتی جزئیات اهمیت دارند، بهتر است آنها را صریح و تا حد امکان واضح بیان کنیم، مانند پیادهسازی بازنگریشدهی عملیات backspace
. پنهان کردن این اطلاعات پشت یک رابط فقط باعث ابهام میشود.
سوالاتی که باید از خود بپرسید
تشخیص طراحی تمیز یک کلاس همهمنظوره آسانتر از ایجاد آن است. در ادامه چند سوال آمده است که میتوانید از خود بپرسید و به شما کمک میکند تا تعادل مناسب بین همهمنظوره بودن و خاصمنظوره بودن یک رابط را پیدا کنید:
سادهترین رابطی که تمام نیازهای فعلی من را پوشش میدهد چیست؟
اگر تعداد متدهای یک API را کاهش دهید بدون اینکه قابلیت کلی آن را کاهش دهید، احتمالاً در حال ایجاد متدهایی همهمنظورهتر هستید. API خاص کلاس متن حداقل سه متد برای حذف متن داشت: backspace
، delete
، و deleteSelection
. API همهمنظوره فقط یک متد برای حذف متن داشت که هر سه هدف را پوشش میداد. کاهش تعداد متدها فقط زمانی منطقی است که رابط هر متد ساده باقی بماند؛ اگر برای کاهش تعداد متدها مجبور شوید تعداد زیادی آرگومان اضافی معرفی کنید، ممکن است در واقع در حال پیچیدهتر کردن کار باشید.
این متد در چند موقعیت مورد استفاده قرار خواهد گرفت؟
اگر متدی فقط برای یک کاربرد خاص طراحی شده باشد، مانند متد backspace
، این میتواند هشداردهنده باشد که بیش از حد خاصمنظوره است. ببینید آیا میتوانید چند متد خاص را با یک متد همهمنظوره جایگزین کنید.
آیا این API برای نیازهای فعلی من استفادهپذیر است؟
این سوال به شما کمک میکند تشخیص دهید که آیا در سادهسازی و عمومیسازی بیش از حد پیش رفتهاید یا نه. اگر مجبور باشید مقدار زیادی کد اضافی بنویسید تا بتوانید از یک کلاس برای هدف فعلی خود استفاده کنید، این یک علامت هشدار است که رابط عملکرد مناسب را فراهم نمیکند. برای مثال، یک رویکرد برای کلاس متن این است که آن را حول عملیات تککاراکتری طراحی کنیم: insert
فقط یک کاراکتر را درج کند و delete
فقط یک کاراکتر را حذف کند. این API هم ساده است و هم همهمنظوره.
با این حال، استفاده از آن برای یک ویرایشگر متن چندان آسان نخواهد بود: کد سطح بالاتر شامل تعداد زیادی حلقه برای درج یا حذف محدودهای از کاراکترها خواهد بود. همچنین، این رویکرد در عملیات بزرگ ناکارآمد خواهد بود. بنابراین بهتر است کلاس متن پشتیبانی داخلی برای عملیات روی بازهای از کاراکترها داشته باشد.
نتیجهگیری
رابطهای همهمنظوره مزایای زیادی نسبت به رابطهای خاصمنظوره دارند. آنها معمولاً سادهتر هستند، با تعداد متدهای کمتر که عمیقترند. آنها همچنین جداسازی تمیزتری بین کلاسها فراهم میکنند، در حالی که رابطهای خاصمنظوره باعث نشت اطلاعات بین کلاسها میشوند. ساختن ماژولهای «تا حدی» همهمنظوره یکی از بهترین راهها برای کاهش پیچیدگی کلی سیستم است.