بوها و اصول (Smells and Heuristics)
در کتاب فوقالعادهاش به نام Refactoring،
مارتین فاولر مجموعهای از «بوی بد کد» (Code Smells) را معرفی کرده است. فهرستی که در ادامه آمده است، شامل بسیاری از بوهای بد معرفیشده توسط مارتین فاولر است و موارد دیگری را نیز که خودم در کارم از آنها استفاده میکنم، به آن اضافه کردهام. همچنین این فهرست شامل نکات و قواعد مفیدی است که من در عمل توسعه نرمافزار از آنها بهره میبرم.
این فهرست را با مرور چند برنامه مختلف و بازآرایی (Refactor
) آنها جمعآوری کردهام. با هر تغییری که ایجاد میکردم، از خودم میپرسیدم چرا این تغییر را اعمال کردم و دلیل آن را در اینجا مینوشتم. حاصل کار فهرستی نسبتاً بلند از چیزهایی است که وقتی کدی را میخوانم، به نظر من بدبو میآیند.
این فهرست هم برای خواندن متوالی از بالا به پایین طراحی شده و هم به عنوان یک مرجع کاربرد دارد. در «ضمیمه C» در صفحه 409، برای هر قاعدهای ارجاعی وجود دارد که نشان میدهد آن قاعده در کجای متن اصلی مورد استفاده قرار گرفته است.
نظرات (Comments
)
C1: اطلاعات نامناسب
درج اطلاعاتی در نظرها که بهتر است در سیستمهای دیگری مانند سیستم کنترل نسخه، سیستم پیگیری خطا یا هر سیستم ثبت اطلاعات دیگر قرار گیرد، نامناسب است. به عنوان مثال، تاریخچه تغییرات فقط فایلهای منبع را با حجم زیادی از متن تاریخی و غیرضروری شلوغ میکند. بهطور کلی، متادیتاهایی مانند نویسنده، تاریخ آخرین تغییر، شماره SPR و مانند آن نباید در نظرات قرار گیرند. نظرات باید صرفاً برای نکات فنی مربوط به کد و طراحی استفاده شوند.
C2: نظر منسوخ
نظری که قدیمی، بیربط یا نادرست شده باشد، یک نظر منسوخ است. نظرات به سرعت منسوخ میشوند. بهتر است اصلاً نظری ننویسید که بهزودی منسوخ شود. اگر نظری منسوخ را پیدا کردید، یا آن را بهروزرسانی کنید یا هر چه سریعتر حذفش کنید. نظرات منسوخ معمولاً از کدی که زمانی شرحش میدادند، فاصله میگیرند و به جزایری شناور از بیربطی و گمراهی تبدیل میشوند.
C3: نظر تکراری
نظری که چیزی را توصیف کند که خودش به اندازه کافی گویاست، یک نظر تکراری است. برای مثال:
i++; // increase i by 1
یا Javadoc
هایی که چیزی بیش از امضای تابع نمیگویند:
/**
* @param sellRequest
* @return
* @throws ManagedComponentException
*/
public SellResponse beginSellItem(SellRequest sellRequest) throws ManagedComponentException
کامنت باید چیزهایی را بیان کند که کد بهتنهایی نمیتواند.
C4: کامنت بد نوشتهشده
کامنتی که ارزش نوشتن دارد، ارزش خوب نوشتن هم دارد. اگر قرار است کامنتی بنویسید، وقت بگذارید تا آن را به بهترین شکل ممکن بنویسید. واژهها را با دقت انتخاب کنید. دستور زبان و نگارش را رعایت کنید. از زیادهگویی و بدیهیات بپرهیزید. مختصر و مفید بنویسید.
C5: کد کامنتشده
دیدن قطعاتی از کد که کامنت شدهاند، من را دیوانه میکند. کسی نمیداند که چقدر قدیمیاند یا هنوز معنا دارند یا نه. ولی هیچکس هم جرات حذفشان را ندارد چون فکر میکند شاید کس دیگری به آن نیاز دارد یا برنامهای برایش دارد.
این کدها در گذر زمان بیشتر و بیشتر از ارتباط خارج میشوند. توابعی را صدا میزنند که دیگر وجود ندارند، از متغیرهایی استفاده میکنند که نامشان عوض شده، و از قراردادهایی پیروی میکنند که منسوخ شدهاند. این کدهای مرده ماژولها را آلوده کرده و خواندن کد را سخت میکنند.
وقتی کد کامنتشده دیدید، حذفش کنید! نگران نباشید، سیستم کنترل نسخه هنوز آن را به یاد دارد. اگر کسی واقعاً به آن نیاز داشته باشد، میتواند نسخه قبلی را بررسی کند. اجازه ندهید کد کامنتشده زنده بماند.
محیط (Environment)
E1: ساخت (Build) بیش از یک مرحله لازم دارد
ساختن یک پروژه باید یک عملیات ساده و تکمرحلهای باشد. نباید نیاز به چکاوت کردن اجزای مختلف از سیستم کنترل نسخه باشد. نباید دستورات پیچیده یا اسکریپتهای وابسته به محیط لازم باشد. همهچیز باید با یک دستور ساده قابل ساخت باشد:
svn get mySystem
cd mySystem
ant all
E2: تستها بیش از یک مرحله لازم دارند
باید بتوانید تمام تستهای واحد (unit test) را با یک دستور اجرا کنید. در بهترین حالت، با یک کلیک در IDE ؛ و در بدترین حالت با یک دستور ساده در ترمینال. اجرای تستها باید سریع، آسان و بدیهی باشد.
توابع (Functions)
F1: تعداد زیاد آرگومانها
توابع باید تعداد کمی آرگومان داشته باشند. بهترین حالت بدون آرگومان است، بعد یک، دو و سه. بیش از سه بسیار مشکوک است و باید با احتیاط شدید از آن پرهیز کرد. (بخش «آرگومانهای تابع» در صفحه 40 را ببینید.)
F2: آرگومانهای خروجی
آرگومانهای خروجی گمراهکننده هستند. خوانندهها انتظار دارند آرگومانها ورودی باشند، نه خروجی. اگر تابعی باید حالتی را تغییر دهد، این تغییر باید روی شیئی که روی آن صدا زده شده اعمال شود.
F3: آرگومانهای پرچمی (Flag Arguments)
آرگومانهای بولی نشاندهنده این هستند که تابع بیش از یک کار انجام میدهد. آنها گیجکنندهاند و باید حذف شوند.
F4: توابع مرده
توابعی که هیچوقت صدا زده نمیشوند، باید حذف شوند. نگهداشتن کد مرده بیهوده است. از حذف این توابع نترسید. سیستم کنترل نسخه آنها را به یاد دارد.
عمومی (General)
G1: چند زبان در یک فایل مبدا
محیطهای برنامهنویسی مدرن امروزی این امکان را فراهم کردهاند که زبانهای مختلفی را در یک فایل سورس قرار دهیم. برای مثال، یک فایل سورس Java
ممکن است قطعاتی از XML،
HTML
، YAML
، JavaDoc
، انگلیسی، JavaScript
و غیره را شامل شود. همچنین، یک فایل JSP
در کنار HTML
ممکن است شامل Java
, نگارش تگهای کتابخانهای، نظرات به زبان انگلیسی، JavaDoc،
XML،
JavaScript
و غیره باشد. این کار در بهترین حالت گیجکننده و در بدترین حالت، کاملاً بیدقت و شلخته است.
ایدهآل این است که یک فایل سورس فقط شامل یک زبان باشد. البته در عمل، ممکن است نیاز به استفاده از بیش از یک زبان داشته باشیم. اما باید تا حد امکان تعداد و میزان زبانهای اضافی را در فایلهای سورس خود کاهش دهیم.
G2: رفتار واضح پیادهسازی نشده است
بر اساس «اصل کمترین شگفتی»، هر تابع یا کلاسی باید رفتارهایی را پیادهسازی کند که یک برنامهنویس دیگر بهطور منطقی از آن انتظار دارد. برای مثال، تابعی را در نظر بگیرید که نام یک روز را به یک enum
معادل آن روز تبدیل میکند:
Day day = DayDate.StringToDay(String dayName);
انتظار میرود که رشتهی "Monday
" به Day.MONDAY
تبدیل شود. همچنین، انتظار داریم مخففهای رایج نیز پشتیبانی شوند و حساسیت به حروف بزرگ و کوچک نادیده گرفته شود.
اگر یک رفتار بدیهی پیادهسازی نشده باشد، خوانندگان و کاربران کد دیگر نمیتوانند به شهود خود در مورد نام توابع اعتماد کنند. آنها اعتماد خود را به نویسندهی اصلی از دست میدهند و مجبور میشوند کد را خطبهخط بخوانند تا عملکرد آن را بفهمند.
G3: رفتار نادرست در مرزها
واضح است که کد باید بهدرستی عمل کند، اما مشکل اینجاست که بهندرت درک میکنیم که عملکرد صحیح چقدر میتواند پیچیده باشد. توسعهدهندگان معمولاً توابعی مینویسند که فکر میکنند درست عمل میکند و بیشتر به شهود خود تکیه میکنند تا اینکه به خود زحمت دهند تا اثبات کنند کد در تمامی موارد مرزی و گوشهای درست عمل میکند.
هیچ جایگزینی برای دقت و وسواس وجود ندارد. هر شرط مرزی، هر مورد گوشهای، هر استثناء و نکتهی خاص میتواند یک الگوریتم زیبا و شهودی را دچار مشکل کند. به شهود خود اکتفا نکنید. بهدنبال تمام شرایط مرزی بگردید و برای هرکدام تست بنویسید.
G4: غیرفعال کردن مکانیزمهای ایمنی
چرنوبیل به این دلیل منفجر شد که مدیر نیروگاه یکییکی مکانیزمهای ایمنی را غیرفعال کرد. این ایمنیها اجرای یک آزمایش را دشوار کرده بودند. نتیجه این شد که نه تنها آزمایش اجرا نشد، بلکه جهان شاهد نخستین فاجعهی هستهای غیرنظامی بود.
غیرفعالکردن ایمنیها خطرناک است. کنترل دستی بر serialVersionUID
ممکن است لازم باشد، اما همیشه همراه با ریسک است. خاموش کردن برخی (یا تمام) هشدارهای کامپایلر ممکن است باعث موفقیت ساخت پروژه شود، اما در عوض ممکن است به باگهایی بیپایان منجر شود. غیرفعال کردن تستهای شکستخورده و گفتن اینکه "بعداً درستش میکنم" مثل این است که با کارت اعتباری خرید کنید و وانمود کنید پولتان تمام نمیشود.
G5: تکرار
این یکی از مهمترین قوانین این کتاب است و باید آن را بسیار جدی بگیرید. تقریباً تمام نویسندگانی که درباره طراحی نرمافزار مینویسند به این قانون اشاره کردهاند. دیو توماس و اندی هانت آن را اصل DRY
(Don’t Repeat Yourself) نامیدند. کنت بک آن را بهعنوان یکی از اصول اصلی Extreme Programming معرفی کرد: «یکبار، و فقط یکبار». ران جفری این قانون را در رتبهی دوم قرار میدهد، درست بعد از اینکه همهی تستها باید پاس شوند.
هر بار که در کد تکراری میبینید، به این معناست که فرصت انتزاع را از دست دادهاید. آن تکرار میتواند به سابروتین یا شاید یک کلاس دیگر تبدیل شود. با تجمیع کدهای تکراری در یک انتزاع، واژگان زبان طراحی خود را گسترش میدهید. دیگر برنامهنویسان نیز میتوانند از این امکانات انتزاعی بهره ببرند. کدنویسی سریعتر و کمتر خطاپذیر میشود، چون سطح انتزاع را بالا بردهاید.
واضحترین نوع تکرار، تکههای مشابه کدی هستند که چندین بار در فایل کپی و پیست شدهاند. اینها باید به متدهای ساده تبدیل شوند.
نوع ظریفتر آن، بلوکهای تکرارشوندهی switch/case
یا if/else
است که در ماژولهای مختلف با همان شرایط دیده میشوند. این موارد باید با پلیمورفیسم جایگزین شوند.
حتی الگوریتمهای مشابهی که خطوط کد یکسانی ندارند، نوعی تکرار محسوب میشوند. برای رفع آنها میتوان از الگوهایی مانند TEMPLATE METHOD
یا STRATEGY
استفاده کرد.
در واقع بیشتر الگوهای طراحی که در ۱۵ سال گذشته معرفی شدهاند، صرفاً روشهایی برای حذف تکرار هستند. نرمالسازی کد (Codd Normal Forms) نیز روشی برای حذف تکرار در طراحی پایگاه داده است. شیگرایی (OO
) نیز راهی برای حذف تکرار است. برنامهنویسی ساختیافته نیز همینطور.
فکر میکنم دیگر مطلب روشن شده است: هرجا تکرار دیدید، آن را حذف کنید.
G6: کد در سطح نادرست انتزاع
مهم است که مفاهیم سطح بالا و عمومی را از مفاهیم جزئی و سطح پایین جدا کنیم. گاهی این کار با استفاده از کلاسهای پایه (abstract
) برای مفاهیم عمومی و کلاسهای مشتق برای مفاهیم جزئی انجام میشود.
در چنین مواردی باید این جداسازی کامل باشد. تمامی مفاهیم سطح پایین باید در کلاسهای مشتق باشند و مفاهیم سطح بالا فقط در کلاس پایه. مثلاً ثابتها، متغیرها یا توابع کمکی که فقط به پیادهسازی جزئی مربوط میشوند نباید در کلاس پایه قرار گیرند. کلاس پایه نباید از آنها اطلاعی داشته باشد.
این قانون برای فایلهای سورس، ماژولها و کامپوننتها هم صدق میکند. طراحی خوب نرمافزار ایجاب میکند مفاهیم را از نظر سطح انتزاع از هم جدا کرده و در ظرفهای متفاوتی قرار دهیم. این ظرفها میتوانند کلاس پایه و مشتق، یا فایل سورس و ماژول و غیره باشند. مهم این است که مفاهیم با سطوح مختلف با هم ترکیب نشوند.
کد زیر را در نظر بگیرید:
public interface Stack {
Object pop() throws EmptyException;
void push(Object o) throws FullException;
double percentFull();
class EmptyException extends Exception {}
class FullException extends Exception {}
}
تابع percentFull
در سطح نادرست انتزاع قرار دارد. گرچه برای برخی پیادهسازیهای Stack، این مفهوم معنادار است، اما برای برخی دیگر اصلاً نمیتوان میزان پر بودن را اندازهگیری کرد. بنابراین این تابع بهتر است در یک واسط مشتقشده مانند BoundedStack
قرار گیرد.
شاید فکر کنید که میتوان تابع را طوری پیادهسازی کرد که در صورت بینهایت بودن پشته، صفر برگرداند. اما هیچ پشتهای واقعاً بینهایت نیست. مثلاً نمیتوان از وقوع OutOfMemoryException
فقط با بررسی اینکه stack.percentFull() < 50.0
است، جلوگیری کرد.
بازگرداندن عدد صفر در چنین حالتی، به نوعی دروغ گفتن است.
نکته این است که نمیتوان با دروغ یا تظاهر از یک انتزاع اشتباه فرار کرد. جداسازی صحیح انتزاعها یکی از سختترین وظایف برنامهنویسان است و راهحل سریع و سادهای برای آن وجود ندارد.
G7: کلاسهای پایهای که به کلاسهای مشتق خود وابستهاند
رایجترین دلیل برای تفکیک مفاهیم به کلاسهای پایه و مشتق این است که مفاهیم سطح بالای کلاس پایه مستقل از مفاهیم سطح پایین کلاس مشتق باشند. بنابراین، وقتی میبینیم یک کلاس پایه به نام کلاس مشتق اشاره میکند، باید به وجود مشکل شک کنیم.
بهطور کلی، کلاس پایه نباید هیچ اطلاعی از کلاسهای مشتق داشته باشد. البته استثنائاتی وجود دارد. مثلاً وقتی تعداد مشتقها ثابت است و کلاس پایه بین آنها انتخاب انجام میدهد—چیزی که در پیادهسازی ماشینهای حالت (FSM
) زیاد دیده میشود.
اما در چنین مواردی، مشتق و پایه بهشدت به هم وابستهاند و همیشه با هم در یک فایل (مثلاً یک jar
) منتشر میشوند.
در حالت کلی، میخواهیم کلاسهای مشتق و پایه در فایلهای جداگانه منتشر شوند. این کار باعث میشود سیستم به اجزای مستقل و قابل تغییر تقسیم شود. در نتیجه، تغییر در یکی از اجزا فقط روی همان جزء تأثیر میگذارد و نیازی به بازنشر اجزای دیگر نیست. این یعنی هزینهی نگهداری سیستم بهشدت کاهش مییابد.
G8: اطلاعات بیش از حد
ماژولهای خوشتعریف، واسطهایی بسیار کوچک دارند که با آنها میتوان کارهای زیادی انجام داد. اما ماژولهای بدتعریف واسطهایی گسترده دارند که مجبور میشوید برای انجام کارهای ساده، عملیات زیادی انجام دهید.
یک واسط خوب، توابع کمی برای استفاده در اختیار میگذارد، بنابراین کوپلینگ پایین است. اما یک واسط بد، توابع زیادی ارائه میدهد که باید از آنها استفاده کنید، در نتیجه کوپلینگ بالا میرود.
برنامهنویسان خوب یاد میگیرند که آنچه در واسط کلاسها و ماژولها فاش میکنند را محدود کنند. هرچه توابع یک کلاس کمتر، بهتر. هرچه متغیرهایی که یک تابع میشناسد کمتر، بهتر. هرچه متغیرهای نمونهی یک کلاس کمتر، بهتر.
اطلاعات خود را پنهان کنید. توابع کمکی، ثابتها و متغیرهای موقتی خود را مخفی کنید. کلاسهایی با توابع زیاد یا متغیرهای نمونهی فراوان ایجاد نکنید. متغیرها و توابع protected
زیادی برای زیرکلاسها تعریف نکنید.
تمرکز خود را روی واسطهای کوچک و فشرده نگه دارید تا کوپلینگ پایین باقی بماند.
G9: کد مرده
کد مرده، کدی است که اجرا نمیشود. این نوع کد را میتوان در بدنهی یک شرط if
یافت که شرطش هیچگاه اتفاق نمیافتد. یا در بلوک catch
از یک try
که هیچگاه استثنایی پرتاب نمیکند. یا در متدهای کمکیای که هرگز فراخوانی نمیشوند، یا در مواردی از دستور switch/case
که هیچگاه رخ نمیدهند.
مشکل کد مرده این است که به مرور زمان بوی بدی میگیرد. هرچه قدیمیتر باشد، بوی آن شدیدتر و زنندهتر میشود. دلیل آن این است که هنگام تغییر طراحی، این کد بهروزرسانی نمیشود. هنوز کامپایل میشود، اما با قوانین و الگوهای جدید هماهنگ نیست. این کد متعلق به زمانی است که سیستم به شکل دیگری طراحی شده بود.
وقتی با کد مرده مواجه شدید، کار درست را انجام دهید: آن را دفن کنید. یعنی حذفش کنید.
G10: جداسازی عمودی
متغیرها و توابع باید نزدیک به محل استفادهشان تعریف شوند. متغیرهای محلی باید درست بالای اولین استفادهشان تعریف شوند و دامنهی عمودی محدودی داشته باشند. نمیخواهیم متغیرهایی را داشته باشیم که صدها خط با محل استفادهشان فاصله دارند.
توابع private
باید درست زیر اولین محل استفادهشان تعریف شوند. اگرچه این توابع به کل کلاس تعلق دارند، اما همچنان باید فاصلهی عمودی بین فراخوانی و تعریف آنها را محدود نگه داریم. یافتن یک تابع private
باید فقط با یک نگاه به پایین از محل فراخوانی ممکن باشد.
G11: ناسازگاری
اگر کاری را به شیوهای خاص انجام میدهید، تمام موارد مشابه را نیز به همان شکل انجام دهید. این اصل برمیگردد به «اصل کمترین شگفتی». در انتخاب الگوها دقت کنید، و وقتی الگویی را انتخاب کردید، آن را در ادامه نیز دنبال کنید.
اگر در یک تابع از متغیری با نام response
برای نگهداری HttpServletResponse
استفاده کردهاید، در توابع دیگر نیز برای همین منظور از همان نام استفاده کنید. اگر نام تابعی را processVerificationRequest
گذاشتهاید، برای توابع مشابه بهتر است نامهایی مانند processDeletionRequest
انتخاب کنید.
چنین سازگاری سادهای، وقتی بهطور مداوم رعایت شود، باعث میشود کد بهشکل قابل توجهی خواناتر و قابل نگهداریتر شود.
G12: شلوغی (Clutter)
سازندهای (constructor
) بدون هیچ پیادهسازی چه فایدهای دارد؟ تنها کاری که میکند، شلوغ کردن کد با چیزهای بیمعناست. متغیرهایی که استفاده نمیشوند، توابعی که هرگز فراخوانی نمیشوند، کامنتهایی که اطلاعاتی اضافه نمیکنند و ... همگی شلوغی هستند و باید حذف شوند. فایلهای سورس خود را تمیز، منظم و عاری از شلوغی نگه دارید.
G13: کوپلینگ مصنوعی
چیزهایی که به هم وابسته نیستند نباید بهطور مصنوعی به هم مرتبط شوند. برای مثال، یک enum
عمومی نباید داخل یک کلاس خاص قرار بگیرد، چون در این صورت کل برنامه مجبور به شناختن آن کلاس خاص میشود. همین موضوع دربارهی توابع static
عمومی که در کلاسهای خاص قرار دارند نیز صدق میکند.
بهطور کلی، کوپلینگ مصنوعی ارتباطی است بین دو ماژول که هدف مستقیمی ندارد. این مسئله نتیجهی قرار دادن یک متغیر، ثابت یا تابع در مکانی موقتی ولی نامناسب است. این کار تنبلی و بیدقتی است.
زمان صرف کنید تا مکان درست برای توابع، ثوابت و متغیرها را پیدا کنید. آنها را در اولین جای راحتی که پیدا کردید نیندازید و رهایشان نکنید.
G14: حسادت به ویژگی (Feature Envy)
این مورد یکی از بویهای کد مطرحشده توسط مارتین فاولر است. متدهای یک کلاس باید بیشتر با متغیرها و توابع همان کلاس درگیر باشند، نه با اعضای سایر کلاسها.
وقتی متدی از getter
و setterهای
یک شیء دیگر استفاده میکند تا دادهی آن را دستکاری کند، درواقع به متغیرهای آن کلاس دیگر حسادت میورزد. گویی آرزو دارد خودش داخل آن کلاس باشد.
مثال:
public class HourlyPayCalculator {
public Money calculateWeeklyPay(HourlyEmployee e) {
int tenthRate = e.getTenthRate().getPennies();
int tenthsWorked = e.getTenthsWorked();
int straightTime = Math.min(400, tenthsWorked);
int overTime = Math.max(0, tenthsWorked - straightTime);
int straightPay = straightTime * tenthRate;
int overtimePay = (int)Math.round(overTime*tenthRate*1.5);
return new Money(straightPay + overtimePay);
}
}
متد calculateWeeklyPay
به اطلاعات درون HourlyEmployee
دست درازی میکند؛ یعنی به کلاس دیگری حسادت دارد.
در حالت ایدهآل باید از چنین وابستگیهایی اجتناب کنیم چون باعث افشای جزئیات درونی یک کلاس به کلاس دیگر میشود.
البته گاهی این حسادت اجتنابناپذیر است. مثلاً:
public class HourlyEmployeeReport {
private HourlyEmployee employee;
public HourlyEmployeeReport(HourlyEmployee e) {
this.employee = e;
}
String reportHours() {
return String.format(
"Name: %s\tHours:%d.%1d\n",
employee.getName(),
employee.getTenthsWorked()/10,
employee.getTenthsWorked()%10);
}
}
در اینجا متد reportHours
نیز حسادت دارد، اما منطقی است. چون قرار نیست HourlyEmployee
از فرمت گزارش چیزی بداند؛ این مسئولیت به آن مربوط نیست.
G15: آرگومانهای انتخابی (Selector Arguments)
آرگومانهایی مانند true/false
که در انتهای توابع قرار میگیرند اغلب آزاردهندهاند. چه چیزی را مشخص میکنند؟ اگر مقدارشان عوض شود چه تغییری رخ میدهد؟ این نوع آرگومانها چندین رفتار را در یک تابع ادغام میکنند، که نشانهی تنبلی در طراحی است.
مثال بد:
public int calculateWeeklyPay(boolean overtime) {
// ...
double overtimeRate = overtime ? 1.5 : 1.0 * tenthRate;
// ...
}
شما باید به خاطر داشته باشید که calculateWeeklyPay(false)
دقیقاً چه کاری انجام میدهد، که اصلاً خوشایند نیست.
روش بهتر:
public int straightPay() {
return getTenthsWorked() * getTenthRate();
}
public int overTimePay() {
int overTimeTenths = Math.max(0, getTenthsWorked() - 400);
int overTimePay = overTimeBonus(overTimeTenths);
return straightPay() + overTimePay;
}
private int overTimeBonus(int overTimeTenths) {
double bonus = 0.5 * getTenthRate() * overTimeTenths;
return (int) Math.round(bonus);
}
توجه: آرگومانهای انتخابی فقط boolean
نیستند؛ ممکن است enum،
int
یا هر نوع دیگری باشند. اما در اغلب موارد بهتر است رفتارهای مختلف را در توابع مختلف پیادهسازی کنیم.
G16: نیت مبهم (Obscured Intent)
کد باید تا جای ممکن واضح و گویا باشد. استفاده از عباراتی فشرده، نامگذاری ضعیف و اعداد جادویی (magic numbers) باعث ابهام در نیت نویسنده میشود.
مثال مبهم:
public int m_otCalc() {
return iThsWkd * iThsRte +
(int) Math.round(0.5 * iThsRte * Math.max(0, iThsWkd - 400));
}
هرچند فشرده است، ولی فهم آن دشوار است. ارزش دارد که وقت بگذاریم تا نیت کد را برای خواننده روشن کنیم.
G17: مسئولیتهای نابهجا (Misplaced Responsibility)
یکی از مهمترین تصمیمها در توسعهی نرمافزار، تعیین مکان مناسب برای قرار دادن کد است.
مثلاً ثابت PI
باید کجا قرار بگیرد؟ در کلاس Math
؟ یا Trigonometry
؟ یا شاید Circle
؟
اصل «کمترین شگفتی» اینجا هم مطرح است. کد باید جایی باشد که خواننده انتظار دارد آن را پیدا کند.
مثال: اگر ماژول گزارشگیری تابعی به نام getTotalHours
دارد، و ماژول دیگری تابعی به نام saveTimeCard،
کدام یک مسئول جمع ساعات است؟ جواب باید واضح باشد.
در مواقعی ممکن است دلایل عملکردی باعث شوند تا کاری در مکانی خاص انجام شود، ولی نام تابع باید این موضوع را منعکس کند (مثلاً computeRunningTotalOfHours
).
G18: استفادهی نابهجا از static
تابع Math.max(double a, double b)
یک مثال خوب برای متد static
است؛ زیرا روی نمونهی خاصی عمل نمیکند و همهی اطلاعاتش را از آرگومانها میگیرد. بعید است بخواهیم آن را بهشکل چندریخت (polymorphic
) پیادهسازی کنیم.
اما گاهی متدهایی را static
مینویسیم که نباید اینطور باشند. برای مثال:
HourlyPayCalculator.calculatePay(employee, overtimeRate);
در نگاه اول منطقی بهنظر میرسد، اما شاید بخواهیم چند الگوریتم مختلف برای محاسبهی دستمزد پیاده کنیم (مثلاً OvertimeHourlyPayCalculator،
StraightTimeHourlyPayCalculator
)، و در این صورت نیاز به چندریختی داریم.
بهطور کلی، بهتر است ترجیح بدهیم متدها non-static
باشند. تنها وقتی مطمئن شدیم که متدی هرگز نیاز به رفتار چندریختی ندارد، آن را static
تعریف کنیم.
G19: استفاده از متغیرهای توضیحی (Explanatory Variables)
یکی از مؤثرترین روشها برای خوانایی کد، شکستن محاسبات به متغیرهای میانی با نامهای گویاست.
مثال:
Matcher match = headerPattern.matcher(line);
if(match.find()) {
String key = match.group(1);
String value = match.group(2);
headers.put(key.toLowerCase(), value);
}
با همین چند متغیر، فوراً مشخص میشود که group(1)
کلید است و group(2)
مقدار.
بیشتر کردن متغیرهای توضیحی معمولاً از کمتر بودن آنها بهتر است.
G20: نام توابع باید گویای عملکردشان باشد
به این کد نگاه کنید:
Date newDate = date.add(5);
آیا این پنج روز به تاریخ اضافه میکند؟ یا پنج هفته؟ آیا خود date
تغییر میکند یا newDate
مستقل از آن است؟
از روی نام تابع نمیتوان فهمید.
اگر تابع پنج روز اضافه میکند و تاریخ اصلی را تغییر میدهد، نامش باید addDaysTo
یا increaseByDays
باشد.
اگر شیء جدیدی را بازمیگرداند، بهتر است نامی مانند daysLater
یا daysSince
داشته باشد.
اگر برای فهم عملکرد تابع نیاز به بررسی پیادهسازی آن دارید، باید نام بهتری برای آن انتخاب کنید یا عملکردش را بازآرایی نمایید.
G21: الگوریتم را درک کنید
کدهای خندهدار زیادی نوشته میشوند فقط به این دلیل که برنامهنویس وقت کافی برای درک الگوریتم صرف نکرده است. او کدی مینویسد که «کار میکند» صرفاً با اضافه کردن تعدادی شرط (if
) و فلگهای کنترلی، بدون آنکه واقعاً متوجه شود پشت صحنه چه میگذرد.
برنامهنویسی اغلب نوعی اکتشاف است. ممکن است تصور کنید که الگوریتم درست را میدانید، اما بعد مجبور میشوید آن را اصلاح، امتحان و دستکاری کنید تا بالاخره «کار کند». چطور میفهمید که کار میکند؟ چون تستهایی که به ذهنتان رسیده را پاس میکند.
این رویکرد فینفسه مشکلی ندارد؛ در واقع، گاهی تنها راه برای رسیدن به عملکرد دلخواه همین است. اما کافی نیست که فقط به «کار کردن» اکتفا کنید—یعنی نباید فقط از دید آزمونها به عملکرد درست بسنده کنید.
قبل از آنکه تصور کنید کارتان با تابعی تمام شده، باید آن را واقعاً درک کرده باشید. اینکه همهی تستها پاس شوند کافی نیست. شما باید بدانید که راهحل درست است.
اغلب بهترین راه برای رسیدن به این درک، بازآرایی (Refactor
) کد به شکلی است که بسیار تمیز و گویا باشد—آنقدر که روش کارکرد آن، واضح و بدیهی شود.
G22: وابستگیهای منطقی را به وابستگیهای فیزیکی تبدیل کنید
اگر یک ماژول به ماژول دیگری وابسته است، این وابستگی باید فیزیکی باشد، نه صرفاً منطقی. ماژول وابسته نباید بر اساس فرضیات دربارهی ماژول دیگر کار کند (یعنی وابستگی منطقی نداشته باشد). بلکه باید صراحتاً تمام اطلاعاتی را که نیاز دارد از ماژول مورد نظر درخواست کند.
برای مثال، تصور کنید تابعی مینویسید که گزارشی متنی از ساعات کاری کارکنان چاپ میکند. کلاسی به نام HourlyReporter
تمام دادههای مورد نیاز را جمعآوری میکند و سپس آن را به کلاس HourlyReportFormatter
میدهد تا چاپ شود.
Listing 17-1 را ببینید
Listing 17-1 -- HourlyReporter.java
public class HourlyReporter
{
private HourlyReportFormatter formatter;
private List<LineItem> page;
private final int PAGE_SIZE = 55;
public HourlyReporter(HourlyReportFormatter formatter)
{
this.formatter = formatter;
page = new ArrayList<LineItem>();
}
public void generateReport(List<HourlyEmployee> employees)
{
for (HourlyEmployee e : employees) {
addLineItemToPage(e);
if (page.size() == PAGE_SIZE)
printAndClearItemList();
}
if (page.size() > 0)
printAndClearItemList();
}
private void printAndClearItemList()
{
formatter.format(page);
page.clear();
}
private void addLineItemToPage(HourlyEmployee e)
{
LineItem item = new LineItem();
item.name = e.getName();
item.hours = e.getTenthsWorked() / 10;
item.tenths = e.getTenthsWorked() % 10;
page.add(item);
}
public class LineItem
{
public String name;
public int hours;
public int tenths;
}
}
این کد دارای یک وابستگی منطقی است که بهصورت فیزیکی پیادهسازی نشده است. میتوانید آن را پیدا کنید؟ آن وابستگی، ثابت PAGE_SIZE
است. چرا باید HourlyReporter
از اندازهی صفحه مطلع باشد؟ اندازهی صفحه باید در حوزهی مسئولیت کلاس HourlyReportFormatter
باشد.
واقعیت این است که PAGE_SIZE
در کلاس HourlyReporter
تعریف شده و این نشاندهندهی یک مسئولیت نابهجا [G17] است که باعث میشود HourlyReporter
فرض کند میداند اندازهی صفحه باید چه باشد. چنین فرضی، یک وابستگی منطقی به حساب میآید. HourlyReporter
به این وابسته است که HourlyReportFormatter
قادر به پردازش صفحاتی با اندازهی ۵۵ باشد. اگر پیادهسازیای از HourlyReportFormatter
وجود داشته باشد که نتواند با چنین اندازهای کار کند، آنگاه یک خطا به وجود میآید.
ما میتوانیم این وابستگی را با ایجاد متدی جدید به نام getMaxPageSize()
در کلاس HourlyReportFormatter
فیزیکی کنیم. سپس HourlyReporter
به جای استفاده از ثابت PAGE_SIZE،
آن متد را فراخوانی خواهد کرد.
G23: ترجیح پلیمورفیسم به جای if/else
یا Switch/Case
شاید این پیشنهاد با توجه به موضوع فصل ششم عجیب بهنظر برسد. بههرحال در آن فصل تأکید کردم که دستور switch
احتمالاً برای بخشهایی از سیستم مناسب است که افزودن عملکردهای جدید در آنها محتملتر از افزودن نوعهای جدید است.
اول، اکثر افراد از switch
استفاده میکنند چون راهحل واضح و سرراست (brute-force
) بهنظر میرسد، نه لزوماً راهحل درست برای مسئله. بنابراین این قاعده اینجاست تا به ما یادآوری کند پیش از استفاده از switch،
به پلیمورفیسم فکر کنیم.
دوم، مواردی که در آنها توابع متغیرتر از نوعها هستند، نسبتاً نادرند. بنابراین، هر دستور switch
باید مشکوک باشد.
من از قاعدهی “تنها یک switch
” استفاده میکنم: نباید بیشتر از یک دستور switch
برای نوع خاصی از انتخاب وجود داشته باشد. case
های آن دستور باید آبجکتهای پلیمورفیکی بسازند که جایگزین سایر switch
ها در باقی سیستم میشوند.
G24: از قراردادهای استاندارد پیروی کنید
هر تیم باید از یک استاندارد کدنویسی تبعیت کند که بر مبنای عرفهای رایج صنعت تعریف شده است. این استاندارد باید مواردی مانند مکان تعریف متغیرهای نمونه، نحوهی نامگذاری کلاسها، متدها و متغیرها، محل قرارگیری براکتها و غیره را مشخص کند. تیم نباید برای توضیح این قراردادها به سندی نیاز داشته باشد، چراکه خود کد باید الگو و نمونهی آنها باشد.
همهی اعضای تیم باید از این قراردادها پیروی کنند. این یعنی هر عضو باید به اندازهی کافی بالغ باشد که درک کند محل قرار دادن براکتها اهمیتی ندارد، مادامی که همه بر سر یک روش توافق داشته باشند.
اگر میخواهید بدانید من از چه قراردادهایی پیروی میکنم، آنها را در کدهای بازآراییشده در لیستهای B-7
تا B-14
خواهید دید.
G25: جایگزینی اعداد جادویی با ثابتهای نامگذاریشده
این احتمالاً یکی از قدیمیترین قواعد در توسعهی نرمافزار است. من آن را در اواخر دههی شصت در کتابهای مقدماتی COBOL،
FORTRAN
و PL/1
خواندم. بهطور کلی، استفاده از اعداد خام در کد ایدهی بدی است. شما باید آنها را پشت ثابتهایی با نام مناسب پنهان کنید.
برای مثال، عدد 86400
باید پشت ثابتی مثل SECONDS_PER_DAY
قرار گیرد. اگر ۵۵
خط در هر صفحه چاپ میشود، آن عدد باید پشت ثابتی مثل LINES_PER_PAGE
قرار گیرد.
برخی از ثابتها آنقدر شناختهشده هستند که نیازی نیست حتماً با ثابتهای نامگذاریشده جایگزین شوند، بهویژه وقتی در کنار کدی کاملاً واضح استفاده میشوند. برای مثال:
double milesWalked = feetWalked / 5280.0;
int dailyPay = hourlyRate * 8;
double circumference = radius * Math.PI * 2;
آیا واقعاً نیاز است FEET_PER_MILE،
WORK_HOURS_PER_DAY
و TWO
بهجای آنها استفاده شود؟ بهوضوح، مورد سوم (عدد ۲) کاملاً بیمعنا است. برخی فرمولها بهتر است اعداد را بهصورت خام نگه دارند.
ممکن است دربارهی WORK_HOURS_PER_DAY
بحث شود، چراکه قوانین یا عرفها ممکن است تغییر کنند. با این حال، آن فرمول با عدد ۸ خیلی روان خوانده میشود و افزودن ۱۷ کاراکتر اضافی برای یک نام ثابت ممکن است سربار بصری ایجاد کند.
عدد 5280 آنقدر معروف است که حتی اگر وسط یک صفحه بدون هیچ متنی قرار گیرد، باز هم برای خواننده آشناست.
با این حال، اعدادی مانند 3.141592653589793
آنقدر دقیق هستند که استفادهی خام آنها خطرناک است. وقتی کسی عدد 3.1415927535890793
را میبیند، تصور میکند عدد پی است و بنابراین آن را به دقت بررسی نمیکند. (آیا متوجه خطای تکرقمی شدید؟)
ما همچنین نمیخواهیم افراد نسخههای مختلفی مثل 3.14
، 3.14159
، 3.142
و غیره استفاده کنند. بنابراین، این موضوع که Math.PI
قبلاً برای ما تعریف شده، بسیار خوب است.
اصطلاح «عدد جادویی» فقط مختص اعداد نیست. هر نشانهای که معنای روشنی نداشته باشد در این دسته قرار میگیرد. برای مثال:
assertEquals(7777, Employee.find("John Doe").employeeNumber());
دو عدد جادویی در این تست وجود دارد: عدد ۷۷۷۷ و رشتهی "John Doe". معنای هیچکدام از آنها مشخص نیست.
در واقع، "John Doe" نام کارمند شماره ۷۷۷۷ در پایگاه دادهی تستی شناختهشدهی تیم ماست. همهی اعضای تیم میدانند که این پایگاه داده شامل چند کارمند آماده با مقادیر شناختهشده است. "John Doe" تنها کارمند ساعتی (hourly) در این پایگاه داده است. بنابراین این تست باید به شکل زیر نوشته شود:
assertEquals(
HOURLY_EMPLOYEE_ID,
Employee.find(HOURLY_EMPLOYEE_NAME).employeeNumber());
G26: دقیق باشید
انتظار اینکه اولین نتیجهی یک کوئری تنها نتیجه باشد، سادهلوحانه است. استفاده از اعداد اعشاری برای نمایش پول تقریباً جنایتکارانه است. پرهیز از قفلگذاری یا مدیریت تراکنش بهخاطر فرض کمبودن احتمال بهروزرسانی همزمان، در بهترین حالت تنبلی است.
تعریف یک متغیر به صورت ArrayList
در حالی که List
کافی بود، بیشازحد محدودکننده است. از طرفی، تعریف همهی متغیرها بهصورت protected
پیشفرض، کافی نیست.
وقتی تصمیمی در کد میگیرید، آن را دقیق بگیرید. بدانید چرا آن تصمیم را گرفتهاید و در صورت بروز استثنا، چگونه با آن برخورد خواهید کرد. در برخورد با دقت تصمیمگیری تنبلی نکنید.
اگر متدی ممکن است null
بازگرداند، برای آن بررسی انجام دهید. اگر فکر میکنید تنها یک رکورد از دیتابیس خوانده میشود، بررسی کنید که رکوردهای دیگری وجود نداشته باشد. اگر با ارز سر و کار دارید، از اعداد صحیح استفاده کنید و گرد کردن را بهدرستی مدیریت نمایید. اگر امکان بهروزرسانی همزمان وجود دارد، حتماً مکانیزم قفلگذاری پیادهسازی کنید.
ابهام و عدم دقت در کد یا نتیجهی اختلاف نظر است یا تنبلی — در هر دو حالت باید حذف شوند.
G34: توابع باید فقط یک سطح از انتزاع را پایین بیایند
دستورات داخل یک تابع باید همگی در یک سطح انتزاعی نوشته شوند؛ آن هم در سطحی پایینتر از عملیاتی که نام تابع توصیف میکند.
درک و اجرای این اصل احتمالاً یکی از سختترین موارد در میان این اصول است. با اینکه ایدهی کلی آن واضح است، اما انسانها به طرز شگفتانگیزی در مخلوط کردن سطوح مختلف انتزاع مهارت دارند.
مثلاً کد زیر از پروژهٔ FitNesse
را در نظر بگیرید:
public String render() throws Exception {
StringBuffer html = new StringBuffer("<hr");
if(size > 0)
html.append(" size=\"").append(size + 1).append("\"");
html.append(">");
return html.toString();
}
با یک نگاه کوتاه میتوان فهمید این تابع تگ HTML
مربوط به خط افقی (<hr>
) را میسازد. ارتفاع این خط توسط متغیر size
تعیین میشود.
اما اگر دوباره نگاه کنید، میبینید که این متد حداقل دو سطح از انتزاع را با هم ترکیب کرده:
مفهوم اینکه یک خط افقی (horizontal rule) اندازه دارد.
نحو (syntax) مربوط به تگ <hr>
در HTML
.
این کد متعلق به ماژول HruleWidget
در FitNesse
است. این ماژول یک ردیف شامل چهار یا بیشتر خط تیره (-) را تشخیص داده و آن را به تگ HR
مناسب تبدیل میکند. هرچه تعداد خط تیرهها بیشتر باشد، اندازه تگ HR
بزرگتر خواهد بود.
کد بالا را اینگونه بازنویسی کردهام. دقت کنید که نام فیلد size
را هم تغییر دادهام تا هدف واقعیاش را بهتر نشان دهد. این فیلد در واقع تعداد خط تیرههای اضافه را نگه میداشت:
public String render() throws Exception {
HtmlTag hr = new HtmlTag("hr");
if (extraDashes > 0)
hr.addAttribute("size", hrSize(extraDashes));
return hr.html();
}
private String hrSize(int height) {
int hrSize = height + 1;
return String.format("%d", hrSize);
}
این تغییر، سطوح مختلف انتزاع را بهخوبی جدا کرده است. تابع render
تنها وظیفهی ساخت تگ HR
را دارد و هیچ اطلاعی از نحو HTML
آن ندارد. ماژول HtmlTag
تمام جزئیات نحوی را مدیریت میکند.
در واقع، با این تغییر یک باگ ظریف هم کشف شد. کد اصلی تگ HR
را بدون / بسته بود (یعنی <hr>
بهجای <hr/>
طبق استاندارد XHTML
). ماژول HtmlTag
مدتها پیش به XHTML
تغییر یافته بود.
جدا کردن سطوح انتزاع یکی از مهمترین و سختترین بخشهای Refactoring
است. مثالی دیگر:
public String render() throws Exception {
HtmlTag hr = new HtmlTag("hr");
if (size > 0) {
hr.addAttribute("size", "" + (size + 1));
}
return hr.html();
}
هدف اولیه من جدا کردن سطوح انتزاع و عبور از تستها بود، که بهراحتی انجام شد. اما همچنان این تابع شامل سطوح ترکیبی انتزاعی بود: ساخت تگ HR
و تفسیر و قالببندی مقدار size
.
وقتی تابعی را بر اساس سطوح انتزاع تقسیم میکنید، اغلب خطوط جدیدی از انتزاع آشکار میشوند که قبلاً پنهان بودند.
G35: دادههای پیکربندی را در سطوح بالا نگه دارید
اگر دادهای مانند مقدار پیشفرض یا مقدار پیکربندی دارید که در سطوح بالا مورد انتظار و شناختهشده است، آن را در توابع سطح پایین پنهان نکنید. آن را بهعنوان پارامتر از توابع سطح بالا عبور دهید.
مثال زیر را از FitNesse
ببینید:
public static void main(String[] args) throws Exception {
Arguments arguments = parseCommandLine(args);
...
}
public class Arguments {
public static final String DEFAULT_PATH = ".";
public static final String DEFAULT_ROOT = "FitNesseRoot";
public static final int DEFAULT_PORT = 80;
public static final int DEFAULT_VERSION_DAYS = 14;
...
}
آرگومانهای خط فرمان در اولین خط اجرایی FitNesse
تحلیل میشوند. مقادیر پیشفرض آنها در ابتدای کلاس Arguments
قرار دارند. نیازی نیست در اعماق سیستم به دنبال چنین دستوراتی بگردید:
if (arguments.port == 0) // default => 80
ثوابت پیکربندی در سطوح بالا نگهداری میشوند و بهراحتی قابل تغییرند. این مقادیر به سطوح پایینتر ارسال میشوند، اما آن سطوح مالک آنها نیستند.
G36: از ناوبری گذرا (Transitive Navigation) اجتناب کنید
بهطور کلی نمیخواهیم یک ماژول چیز زیادی دربارهٔ همکاران خود بداند. بهخصوص اگر A با B کار میکند و B با C، نمیخواهیم ماژولهای استفادهکننده از A، دربارهٔ C چیزی بدانند. مثلاً این بد است:
a.getB().getC().doSomething();
این قانون به Law of Demeter
یا اصطلاحاً کد خجالتی (Shy Code
) معروف است. ایده این است که هر ماژول تنها دربارهٔ همکاران مستقیم خود اطلاع داشته باشد و از نقشهٔ ناوبری کل سیستم بیخبر باشد.
اگر در نقاط مختلف سیستم چنین عبارتی داشته باشید:
a.getB().getC()
تغییر معماری سیستم برای افزودن یک جزء جدید (مثلاً Q بین B و C) بسیار سخت خواهد شد، چون باید تمام موارد استفاده را پیدا و بازنویسی کنید.
در عوض، باید همکاران مستقیم تمام خدمات موردنیاز را ارائه دهند تا نیازی به گردش در گراف شیء نباشد. مثلاً فقط بنویسیم:
myCollaborator.doSomething();
J1: با استفاده از Wildcard
لیست ایمپورتها را کوتاه نگه دارید
اگر از دو یا چند کلاس از یک پکیج استفاده میکنید، کل پکیج را با import package.*
ایمپورت کنید.
لیست بلندبالای ایمپورتها باعث آزار خواننده میشود. هدف ما یک بیان مختصر از وابستگیهای ماژول است.
ایمپورت مشخص (مثل import a.b.ClassX
) یک وابستگی سخت است، در حالی که ایمپورت کلی (wildcard
) فقط پکیج را به مسیر جستجو اضافه میکند و وابستگی مستقیم ایجاد نمیکند. پس ماژولها را کمتر کوپل میکند.
در برخی موارد (مثلاً کدهای قدیمی که میخواهیم برای آنها mock
یا stub
بسازیم) ایمپورتهای مشخص مفیدند، اما این موارد نادرند. بیشتر IDE ها هم میتوانند با یک دستور، wildcard
را به ایمپورتهای مشخص تبدیل کنند.
مشکل نادر wildcard
این است که اگر دو کلاس با نام یکسان ولی در پکیجهای متفاوت داشته باشید، باید آنها را مشخصاً ایمپورت کنید.
J2: از ارثبری برای دستیابی به ثابتها استفاده نکنید
گاهی برخی برنامهنویسان ثابتها را در یک interface
قرار میدهند و از طریق ارثبری به آنها دسترسی پیدا میکنند:
public class HourlyEmployee extends Employee {
private int tenthsWorked;
private double hourlyRate;
public Money calculatePay() {
int straightTime = Math.min(tenthsWorked, TENTHS_PER_WEEK);
int overTime = tenthsWorked - straightTime;
return new Money(
hourlyRate * (tenthsWorked + OVERTIME_RATE * overTime)
);
}
}
اما این ثابتها از کجا آمدهاند؟ ممکن است از کلاس Employee
نیامده باشند، بلکه از یک interface
مانند زیر:
public interface PayrollConstants {
public static final int TENTHS_PER_WEEK = 400;
public static final double OVERTIME_RATE = 1.5;
}
این کار زشت و گمراهکننده است! ثابتها در بالای سلسلهمراتب ارثبری پنهان شدهاند. از ارثبری برای دور زدن قوانین Scope
زبان استفاده نکنید.
بهجای آن از static import
استفاده کنید:
import static PayrollConstants.*;
public class HourlyEmployee extends Employee {
...
}
J3: استفاده از Constant بهجای Enum نکنید
از زمانی که enum
ها به زبان Java
افزوده شدهاند (Java 5)، از آنها استفاده کنید! نه از public static final int
.
مقدارهای عددی (int
) معنا را گم میکنند، اما enum
ها این مشکل را ندارند چون در یک دستهبندی معنادار قرار میگیرند.
علاوه بر این، enum
ها میتوانند متد و فیلد داشته باشند و ابزارهایی قدرتمند برای بیان و انعطافپذیری هستند. مثلاً:
public class HourlyEmployee extends Employee {
private int tenthsWorked;
HourlyPayGrade grade;
public Money calculatePay() {
int straightTime = Math.min(tenthsWorked, TENTHS_PER_WEEK);
int overTime = tenthsWorked - straightTime;
return new Money(
grade.rate() * (tenthsWorked + OVERTIME_RATE * overTime)
);
}
}
public enum HourlyPayGrade {
APPRENTICE {
public double rate() { return 1.0; }
},
LEUTENANT_JOURNEYMAN {
public double rate() { return 1.2; }
},
JOURNEYMAN {
public double rate() { return 1.5; }
},
MASTER {
public double rate() { return 2.0; }
};
public abstract double rate();
}
نامگذاریها
N1: انتخاب نامهای توصیفی
برای انتخاب نام عجله نکنید. مطمئن شوید که نام انتخابشده توصیفی باشد. به یاد داشته باشید که با گذر زمان و تکامل نرمافزار، معنای نامها نیز ممکن است تغییر کند؛ پس باید بهطور منظم مناسب بودن آنها را ارزیابی کنید.
این فقط یک توصیهی «حسی» نیست. نامها در نرمافزار، ۹۰ درصد قابلخواندنبودن کد را تعیین میکنند. باید برای انتخاب آنها وقت بگذارید و دائماً آنها را بهروز نگه دارید. نامها بسیار مهمتر از آناند که با بیدقتی با آنها رفتار شود.
کدی مانند زیر را در نظر بگیرید. چه کاری انجام میدهد؟ اگر همان کد را با نامگذاری مناسب ببینید، کاملاً برایتان روشن خواهد بود. اما بهصورت فعلی، چیزی جز مجموعهای از نمادها و اعداد جادویی نیست:
public int x() {
int q = 0;
int z = 0;
for (int kk = 0; kk < 10; kk++) {
if (l[z] == 10) {
q += 10 + (l[z + 1] + l[z + 2]);
z += 1;
} else if (l[z] + l[z + 1] == 10) {
q += 10 + l[z + 2];
z += 2;
} else {
q += l[z] + l[z + 1];
z += 2;
}
}
return q;
}
اکنون همان منطق را با نامگذاری صحیح ببینید. این نسخه حتی کاملتر هم نیست، اما بلافاصله میتوانید بفهمید که چه کاری انجام میدهد و احتمالاً میتوانید توابع ناقص را خودتان بنویسید:
public int score() {
int score = 0;
int frame = 0;
for (int frameNumber = 0; frameNumber < 10; frameNumber++) {
if (isStrike(frame)) {
score += 10 + nextTwoBallsForStrike(frame);
frame += 1;
} else if (isSpare(frame)) {
score += 10 + nextBallForSpare(frame);
frame += 2;
} else {
score += twoBallsInFrame(frame);
frame += 2;
}
}
return score;
}
قدرت نامگذاری دقیق در این است که ساختار کد را با توصیف غنی میکند و انتظارات خواننده را از توابع دیگر شکل میدهد. برای مثال، وقتی تابع isStrike
را ببینید، دقیقاً همان چیزی خواهد بود که انتظارش را داشتید:
private boolean isStrike(int frame) {
return rolls[frame] == 10;
}
N2: انتخاب نام در سطح انتزاع مناسب
نامهایی انتخاب نکنید که جزئیات پیادهسازی را منتقل کنند؛ نام باید با سطح انتزاع کلاس یا تابع مطابقت داشته باشد. این کار آسان نیست. مردم معمولاً سطوح انتزاع مختلف را با هم قاطی میکنند. هر بار که کد را مرور میکنید، احتمالاً نامهایی میبینید که در سطح اشتباهی قرار دارند؛ آنها را تغییر دهید.
برای مثال، این اینترفیس را ببینید:
public interface Modem {
boolean dial(String phoneNumber);
boolean disconnect();
boolean send(char c);
char recv();
String getConnectedPhoneNumber();
}
برای مودمهای سنتی مناسب است، اما حالتی را در نظر بگیرید که مودمها با سیمکشی مستقیم یا از طریق USB
متصل میشوند. در این صورت «شمارهتلفن» سطح انتزاع مناسبی نیست. نسخهی بهتر:
public interface Modem {
boolean connect(String connectionLocator);
boolean disconnect();
boolean send(char c);
char recv();
String getConnectedLocator();
}
N3: استفاده از نامگذاری استاندارد در صورت امکان
درک نامهایی که مطابق با قراردادهای موجود هستند راحتتر است. مثلاً در الگوی Decorator
بهتر است نام کلاسها شامل کلمهی Decorator
باشد. مانند:
AutoHangupModemDecorator
همچنین در جاوا متدی که یک شیء را به رشته تبدیل میکند، معمولاً toString
نام دارد. بهتر است از چنین قراردادهایی پیروی کنید تا نام خودساخته.
در پروژهها ممکن است تیمها زبان خاصی ایجاد کنند که اریک ایوانز از آن با عنوان زبان فراگیر (Ubiquitous Language
) یاد میکند. کد شما باید از همین زبان استفاده کند.
N4: نامهای بدون ابهام
نامی انتخاب کنید که عملکرد متد یا متغیر را به روشنی بیان کند. مثلاً در مثال زیر:
private String doRename() throws Exception {
if(refactorReferences)
renameReferences();
renamePage();
pathToRename.removeNameFromEnd();
pathToRename.addNameToEnd(newName);
return PathParser.render(pathToRename);
}
نام doRename
مشخص نمیکند این متد دقیقاً چه کاری انجام میدهد. بهتر است آن را به:
renamePageAndOptionallyAllReferences
تغییر دهید. اگرچه طولانی است، اما اگر فقط در یک مکان فراخوانی شود، ارزش توصیف بالا دارد.
N5: استفاده از نامهای بلند برای حوزههای بلند
طول نام باید متناسب با طول حوزهی استفاده باشد. نامهای کوتاه مانند i
یا j
برای محدودههای کوچک (مثلاً پنج خط) قابلقبولاند:
private void rollMany(int n, int pins) {
for (int i=0; i<n; i++)
g.roll(pins);
}
اما در حوزههای بزرگ، نامهای کوتاه معنی خود را از دست میدهند. پس هرچه دامنهی کاربرد نام طولانیتر باشد، نام نیز باید دقیقتر و طولانیتر باشد.
N6: پرهیز از رمزگذاری در نامها
نباید نوع یا حوزهی متغیر را در نام کدگذاری کنید. پیشوندهایی مانند m_
یا f
یا استفاده از نام زیرسیستمها مانند vis_
دیگر در محیطهای مدرن بیمعنی هستند. IDEها این اطلاعات را بهطور خودکار نمایش میدهند. پس نامها را تمیز نگه دارید و از "آلودگی مجارستانی" پرهیز کنید.
N7: نام باید نشاندهندهی اثرات جانبی باشد
نامها باید نشان دهند که یک تابع یا کلاس چه کاری انجام میدهد یا چه اثری دارد. اگر تابعی کاری بیش از آنچه در نامش آمده انجام میدهد، نام باید همهی اثرات را پوشش دهد:
public ObjectOutputStream getOos() throws IOException {
if (m_oos == null) {
m_oos = new ObjectOutputStream(m_socket.getOutputStream());
}
return m_oos;
}
این تابع فقط get
نمیکند؛ بلکه ایجاد هم میکند. پس بهتر است نام آن باشد:
createOrReturnOos
تستها
T1: تستهای ناکافی
چند تست کافی است؟ بسیاری از برنامهنویسان میگویند: «بهنظر کافی میرسد.» اما واقعاً باید همهی چیزی که ممکن است خراب شود، تست شود.
T2: از ابزار پوشش تست استفاده کنید
ابزارهای پوشش (Coverage Tools) نشان میدهند کدام بخشها تست نشدهاند. اکثر IDEها کدهای تستشده را سبز و بخشهای بدون تست را قرمز نمایش میدهند. این کمک میکند سریع شرطهایی که تست نشدهاند را پیدا کنید.
T3: تستهای ساده را نادیده نگیرید
نوشتن آنها آسان است و ارزش مستندسازیشان بیشتر از هزینهی نوشتنشان است.
T4: تست نادیده گرفتهشده یعنی ابهام در نیازمندیها
گاهی دقیقاً نمیدانید باید چه رفتاری انتظار داشت. در این مواقع میتوانید تست را موقتاً کامنت کنید یا از @Ignore
استفاده کنید تا سؤال را به شکل تست مطرح کنید.
T5: شرایط مرزی را تست کنید
مرزها معمولاً منبع اشتباه هستند، حتی وقتی وسط الگوریتم درست عمل میکند.
T6: نزدیکی به باگ را کامل تست کنید
باگها تمایل به تجمع دارند. وقتی یک باگ پیدا کردید، احتمالاً باگهای بیشتری در همان ناحیه هستند.
T7: الگوهای شکست در تستها گویا هستند
الگوهای خطا در تستها میتوانند شما را به کشف منبع مشکل برسانند. مثلاً اگر تمام تستهایی که ورودیشان بیشتر از ۵ حرف است، شکست میخورند؟ یا تستهایی با عدد منفی در آرگومان دوم؟ الگوی شکستها میتواند باعث جرقهی کشف مشکل شود.
T8: الگوی پوشش تست میتواند راهگشا باشد
بررسی اینکه چه کدی توسط تستهای موفق اجرا شده و چه کدی نه، میتواند علت شکست تستهای دیگر را نشان دهد.
T9: تستها باید سریع باشند
تست کند، تستی است که اجرا نمیشود. وقتی زمان کم است، تستهای کند کنار گذاشته میشوند. پس تستها را سریع نگه دارید.
نتیجهگیری
این فهرست از نشانهها و قواعد کامل نیست و شاید هیچگاه کامل نشود. اما هدف آن هم کمال نبوده است، بلکه هدف، بیان یک سیستم ارزشی بوده است.
در واقع، تمام کتاب دربارهی همین سیستم ارزشی بوده است. کد تمیز با پیروی کورکورانه از قواعد نوشته نمیشود. برنامهنویس حرفهای با حفظ ارزشها و پیروی از انضباط و هنر رشد میکند.