در این مجموعه مقالات وبسایت راکت، قصد دارم کتاب کد تمیز رو به صورت خلاصه برای شما بازگو کنم، تا دوستانی که وقت کافی برای خواندن کتاب به صورت کامل رو ندارند بتونن استفاده کنند. به عبارتی موارد و مفاهیم اصلی کتاب بدون هیچ مطلب اضافهای در این مجموعه نقل میشه( البته که کلمه به کلمهی این کتاب مفیده و خوندنش رو به همهی شمایی که نخوندین پیشنهاد میکنم).
خب بدون تلف کردن وقت میریم سراغ کتاب! در ابتدا خوبه که خلاصهای از این کتاب به شما بگم و یه معرفی اجمالی ازش داشته باشیم.
حتی یه کد بد هم میتونه کار کنه، اما اگر کد تمیز نباشه میتونه یک سازمان رو به زانو دربیاره. Robert C. Martin یکی از متخصصان برجسته نرمافزار یک الگوی انقلابی را با کتاب کد تمیز یا Clean Code: A Handbook of Agile Software Craftsmanship ارائه میدهد. مارتین سعی کرده در این کتاب روشها و تمرینهایی به شما بده تا مهارتهای شما به عنوان یک برنامهنویس در نوشتن یک کد خوب افزایش پیدا کنه( اما باید توجه کنید که این اتفاق در صورتی میافتد که شما سخت تمرین کنید.)
خب شاید سؤال پیش بیاد که در طول خواندن این کتاب چه کارهایی رو انجام میدهید؟ شما کدهای زیادی را میخوانید و سپس در مورد درستی کد و همینطور اون چیزی که در رابطه با یک قطعه کد اشتباهه، به چالش کشیده خواهید شد.
Clean code به سه بخش تقسیم میشه :بخش اول که شامل چندین فصل اول هست، اصول،الگوها و روشهای نوشتن کد تمیز را توصیف میکنه و لازمه بگم که شما رو برای پارت دوم کتاب هم آماده میکنه. در قسمت دوم، سطح کتاب بالاتر میره و تمرینهایی برای تمیز کردن یک قطعه کد به شما داده میشه- تبدیل یک قطعه کد که دارای مشکل هست به یک کد صحیح و سالم- و بخش سوم، به نتیجهی نهایی میپردازه؛ در بخش نهایی تمامی دلایل ما برای تبدیل و تغییر در یک کد را ، به صورت لیست بیان میکنه، به عبارتی نحوه تفکر ما هنگام نوشت، خواندن و تمیز کردن کد رو توصیف میکنه.
کدام یک از این تصاویر وضعیت شما را در کدنویسی نشان میدهد؟ کدام یک تیم یا شرکت شما را نشان میدهد؟ چه چیزی باعث میشود که ما هر یک از این درها باشیم؟ آیا این فقط یک بررسی ساده از کد است یا ممکن است مدتی بعد از اجرا با مشکل روبهرو شویم؟ آیا به خاطر وحشت از خراب شدن کدی که فکر میکردیم درسته اون رو دیباگ میکنیم؟ آیا مشتریها برنامههای ما رو کنار میگذارند و مدیران ما رو تنبیه میکنند؟ چطور مطمئن شویم در لحظات سخت و حساس پشت در درست قرار گرفتهایم؟ جواب یک کلمه است : مهارت.
یادگیری مهارت از دو بخش تشکیل شده :دانش و تمرین؛ شما باید الگوها، اصول و تمامی مواردی که باید را بدانید و آن را با تمام وجود حس کنید و سپس سخت کار و تمرین کنید.
من میتوانم برای شما نکات و اصول کد نویسی صحیح را لیست کنم و بعد به شما اطمینان دهم که حتماً با انجام این لیست موفق میشوید. اما نه، این راهی نیست که این کتاب طی خواهد کرد؛ یادگیری و نوشتن یک کد تمیز مستلزم تمرین و کار سخت است. باید تمرین کنید و از شکستهایتان درس بگیرید، یا حتی ببینید که دیگران چگونه زمین میخورند و دوباره بلند میشوند، چطور از شکستهایی که میخورند عذاب میکشند و چه بهایی برای اشتباهاتشان پرداختند.
فصل اول : کد تمیز
به دو دلیل شما در حال خواندن این کتاب هستید: یک: یک برنامهنویس هستید. دو: میخواهید برنامهنویس بهتری شوید. خوبه، ما به برنامهنویسهای بهتر نیاز داریم.
شاید بعضیها معتقد باشند که دوران کد نویسی توسط برنامهنویسان به زودی تمام میشود و کامپیوترها وظیفه تولید نرمافزارها رو به عهده میگیرند. اما این حرف کاملاً پوچ و خارج از منطق است و ما هرگز از کدها فارغ نخواهیم شد.
اگر بیشتر از دو یا سه سال است که برنامهنویس هستید، احتمالا با کد کثیف شخص دیگری برخورد کردهاید و به خاطر آن معطل ماندهاید. میزان معطل ماندن میتواند مورد قابل توجهی باشد؛ طی یک بازه زمانی یک یا دوساله، تیمهایی که در ابتدای یک پروژه بسیار سریع در حال حرکت بودند، خود را درحال حرکت با سرعت حلزونی میبینند.
( وسط بحث شیرین کتاب لازم دیدم اینجا یه نکتهای رو بگم: چند وقت پیش یکی از دوستانم سؤالی رو مطرح کرد؛ سؤالش این بود: برنامه نویسها همیشه با زمانبندی مشکل دارن و همیشه تایم محدودی برای توسعه دارن اما بعضی وقتها کارفرما از اونها زمانبندیی رو درخواست میکنه که امکان نداره بهش برسن شما توی اینجور موارد چه جوابی میدین و یا چه کاری میکنید؟
خب بیایین بررسی کنیم که جواب برنامه نویسها چی میتونه باشه:
برنامه نویسها معمولا دو جواب میدن:
۱. سعی میکنن که برسونن: اگر این جواب رو بدن یعنی الان به اندازه کافی تلاش نمیکنن که میتونن با کار بیشتر یه مورد رو برسونن.
۲. نه: که جواب درست هست. یه نه قاطعانه میتونه اطلاعات کافی به تیم لیدر و یا افراد بالادست و حتی مشتری بده که زمانبندی، امکان محقق شدن نداره.
خب حالا اون دسته که میگن سعی میکنم از چی میزنن:
۱. معمولا اولین چیزی رو که فدا میکنن کیفیت کار هست و اولین جایی که کیفیت فدا میشه تستها هستن. (من با اون گروه که تست نمینویسن اصلا کاری ندارم و در موردشون صحبت نمیکنم).
۲. کد تمیز نمیزنن (خیلی شلخته کد میزنن که فقط خروجی بگیرن).
هر دوی این کارها سمی هستن و ممکن هست در ابتدا سرعت کار شما رو بیشتر کنه اما به مرور زمان نه تنها سرعت کار را کاهش میده بلکه پروژه رو به سمت نابودی میکشونه. چرا به نابودی و یا چرا سرعت کم می شه؟ خب بعد از یک مدت داخل سیستم که فیچرهای جدید معرفی میکنید و یا باگها رو شروع به اصلاح میکنید و دیگه تستی وجود نداره تا بتونید متوجه بشید که مابقی قسمتها چطور کار میکنن و اضافه کردن و یا تغییر یه مورد میتونه بصورت سریالی مشکلات جدید فراهم کنه و در نتیجه سرعت پیشرفت به مرور کندتر و کندتر میشه تا جایی که عملا صفر میشه.
یه محصول معیوب میتونه کلی خرج باز کنه و هیچ وقت هم نمیشه درست اصلاحش کرد. یه محصول معیوب می تونه باعث شکست یک شرکت بشه.
تمام اینها فقط بخاطر یک نه گفتن اتفاق میافته. هر کاری میتونید بکنید این چند مورد را رعایت کنید :
۱. کد را تا میتونید تمیز و طبق اصول بنویسید.
۲. حتما و حتما برای محصول تست به اندازه کافی بنویسید و حتی شده TDD کد بزنید.
۳. قاطعانه نه بگید.
شماهم میتونید جوابای خودتون رو در رابطه با این سؤال در قسمت نظرات با ما به اشتراک بزارین.)
برمیگردیم به کتاب.
در نهایت اگر که یک کد کثیف تولید شود به مرور زمان پیچ و تابها و مشکلات زیادتر میشوند و باگذشت زمان یک شلختگلی بسیار بزرگ و عمیق در کار به وجود خواهد آمد، به گونهای که قادر به تمیز کردن آنها نیستید. در نهایت مدیر به امید افزایش بهروری کارمندان جدیدی را به پروژه اضافه میکند، اما آنها هیچ شناختی از سیستم ندارند. درواقع آنها تفاوت بین تغییری را که طبق هدف طراحی میشودو تغییری که هدف طراحی را ناکارآمدتر میکند را نمیدانند؛ علاوه بر این آنها تحت فشار زیادی برای افزایش بهروری پروژه هستند و تمام این عوامل باعث میشود که بازدهی هر چه بیشتر به سمت صفر برود.
سرانجام تیم شورش خواهد کرد، چراکه هیچکس نمیتواند بر مبنای این کد نفرت انگیز محصولی را توسعه دهد و همه خواستار طراحی مجدد خواهند شد. در همین حین مدیریت نمیخواهد منابع را صرف طراحی مجدد کند ولی نمیتوانند این را انکار کنند که بهروری وحشتناک است؛ و سرانجام به خواستههای توسعه دهندگان تن میدهند و طراحی مجدد اتفاق میافتد.
خلاصه میکنم، اگر حتی یک بخش کوچک از داستانی که تعریف کردم را تجربه کرده باشید، پس حتماً میدانید که صرف وقت برای تمیز کردن کد، صرفاً مقرون به صرفه نیست، بلکه این یک کار حرفهای برای بقا است.
هنر کد تمیز ؟
بیایید فرض کنیم شما معتقد هستید که کد کثیف مانعی قابل توجه است و میپذیرید که تنها راه برای پیشبرد سریع پروژه، تمیز نگه داشتن کدهایتان است. حالا باید از خود بپرسید: "چگونه میتوانم کد تمیز بنویسم؟". اگر نمیدانید کد تمیز چیست تلاشتان هم برای نوشتن یک کد تمیز جواب نمیدهد؛ خبر بد هم این است که نوشتن کد تمیز شبیه به کشیدن نقاشی است؛ بیشتر ما میدانیم یک تصویر، خوب نقاشی شده یا بد، اما قدرت تشخیص کد تمیز به معنای این نیست که ما میدانیم چگونه کد تمیز بنویسیم!! و این کار مستلزم استفاده از تکنیکهای ظریف بیشماریست. به طور خلاصه، برنامهنویسی که کد تمیز مینویسد، هنرمندیست که میتواند یک صفحه خالی را بگیرد و با انجام یک سری تغییرات آن را به یک سیستم با کدنویسی زیبا تغییر دهد.
کد تمیز چیست ؟
شاید به اندازهی تمام برنامهنویسها تعریف وجود داشته باشد. درنتیجه نظرات برخی از بزرگان برنامهنویسی را پرسیدیم که آنها چه فکر میکنند. ( من تمام حرفهای این عزیزان را به طور خلاصه و موردی برای شما نقل میکنم ).
در اینجا از آقایان Bjarne Stroustrup مخترع زبان c++، گردی بوچ نویسنده Object Oriented analysis and Design with Applications، آقای Dave Thomas پدرخوانده استراتژی Eclipse ، میشل فیدرز نویسنده Working Effectively with Legacy Code و Ron Jeffries نویسنده Extreme Programming Installed و #Extreme Programming Adventures in C ، سؤال شده.
مواردی که آقای Bjarne Stroustrup به آنها اشاره دارد به شرح زیر است:
1. کد باید زیبا و کارآمد باشد.
۲.منطق برنامه باید سر راست باشد تا پنهان کردن باگها دشوار باشد.
۳.وابستگی حداقلی برای نگهداری برنامه.
۴.کنترل کامل بر خطاها طبق یک استراتژی تکه به تکه و عملکردی بهینه.
همچنین ایشان ذکر میکنند که کد باید از نظر ظاهری و رفتاری دلپذیر باشد؛ همینقدر ساده! ظاهراً او فکر میکند خواندن یک کد تمیز لذت بخش است.( واقعاً هم یکی از لذتهای دنیاس :)))) ).
مواردی که آقای گردی بوچ گفتن هم اینهاست:
کد تمیز ، ساده و سر راست است؛ دقیقاً مثل یک نثر خوب. کد تمیز هرگز هدف طراح را مبهم بیان نمیکند. و در نهایت ایشون از عبارت crisp abstraction برای توصیف یک کد خوب استفاده میکنند. این یک کلمه با ضد و نقیض ولی جذاب است. در دیکشنری مک بوک من تعریف crisp یا (خشک) به این صورت است : قاطعانه و سرنوشت ساز، بدون تردید و جزيیات غیر ضروری. کد ما باید برخلاف حدس و گمانها، واقعی باشد و همینطور فقط شامل چیزهای مهم و ضروری. خوانندگان ما باید قاطعیت ما را درک کنند.
خب میریم سر وقت مواردی که آقای Dave thomas مؤسس OTI و پدرخوانده استراتژی Eclipse عرض میکنن :
۱.Unit test
۲.Acceptance test
۳. دارای اسامی معنادار باشد.
۴. کد تمیز به جای اینکه راههای زیادی را برای انجام یک کار ارائه دهد، یک راه را برای انجام یک کار دارد.
۵.کد تمیز حداقل وابستگی ممکن را دارد و یک API واضح و حداقلی را ارائه میدهد.
همچنین Dave تمیز بودن کد را با تستها پیوند میزند و میگوید: کد بدون تست تمیز نیست، مهم نیست که چقدر زیبا و قابل خواندن باشد، اگر تست نشده باشد کثیف است.
نفر بعدی آقای میشل فیدرز هستند:
میتوانم گفتههای ایشان را در یک جمله خلاصه کنم: اهمیت دادن! کدی تمیز است که از آن مراقبت شده باشد و شخصی وقت خود را برای ساده و منظم نگه داشتن آن صرف کرده باشد.
از نظر رون جفری :
۱. تمام تستها را اجرا کند.
۲.بدون کد اضافه یا duplicate باشد.
۳.تمام ایدههای طراحی موجود در سیستم را بیان کند.
۴.تعداد موجودیتها مثل کلاسها، متدها، فانکشن ها و موارد مشابه را به حداقل میرساند.
۵.درای اسامی معنا دار است.
وی بیشتر از همه به Duplicate تمرکز دارد. میگوید وقتی کارها بارها و بارها انجام شد، نشانگر این است که ایدهای در ذهن ما وجود دارد که به خوبی در کد نمایش داده نشده است
همینطور بیان میکند که قطعه کدها باید کوچک باشد، مثلاً اگر یک آبجکت داریم باید به دو یا چند آبجکت تبدیل شود و اگر متد باشد از روش refactoring extract method برای اینکه کاری را که یک متد انجام میدهد را واضحتر بیان کند، استفاده کنیم.
( حالا که انقدر درباره تست صحبت کردیم خوبه که خارج از کتاب یه کم بیشتر به مفهوم و لزوم استفاده ازش تأکید کنم :
ممکنه براتون سوال شده که TDD چی هست؟ TDD از ترکیب TFD یا همون Test First Design و Refactor هست. یعنی ما اول قبل از اینکه شروع کنیم به کد نویسی اول تست هارو طراحی میکنیم و بعد کد نویسی و بعدش هم رفاکتور میکنیم.
یه اشارهای به Reafactor کنیم بعد ادامه TDD رو میگیم. Reafactor یه پروسه بهبود طراحی کد هست بدون اینکه رفتار کدها تغییر کنه ،مثل خوانا کردن کد یا کلاس بندی یا هر چیزی که مربوط به طراحی کدها بشه .
بر میگردیم به TDD ، TDDشامل چندتا مرحله میشه :
1. اول یه تست نوشته میشه . (Add a test)
2.تست نوشته شده ازمایش میشه و چون هنوز کدی برای این تست نوشته نشده پس نتیجه Fail میشه (Run all tests and see if the new one fails)
3.نوشتن کدی که منجر به Pass شدن تستها میشه، البته فقط به اندازه کافی کد مینویسیم که فقط این تست هارو pass کنیم .(Write some code)
4.اجرای تستها و Pass شدن همه تستها ( اگه تست نوشته شده pass شد میریم به مرحله بعدی در غیر این صورت برمیگردیم به مرحله قبل )(Run tests)
5. Refactor code
6.پیاده سازی یه تست جدید Repeat
به این روش Red-Green هم گفته میشه که فکر میکنم مشخصه که دلیلش چیه (قرمز نشانه fail شدن و سبز نشانه pass شدنه)
خب شاید الان با خودتون بگید که اصلا این کار به چه درد میخوره ؟؟!
من سعی میکنم به چندتا از دلیلهاش اشاره کنم :
1. به اندازه کافی کد مینویسید نه بیشتر.
2.هزینه باگ زدایی کاهش پیدا میکنه.
3نوشتن تست دست اخر سخته و البته میشه گفت به درد نمیخوره .
4.محصولی که تولید میکنید بهینهتر میشه .
و خب البته هدف اصلی این روش همون حذف کدهای اضافی و پیاده سازی بهینه نرم افزار در کمترین زمان هست.)
و درپایان توجه داشته باشید که این زبان نیس که برنامهها را ساده جلوه میدهد بلکه این برنامهنویس است که باعث میشود زبان ساده به نظر برسد.
خب خلاصهی فصل اول کتاب همین بود، امیدوارم که خوشتون اومده باشه و ممنونم که برای مطالعه وقت گذاشتید. اگر هر انتقاد یا پیشنهادی داشتید در بخش نظرات با ما در میون بزارید.
فصل دوم – اسامی معنادار
مقدمه
اسمها همهجای نرمافزار وجود دارند. ما متغییرها، فانکشنها، آرگومانها، کلاسها و پکیجها یا سورس فایلهای خود و دایرکتوریهایی که شامل آنها میشن رو نامگذاری میکنیم. ما حتی فایلهای jar ، war، ear رو هم نامگذاری میکنیم. نامگذاری میکنیم، نامگذاری میکنیم و نام گذاری میکنیم؛ و از اونجایی که این کار رو زیاد انجام میدیم پس بهتر هست که این کار رو به روشی درست انجام بدیم. اون چه رو که در ادامه میخونید قوانینی ساده برای خلق اسمهای خوب هستند.
نکاتی که باید برای انتخاب یک اسم بهشون توجه کنین اینها هستن:
۱. استفاده از اسمهایی که منظور شما رو بیان کنند.
۲.از دادن اطلاعات اشتباه خودداری کنید.
۳.تفاوتهای معنادار ایجاد کنید.
۴.از اسمهای قابل تلفظ استفاده کنین.
۵.از اسمهای قابل جستجو استفاده کنین.
۶.از رمزگذاری دوری کنید.
۷.از mental mapping خودداری کنید. (به عبارتی استفاده از اسمهایی که برنامهنویسان قبلاً اونها رو با نام دیگری میشناسند
۸.نام کلاسها
.۹نام متدها
۱۰.بانمک بازی درنیارین.
۱۱.برای هر مفهوم یک کلمه انتخاب کنین.
۱۲.ایهام ایجاد نکنید.
۱۳.از دامنه کلمات مرتبط با راهحل استفاده کنین.
۱۴.از دامنه کلمات صورت مسأله استفاده کنین.
۱۵.کانتکست با معنی اضافه کنین.
۱۶.کانتکست بیخودی اضافه نکنین.
خب بریم که هر کدوم رو توضیح بدیم:
استفاده از اسمهایی که منظور شما را بیان کنند (Intention-Revealing Names)
اسمها باید منظور شما را بیان کنند؛ درواقع انتخاب کردن اسمهای خوب زمانبر هست ولی بیشتر از آنچه زمان میگیرد، زمان رو برای شما ذخیره میکنه. پس به اسمهاتون توجه کنید و هر وقت هم اسمهای بهتری پیدا کردید، اون هارو تعویض کنید. مطمئن باشید هم خودتون و هم هرکسی که کد شما را بخونه خوشحال خواهد شد. اسم یک متغییر، فانکشن یا کلاس باید به تمامی سؤالات پاسخ بده، اسم باید بگه چرا این وجود داره، چه کاری انجام میده و چطور استفاده میشه. اگر اسمی نیاز به کامنت داشته باشه پس نمیتونه منظور خودش رو برسونه.
int d; // elapsed time in days
اسم d در کدبالا هیچ منظور خاصی را منتقل نمیکنه و شما میتوجه نمیشید که این متغییر برای گذشت زمان سپری شده در روز هست. درواقع ما باید اسمی انتخاب کنیم که مشخص کنه چه چیزی در حال اندازهگیری هست و واحد و مقایس اون اندازهگیری چی هست:
int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;
انتخاب اسمهایی که منظور ما رو بیان میکنه، فهمیدن و تغییر دادن کد رو آسونتر میکنه. میتونید بگید کدی که در زیر وجود داره چه کاری میکنه؟
public List getThem() {
List list1 = new ArrayList;
for (int[] x : theList)
If (x[0] == 4)
List1.add(x);
Return list1;
}
چرا سخته که بگیم این کد چه کاری رو انجام میده؟ هیچ کد پیچیدهای وجود نداره، فاصلهها و تورفتگیها کاملاً معقول هستند و در کل سه متغییر و سه ثابت استفاده شده است، همینطور هیچ گونه کلاس عجیب غریب یا متد پیچیدهای هم وجود نداره و فقط یک لیست از آرایهها وجود داره. مشکل سادگی کد نیست، صراحت کد است. به عبارتی میزان صراحت کد در بیان منظور خودش. صراحت یک کد نیازمند این هست که ما به سؤالاتی مثل اینها پاسخ دهیم
۱. چه چیزهایی در theList وجود دارد؟
۲.اهمیت عضو صفرم یک آیتم در theList چیست؟
۳.اهمیت مقدار ۴ چیست؟
۴.چگونه از لیستی که برگشت داده شده (return شده) استفاده کنم؟
جواب سوالهای بالا در مثال قبل، قابل تشخیص نیستند ولی باید مشخص شوند. خب بیایین فکر کنیم که داریم روی بازی مین روب کار میکنیم بنابراین میدونیم که صفحه بازی ما یک لیست از سلولهاست که اون رو با theList نمایش میدیم. خب بیایین اسم اون رو به gameBoard تغییر بدیم و باید بگم که هر سلول در صفحه توسط یک آرایه ساده نمایش داده میشه. همینطور این رو هم میدونیم که مقدار صفرم وضعیت سلول هست و وضعیت ۴ هم به معنای این هست که "پرچم گذاری شده".بیایین فقط با دادن اسامی این کانسپتها، کد رو به طور قابل توجهی بهبود ببخشیم:
public List getFlaggedCells() {
List flaggedCells = new ArrayList();
for (int[] cell : gameBoard)
If (cell[STATUS_VALUE] == FLAGGED)
flaggedCells.add(cell);
return flaggedCells;
}
خب دقت داشته باشین که سادگی کد تغییری نکرد و هنوز هم همون تعداد عملگرها و ثابتها و دقیقاً با همون تعداد تورفتگی وجود دارن؛ اما کد صراحت بسیار بیشتری پیدا کرد. حالا بیایین یکم پامون رو فراتر بزاریم به جای یک آرایه از اعداد، یک کلاس ساده برای سلولها بنویسیم که این کلاس میتونه شامل یه فانکشن باشه ( که اون رو isFlagged مینامیم) و این کار باعث میشه تا این عددهای عجیب از کد حذف بشن که منجر به، به وجود اومدن نسخهی جدیدی از فانکشن میشه.
public List getFlaggedCells() {
List flaggedCells = new ArrayList();
for (Cell cell : gameBoard)
if (cell.isFlagged())
flaggedCells.add(cell);
return flaggedCells;
}
با انجام این تغییرات ساده دیگه فهمیدن اینکه این کد چه کاری انجام میده سخت نیست. و این است قدرت انتخاب کردن اسمهای خوب!!
از دادن اطلاعات اشتباه خودداری کنید
ما باید از کلماتی که مفهومشان با منظور ما فاصله زیادی دارن اجتناب کنیم. به عنوان مثال، کلمههای hp , aix و sco اسمهای ضعیفی برای متغییرها هستند؛ چراکه اسامی یونیکس پلتفرم هستند. حتی اگه در حال کدنویسی Hypotenuse هستید ( اگه احیاناً یادتون رفته که Hypotenuse چی هست در ادامه یه اشاره کوچیک بهش میکنم) و فکر میکنین hp مخفف خوبی واسش به نظر میرسه، اما باید بگم که بازم ممکنه باعث دادن اطلاعات اشتباه بشه ( یا به عبارتی disinformation باشه)
Hypotenuse
خب بریم ادامه بحث...
هیچ وقت به یک لیست از اکانتها،نام accountList رو ندید چون اون واقعاً یه لیسته ولی کلمهی List معنی یه چیز خاص رو برای برنامهنویسا داره. اگه یه کانتینر اکانت نگهداری میکنه به این معنی نیست که یک List هست و همونطور که گفتیم میتونه منجر به disinformation بشه. پسaccountGroup ،bunchOfAccounts یا فقط accounts اسمهای بهتری هستن.
از استفاده از اسمهایی که تفاوتهای جزئی و کوچک باهم دارن هم خودداری کنین، مثلاً چقدر طول میکشه که تفاوت جزئی این XYZControllerforEfficientHandlingOfStringsin رو در یک ماژول و بعد یکم جلوتر با این XYZControllerforEfficientStorageOfStrings بفهمید و باهم اشتباه گرفته نشن؟ چراکه هر دو اینها، شکل یکسانی دارن.
مثالی از اسمهایی که اطلاعات غلط میدن، اسمهایی هستن که از حروف مثل هم استفاده میکنن. مثل کد زیر:
int a = l;
if(0==1)
a=01;
else
l = 01;
خب شاید فکر کنین که همچین چیزی محاله اما ما به وفور این موارد رو در کدهای مختلف مشاهده کردیم. در وهله اول نویسنده پیشنهاد میده که فونت متن رو عوض کنین تا قابل دیدن باشه اما این راهحل رو باید در داکیومنت بنویسید یا به طور شفاهی به برنامهنویسان بگید و واضحه که بهترین راهحل یه تغییر اسم ساده هست.
تفاوتهای بامعنی ایجاد کنید
برنامهنویسا با نوشتن کدهایی که کامپایلر پسند هستن خودشون رو توی زحمت میندازن، شما هیچ وقت نمیتونین از یه اسم داخل دوتا متغییر استفاده کنین یا حتی ممکنه اگه املای اون رو هم تغییر بدین مشکلاتی در کامپایل نرمافزار اتفاق بیفته؛اضافه کردن عدد و حروف هم که کار بیهودهایه. پس یه کلام اگه اسمها باید متفاوت باشن، معنی اونها هم باید متفاوت باشه.
(a1,a2,…,aN) اینطور نامگذاری هم که کلاً با اصول نامگذاری در تضاده، شاید فکر کنین چون اطلاعات غلط میدن، اما نه! درواقع اصلاً اطلاعاتی نمیدن؛ مثلاً به این کد نگاه کنین:
public static void copyChars(char a1[], char a2[] {
for (int i=0; i<a1.length; i++) {
A2\[[i] = a1[i];
}
}
اگه از source و destination استفاده میشد فانکشن خواناتری داشتیم.
noise word ها هم دستهی دیگهای از اسامی بیمعنی هستن. فکر کنین که یه کلاس به اسم Product دارین . اگه کلاسای دیگهای با اسمای ProductInfo و ProductData درست کنین، شاید اسماتون متفاوت باشه ولی معانی متفاوتی نداره و همش یکیه. البته استفاده از پریفیکسهایی مثل a, an و the در نام گذاری مشکلی نداره؛ همهی مشکلا از اون جایی شروع میشه که مثلاً اسم یک متغییر رو theZork بزارین درحالی که یه متغییر دیگه به نام Zork دارین.
توجه کنین که هیچ وقت نباید از کلمه Variable در یک متغییر و از اسم table در یک Table استفاده کنید.
چطور ممکنه NameString از Name بهتر باشه؟ مثلاً ممکنه Name یه عدد اعشاری باشه؟ خب اگه جوابتون اره هست باید بگم که کل قوانین رو زیر سؤال بردین!
فرمی از نامگذاری اشتباه :
getActiveAccount();
getActiveAccounts();
getActiveAccountInfo();
برنامهنویس چطوری متوجه میشه از کدوم استفاده کنه؟
تفاوتmoneyAmount از money یا customerInfo از customer یا accountData از account و theMessage از message غیر قابل تشخیصه. نامهایی قابل تشخیصه، که تفاوت رو به خواننده نشون بده.
از اسمهای قابل تلفظ استفاده کنین.
قسمتهای مشخصی از مغزما برای درک مفهوم کلمات هست و مفهوم کلمات با تلفظ اونها ارتباط مستقیم دارند، درواقع مغز ما کلمات را با توجه به تلفظ اونها درک میکنه نه صورت نوشتاری کلمات.
اگه نتونین تلفظ کنین پس نمیتونین راجع به چیزی بحث کنین. مثل این جمله :
Well,over here on the bee cee arr three cee enn tee we have a pee ess zee kyew it, see?
پس متوجه اهمیت موضوع شدین! یه کمپانی رو میشناسم که مفهومی به اسم genymdhms داره،(generation date, year, month, day, hour, minute and second ) و اونا برای به خاطر سپردن این مفهوم، این جمله را مدام تکرار میکنن "gen why emm dee aich emm ess". منم عادت بدی دارم که همه چیز رو همون جور که نوشته شده تلفظ میکنم و نهایتاً شروع کردم به گفتن "gen-yah-mudda-hims." و بعدها این مفهوم توسط جمعی از آنالیزورها و طراحان به همین اسم نامیده شد؛ این برای ما مثل یک شوخی بود اما چه بانمک باشه چه نه، ما داریم با نام گذاری ضعیف کنار میاییم. مقایسه کنین :
class DtaRcrd102 {
private Date genymdhms;
private Date modymdhms;
private final String pszqint = “102”;
/\* _... \*_/
}
یا این؟
class Customer {
private Date generationTimestamp;
private Date modificationTimestamp;
private final String recordId = "102";
/\* _... \*_/
}
از اسمهای قابل جستجو استفاده کنین.
از اسامی که در متن قابل پیدا کردن نیستن استفاده نکنین، به عنوان مثال استفاده از حروفی مثل e که بسیار در کلمات پرکاربرد هستن یا استفاده از اعداد طولانی( ممکنه کسی عدد رو بعداً تغییر بده و دیگه پیداش نکنین) یا حتی اگه عدد طولانی هم نباشه و مثلاً از عدد ۷ استفاده کنین قطعاً پیدا کردن اون رو برای شما زمانگیر خواهدکرد.
به همین علت، نامهای طولانی نسبت به نامهای کوتاه برتری دارند. البته طول یک نام باید با اندازهی اسکوپ اون هم مطابقت داشته باشد؛ پس اگه یک متغییر یا ثابت باید در جاهای مختلفی از یک کد استفاده بشه، استفاده از یک اسم search- friendly ضرورت داره. یه بار دیگه مقایسه کنین:
for (int j=0; j<34; j++) {
S+= (t[j]*4)/5;
}
یا این؟
int realDaysPerIdealDay = 3;
const int WORK_DAYS_PER_WEEK = 5;
int sum = 0;
for (int j=0; j<NUMBER_OF_TASKS; j++) {
int realTaskDays = taskEstimate[j] * realDaysPerIdealDay;
int realTaskWeeks = (realdays / WORK_DAYS_PER_WEEK);
sum += realTaskWeeks;
}
به sum دقت کنین، اسم کاملاً مناسبی نیست اما حداقل قابل جستجو کردن هست. ممکنه کدنویسی با این اسمها یکم طولانیتر بشه اما این رو هم در نظر داشته باشین که پیدا کردن WORK_DAYS_PER_WEEK چقدر راحته.
از رمزگذاری دوری کنید
همنیطوری هم سرکله زدن با رمزنگاری سخته پس لطفاً دردسر جدید اضافه نکنین. اسمهای رمزگذاری شده سخت تلفظ میشن، و فهمیدن اونها برای برنامهنویسان جدید هم مشکله. پس از این کار دوری کنین.
Member prefixes
شما همچنین نیازی به پرفیکس m_ در member variable ها ندارین. کلاسها و فانکشنهای شما باید بهقدری کوچیک باشه که نیازی به اونها نباشه و همینطور شما باید از ویرایشگری استفاده کنین که member ها رو بولد یا رنگی کنه و اونها رو متمایز کنه.
public class Part {
private String m_dsc; // The textual description
void setName(String name) {
M_dsc = name;
}
}
public class Part {
String description;
void setDescription(String description) {
this.description = description;
}
}
علاوه بر این افراد به سرعت یادمیگیرن که پریفیکس، پیشوند یا ( سافیکس، پسوند) رو نادیده بگیرن تا قسمت با معنی اسم رو ببینن. هر چه که بیشتر کد رو بخونن، کمتر پریفیکسهارو میبینن.
Interfaces and implementations
گاهی اوقات موارد خاصی برای رمزگذاری وجود دارن. به طور مثال به شما میگن که یک ABSTRACT FACTORY برای ایجادکردن شکلها پیادهسازی کنین. این factory یک اینترفیس خواهد بود و توسط یک کلاس concrete پیادهسازی خواهد شد.خب چه اسمی باید برای اینها انتخاب کنم؟ IShapeFactions و ShapeFective ؟ من ترجیح میدم که اینترفیس رو بدون اینکه آراستش کنم ولش کنم. من نمیخوام کاربرانم بدونن که من اینترفیس خودم رو به اون ارائه میدم، همین که بدونن این یک shape factory هست کافیه. اسم اون رو ShapeFactoryImp یا حتی CshapeFective بزارین، رمزگذاری اینترفیسها واجب و ضروری هست.
اسامی معنادار
توی بخش قبلی توضیح دادیم که چه نکاتی رو باید در زمان نامگذاری بهشون توجه کنین و تک تک توضیحشون دادیم ( اگه احیاناً فراموش کردین یه نگاه به پایین بندازین تا یادتون بیاد)
۱. استفاده از اسمهایی که منظور شما رو بیان کنند.
۲.از دادن اطلاعات اشتباه خودداری کنید.
۳.تفاوتهای معنادار ایجاد کنید.
۴.از اسمهای قابل تلفظ استفاده کنین.
۵.از اسمهای قابل جستجو استفاده کنین.
۶.از رمزگذاری دوری کنید.
۷.از mental mapping خودداری کنید. (به عبارتی استفاده از اسمهایی که برنامهنویسان قبلاً اونها رو با نام دیگری میشناسند)
۸. نام کلاسها.
۹.نام متدها.
۱۰.بانمک بازی درنیارین.
۱۱.برای هر مفهوم یک کلمه انتخاب کنین.
۱۲.ایهام ایجاد نکنید.
۱۳.از دامنه کلمات مرتبط با راهحل استفاده کنین.
۱۴.از دامنه کلمات صورت مسأله استفاده کنین.
۱۵.کانتکست با معنی اضافه کنین.
۱۶.کانتکست بیخودی اضافه نکنین.
تو بخش قبلی تا مورد ششم رو براتون گفتم و اما بریم که ببینیم در ادامه عمو باب چی گفته …
از mental mapping خودداری کنید
به طور کلی نباید از اسمهایی استفاده کنید که برنامهنویس در ذهنش اونها رو به اسمهای دیگهای که از قبل میشناسه ترجمه کنه.
این مشکل مربوط به متغییرهای تک حرفیه، به طور مثال i ، j یا k همیشه از قدیم به عنوان متغییر حلقه استفاده میشدند. البته با وجود این، انتخاب یک نام تک حرفی زیاد انتخاب جالبی نیست.
در کل برنامهنویسا آدمای خیلی باهوشی هستن و آدمای باهوش دوس دارن با نشون دادن تواناییهاشون میزان هوش خودشون رو به نمایش بزارن ( به عبارتی شواف یاshow off کنن).
اما تفاوت بین یک برنامهنویس هوشمند با یه برنامهنویس حرفهای این هست که حرفهایها میدونن که شفافیت چقدررر مهمه؛ پس حرفهایها از توانایی خودشون برای نوشتن کدی که دیگران بتونن به راحتی اون رو درک کنن استفاده میکنند.
نام کلاسها
کلاسها و آبجکتها باید اسمهایی مثل Account، WikiPage، Customer و AdressParser داشته باشن و نبایداز کلماتی مثل Data، Processor، Manager و Info برای نام کلاسها استفاده کنید. نام کلاس نباید یک فعل باشد.
نام متدها
برای نامگذاری متدها باید از فعل یا عبارات دارای فعل مثل DeletePage، postPayment یا save استفاده کنید. Accessorها، mutatorها، و predicateها رو هم باید با مقادیر خودشون نامگذاری کنید و طبق استاندارد javabean، از پریفیکسهای set، get و is استفاده کنید.
String name = employee.getName();
customer.setName("mike")
if (paychech.isPosted()) ...
وقتی که constructor متدها( متدهای سازنده) زیاد میشن، از factory method های استاتیک با اسمهایی که آرگومانها را توصیف میکنند استفاده کنید. به طور مثال این خط کد :
Complex FulcrumPoint = Complex.FromRealNumber(23.0);
از این خط کد بهتر هست :
Complex FulcrumPoint = new Complex(23.0);
بانمک بازی درنیارین
استفاده از اسمهایی که خیلی هوشمنداس،فقط در ذهن افرادی موندگار میشه که از نظر شوخطبعی شبیه به نویسنده اصلی باشن، البته اونم تا وقتی که اون شوخی رو به یاد بیارن. مثلاً یک فانکشن با اسم HolyHandGrenade دقیقاً چیکار می کنه؟خب اره بامزس اما قطعاً DeleteItems اسم خیلی بهتریه. اغلب این بامزگیا به صورت محاوره ظاهر میشن، مثلاً از ()whack به جای ()kill استفاده نکنین؛ همینطور استفاده از شوخیهای خاص مختص به یک فرهنگ مثل استفاده از eatMyShors به جای abort هم کار درستی نیست.
شفافیت رو انتخاب کنید نه سرگرمی!
برای هر مفهوم یک کلمه انتخاب کنین
برای هر مفهوم یک کلمه انتخاب کنین و تا آخر بچسبید به همون! به طور مثال استفاده از retrieve، fetch و get به عنوان اسم یک متد یکسان در کلاسهای متفاوت گیجکننده هست.
محیطهای ویرایشی مدرن مثل Eclipse و IntelliJ به شما context-sensitive clues ( یا به عبارتی سرنخهایی که به متن حساس باشن) میدن، اما توجه کنین که لیستی که به شما میده شامل کامنتهای مختلفی که برای اسم فانکشن و لیست پارامترهاتون نوشتید نمیشه. شما باید بتونید بدون جستجوهای اضافی به چیزی که میخوایید برسید.
یک واژه نامه متناقض میتونه لطف بزرگی به برنامهنویسهایی که باید از کد شما استفاده کنن باشه.
ایهام ایجاد نکنید
از استفاده از یک کلمه برای دو منظور متفاوت خودداری کنین، درواقع استفاده از یک اصطلاح یکسان برای دو ایدهی متفاوت ، ایجاد ایهام هست.
اگه از قانون بالا یعنی ( استفاده از یک کلمه برای هر مفهوم ) پیروی میکنین، به طور مثال میدونید که خیلی از کلاسها مثلاً یک متد مثل add دارن، خب تا وقتی که لیست پارامترها و مقادیر برگشتی ( return value ) در متدهای مختلف با نام add معنای یکسانی داشته باشن، همه چیز درسته؛ اما شاید یکی بیاد و تصمیم بگیره که از کلمه add برای مفهومی غیر از اضافه کردن استفاده کنه، مثلاً کلاسی داریم که دارای متدی هست که یک پارامتر جدید رو داخل یک کالکشن قرار میده، خب حالا باید اسم این متد رو add بزاریم؟
ممکنه متدهای دیگهای به اسم add داشته باشیم و این متد هم با اونا سازگار به نظر برسه ولی در این حالت، مفاهیم متفاوتی داریم. مثلاً میتونیم به جای add از insert یا append استفاده کنیم.
یه بار دیگه عرض میکنم : شفاف بنویسید، طوری که نیاز به مطالعه عمیق نداشته باشه و سریع قابل فهمیدن باشه.
از دامنه کلمات مرتبط با راهحل استفاده کنین
یادتون باشه که آدمایی که قراره کد شما رو بخونن،برنامهنویس هستند؛ بنابراین از اصطلاحات علوم کامپیوتر، نام الگوریتمها، اصلاحات ریاضی و موارد اینطوری استفاده کنین، عاقلانه نیست که هر اسمی رو از دامنه صورت مسأله انتخاب کنیم، چون نمیخواییم همکارانمون رو مجبور کنیم تا از مشتری سؤال بپرسه در صورتی که خودش مفاهیم رو با اسمهای دیگهای میشناسه.
مثلاً اسم AccountVisitor برای برنامهنویسی که با پترن VISITOR آشناست، خیلی پرمعنیه. چه برنامهنویسی هست که ندونه JobQueue چیه؟
انتخاب نامهای تکنیکال یا به عبارت دیگه فنی، برای موارد فنی زیادی که برنامهنویسان انجام میدن، معمولاً مناسبترین روش هست.
از دامنه کلمات صورت مسأله استفاده کنین
وقتی هیچ اصطلاح برنامهنویسی برای کاری که انجام میدین وجود نداره، از دامنه کلمات صورت مسأله استفاده کنید. حداقل این کار این هست که برنامهنویس میتونه از یک متخصص دامنه سؤال کنه که منظور فلان چیز چیه؟
جداکردن دامنه کلمات راهحل و صورت مسأله بخشی از کار یک برنامهنویس و طراح خوب هست. کدی که ارتباط بیشتری با مسائل مربوط به صورت مسأله داره، باید از نامهایی استفاده کنه از صورت مسأله گرفته شده باشه.
کانتکست با معنی اضافه کنین
خب میدونید که فقط یه سری از اسمها هستن که به خودی خود بامعنی هستن و بقیه اکثراً بیمعنین. در اینجا باید این اسمهارو در کلاسها، فانکشنها و … قرار بدین تا خواننده در جریان موضوع قرار بگیره.البته استفاده از پریفیکس هم یکی از راه حلهاست.
فکر کنین که متغییرهایی با اسمهای State، city، houseNumber، street، lastName، firstName و zipcode دارین، که همشون باهم یک آدرس رو تشکیل میدن. اما حالا فرض کنین که متغییر State رو تنها ببینین! آیا خودکار میفهمین که این بخشی از یک آدرس هست؟ خب میتونید به اونا پریفیکس اضافه کنین: addrState، addrLastName، addrFirstName و … و اینطوری حداقل خواننده میفهمه که این متغییر جز یک ساختار بزرگتره، البته راهحل بهتر این هست که یه کلاس به اسم Address درست کنین و بعد حتی کامپایلر هم میفهمه که متغییرها متعلق به یک ساختار بزرگتر هستن.
یک متد را در ( Listing 2-1 ) درنظر بگیرین؛ فکر میکنین متغییرها به یک ساختار بامعنیتر نیاز دارن؟ بعد از اینکه این فانکشن رو خوندید متوجه میشید که متغییرهای verb، number و pluralModifier بخشی از پیام "guess statistics" هستند. متأسفانه با اولین نگاه، معنی متغییرها مبهم هست و مفهوم اونها باید حدس زده بشه.
Listing 2-1 Variables with unclear context.
private void printGuessStatistics(char candidate, int count)
{
String number;
String verb;
String pluralModifier;
if (count == 0){
number = "no";
verb = "are";
pluralModifier = "s";
} else if (count == 1) {
number = "1";
verb = "is";
pluralModifier = "";
} else {
number = Integer.toString(count);
verb = "are";
pluralModifier = "s";
}
String guessMessage = String.format( "There %s %s %s%s", verb, number, candidate, pluralModifier );
print(guessMessage);
}
خب همونطور که میبینید این فانکشن یکم طولانیه، بنابراین اون رو به بخشهای کوچکتر تبدیل میکنیم و برای این کار یک کلاس GuessStatisticsMessage ایجاد میکنیم
و سه فیلد متغییر این کلاس رو میسازیم، و بعد هم یک متن واضح برای هر سه فراهم میکنیم. این سه متغییر به طور قطع بخشی از GuessStatisticsMessage هستند.
این کار (یعنی شکستن کد به قطعات کوچتر) باعث تمیزتر شدن و افزایش خوانایی کد میشه.
Listing 2-2 Variables have a context.
public class GuessStatisticsMessage{
private String number;
private String verb;
private String pluralModifier;
public String make(char candidate, int count){
createPluralDependentMessageParts(count);
return String.format( "There %s %s %s%s", verb, number, candidate, pluralModifier );
}
private void createPluralDependentMessageParts(int count{
if (count == 0){
thereAreNoLetters();
} else if (count == 1) {
thereIsOneLetter();
} else {
thereAreManyLetters(count);
}
}
private void thereAreManyLetters(int count) {
number = Integer.toString(count);
verb = "are"; pluralModifier = "s";
}
private void thereIsOneLetter() {
number = "1";
verb = "is";
pluralModifier = "";
}
private void thereAreNoLetters() {
number = "no";
verb = "are";
pluralModifier = "s";
}
}
کانتکست یا متن بیخودی اضافه نکنین
خب فرض کنین که یک اپلیکشین با اسم Gas Station Deluxe داریم، حالا اینکه بیاییم به همهی کلاسهامون پریفیکس GSD بدیم خیلی ایدهی بدیه. صادقانه بگم، با این کار واسه خودتون دردسر درست میکنین.
مثلاً کلاس MailingAddress رو با پریفیکس GSD بنویسید (GSDAccountAddress) و خب با این کار فقط یک سری کاراکتر بیخودی رو اضافه کردین. معمولاً تا زمانی که نامهای کوتاه واضح باشن، استفاده از نامهای طولانی کار بیهودهایه، پس هیچ وقت هیچ متنی بیشتر از اون چیزی که نیاز هست اضافه نکنین.
سعی کنین این قوانین رو دنبال کنین تا خوانایی کد خودتون رو بهبود ببخشید. از تغییر دادن اسمهاتون نترسید و بهترین چیزی که ممکن هست رو انتخاب کنین؛ مطمئن باشین بعد از یه مدت کوتاه نتیجه رو میبینید و در طولانی مدت هم این مسأله به شما کمک خواهد کرد.
فصل سوم : فانکشنها
اون اوایل برنامهنویسی، ما سیستمهای خودمون رو با routineها و subroutine ها میساختیم ( اگه احیاناً نمیدونین چیه یه نگاهی به اینجا بندازین). بعدها، یعنی زمانی که Fortran و PL / 1 بود، سیستمهامون رو با برنامهها، زیربرنامهها و فانکشنها میساختیم و حالا تنها چیزی که از اون دوران باقی مونده همین فانکشنها یا توابع هستند؛ و دقیقا موضوع این فصل درباره چگونگی نوشتن یک فانکشن خوب هست!
به Listing 3-1 یه نگاه بندازین، پیدا کردن یه فانکشن تو این کد سخته، نه؟ من بعد از یکم جستجو یکی رو پیدا کردم اما این فانکشن نه تنها طولانیه بلکه یه عالمه کد تکراری و یه سری رشته عجیب غریب به علاوه یه سری انواع داده و API های مبهم داره. شما ببینید توی ۳ دقیقه چقدرش رو میتونین درک کنین؟!
Listing 3-1
HtmlUtil.java (FitNesse 20070619)
public static String testableHtml(PageData pageData,boolean includeSuiteSetup) throws Exception {
WikiPage wikiPage=pageData.getWikiPage();
StringBuffer buffer=new StringBuffer();
if(pageData.hasAttribute("Test")){
if(includeSuiteSetup){
WikiPage suiteSetup=PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_SETUP_NAME,wikiPage);
if(suiteSetup!=null){
WikiPagePath pagePath=suiteSetup.getPageCrawler().getFullPath(suiteSetup);
String pagePathName=PathParser.render(pagePath);
buffer.append("!include -setup .")
.append(pagePathName)
.append("\n");
}
}
WikiPage setup=PageCrawlerImpl.getInheritedPage("SetUp",wikiPage);
if(setup!=null){
WikiPagePath setupPath=wikiPage.getPageCrawler().getFullPath(setup);
String setupPathName=PathParser.render(setupPath);
buffer.append("!include -setup .")
.append(setupPathName)
.append("\n");
}
}
buffer.append(pageData.getContent());
if(pageData.hasAttribute("Test")){
WikiPage teardown=PageCrawlerImpl.getInheritedPage("TearDown",wikiPage);
if(teardown!=null){
WikiPagePath tearDownPath=wikiPage.getPageCrawler().getFullPath(teardown);
String tearDownPathName=PathParser.render(tearDownPath);
buffer.append("\n")
.append("!include -teardown .")
.append(tearDownPathName)
.append("\n");
}
if(includeSuiteSetup){
WikiPage suiteTeardown=PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_TEARDOWN_NAME,wikiPage);
if(suiteTeardown!=null){
WikiPagePath pagePath=suiteTeardown.getPageCrawler().getFullPath(suiteTeardown);
String pagePathName=PathParser.render(pagePath);
buffer.append("!include -teardown .")
.append(pagePathName)
.append("\n");
}
}
}
pageData.setContent(buffer.toString());
return pageData.getHtml();
}
تونستین چیزی بفهمین؟ احتمالاً نه، همونطور که گفتم رشتهها و فراخوانی فانکشنها با ifهای تو در تو که توسط Flag ها کنترل میشه خیلی عجیب غریبه. با این حال فقط با اکسترکت(یا همون استخراج) کردن چند متد ساده و تغییر نام و ساختار، تونستم هدف این فانکشن رو در ۹ خط در لیست 2-3 پیادهسازی کنم. حالا ببینید میتونید توی ۳ دقیقه این رو درکش کنین؟
Listing 3-2
HtmlUtil.java (refactored)
public static String renderPageWithSetupsAndTeardowns( PageData, boolean isSuite) throws Exception
{
boolean isTestPage = pageData.hasAttribute("Test");
if (isTestPage)
{
WikiPage testPage = pageData.getWikiPage();
StringBuffer newPageContent = new StringBuffer();
includeSetupPages(testPage, newPageContent, isSuite);
newPageContent.append(pageData.getContent());
includeTeardownPages(testPage, newPageContent, isSuite);
pageData.setContent(newPageContent.toString());
}
return pageData.getHtml();
}
به غیر اینکه شما کاملاً از FitNesse سر دربیارین میتونید متوجه تمامی جزيیات بشین؛ با وجود این احتمالاً متوجه میشین که این فانکشن برای وارد کردن صفحات setup و teardown به یک صفحهی تست و تبدیل اون صفحه به HTML هست. اگه با باJUnit آشنا باشین احتمال زیاد میدونین که این فانکشن برای یه نوع فریمورک تست مبتنی بر وب هست.
همونطور که دیدین لیست دوم نسبت به لیست اول خیلی خواناتر بود، اما فکر میکنین چی باعث میشه که ما یه تابع رو خیلی آسون درکش کنیم؟ یا چه ویژگیهایی رو میتونیم به فانکشنمون اضافه کنیم تا حتی کسی که به طور تصادفی تابع مارو میخونه بتونه راحت اون رو بفهمه و هدفش رو درک کنه؟
کوچیک باشه!
اولین قانون برای فانکشنها کوچیک بودن اوناست و خب قانون دومم اینه که حتی فانکشنها باید کوچیکتر از اون فانکشن کوچیک هم باشند(امیدوارم جمله رو فهمیده باشین :))))
قدیما یعنی حدوداً دههی هشتاد میگفتن یه تابع نباید بزرگتر از صفحه نمایش باشه، البته اینو وقتی میگفتن که VT100 بود( عکسش رو پایین براتون گذاشتم ببینین روحیتون عوض شه) و ۲۴ خط و ۸۰ ستون توی این اسکرینها جا میشد، اما حالا خداروشکر اسکرینها بزرگ و جاداره و به طور متوسط می تونین ۱۵۰ کاراکتر رو در یک خط و ۱۰۰ خط یا بیشتر رو در یک صفحه داشته باشیم؛ امااا این دلیل نمیشه که فانکشنهامونم بزرگ باشه، درواقع فانکشنهاتون باید به سختی به ۲۰ خط برسه. ( درواقع دو، سه یا چهار خط خیلی عالیه)
خب شاید بگین یه تابع باید چقدر کوتاه باشه؟ معمولاً باید از لیست 2-3 کوتاهتر باشه! درواقع لیست 2-3 باید به اندازه لیست 3-3 کوتاه بشه. (در این حد کوتاه!)
Listing 3-3
HtmlUtil.java (re-refactored)
public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) throws Exception
{
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}
خب بزارین همینجا یه مطلب دیگه رو درباره متدها براتون بگم:
خط اصول نگهداری کد حدود ۱۰ تا اصل هست که تضمین میکنه کد شما هزینه کمتری برای تولید داره، نگهداری اون راحتتر خواهد بود و کیفیت نرمافزار شما هم به شدت بالا میره. به عنوان مثال اولین قانون نگهداری این هست که هر متد نباید تعداد خط کد زیادی داشته باشه و طبق همین اصل هم میتونیم برنامهها رو دسته بندی کنیم:
۱. برنامه ۵ ستاره : همه متدها زیر ۱۵ خط کد دارن.
۲. برنامه ۴ ستاره : اکثر متدها زیر ۱۵ خط کد هستن ولی میانگین کمتر از ۳۰ خط هست.
۳. برنامه ۳ ستاره : میانگین ۳۰ خط هست ولی کدهای بیشترم توش پیدا میشه.( قابلیت تعمیر و نگهداری متوسط رو به پایین)
۴.برنامه ۲ ستاره : کدهای بیشتر از ۴۰ خط داره.( قابلیت تعمیر و نگهداری خیلی خیلی پایین هست)
اینجاست که میگن کم گوی و گزیده گوی چون دُر ( البته خب شایدم ربطی نداشته باشه نمیدونم) ولی به هرحال، آقاااجون کم بنویس! )
بلوکها و تو رفتگیها
لیست 3-3 به این مورد اشاره داره که بلوکهای موجود در if , else یا while و … باید به اندازهی یک خط طول داشته باشن و احتمالاً اون خط هم باید مربوط به فراخوانی یه تابع باشه. این کار نه تنها تابع رو کوچیک نگه میداره بلکه ارزش مستندسازی هم بش اضافه میکنه. همینطور این لیست نشون میده که فانکشنها برای نگه داشتن ساختارهای تو در تو نباید زیاد بزرگ باشن و تورفتگی یک تابع هم نباید بیشتر از یکی یا دوتا اسپیس باشه که البته این کار باعث میشه درک کردن توابع آسونتر بشه.
یک کارو انجام بده
خیلی واضحه که فانکشن 1-3 بیشتر از یک کارو انجام میده. درواقع بافر میسازه، صفحات رو فچ میکنه، دنبال صفحاتی که ارث بری کردهاند میگرده، مسیرهارو رندر میکنه و HTML ایجاد میکنه. ولی دقت کنین که از اون طرف لیست ۳-۳ فقط یک کاره ساده رو داره انجام میده.
پس یک کلام، تابع باید مثه آدم یک کار انجام بده و البته که خوب انجامش بده؛ شما نباید وقتی یه نفر ازتون پرسید این تابع واسه چیه مجبور بشین ۱ ساعت براش توضیح بدین؛ فقط خیلی شیک بگین این تابع داره فلان کارو ( مثلاً جمع دو عدد) رو انجام میده و تماام.
و حالا شاید بگین چطوری بفهمیم فانکشن ما داره فقط یک کارو انجام میده؟ خب دوتا شرط داره:
۱. اگه بتونین از داخل تابعی که دارین، یه تابع دیگه رو استخراج کنین و اسم تابع جدید هیچ ربطی به تابع قبلی نداشته باشه یعنی فانکشنی که داری یه کارو انجام نمیده.
۲. سطوح انتزاع: به لیست 3-3 دقت کنین، به نظرتون این فانکشن فقط یک کارو انجام میده؟ به راحتی میتونیم بگیم که این تابع داره سه تا کارو انجام میده:
۱. مشخص میکنه که آیا این یک صفحه تست هست یا نه.
۲.اگه تست بود، setups و teardowns رو اضافه میکنه.
۳. صفحه رو در HTML رندر میکنه.
خب پس چرا گفتیم این تابع فقط یک کارو انجام میده؟ داستان چیه؟ توجه داشته باشین که این سه مرحله در یک سطح از انتزاع قرار دارن. پس میتونیم بگیم که این تابع یک کارو انجام میده.
بزارین یه مثال سادهتر بزنم: مثلاً اگه فانکشن شما داره اسم یه سری از افراد رو ست میکنه نیایین وسط این کار چک کنین ببینین شغل طرف چیه! این مورد باید بره توی یک تابع دیگه چون سطح انتزاعش با مورد قبلیش فرق داره.
خیلی واضحه که لیست 1-3 دارای سطوح زیاد و مختلفی از انتزاع هست؛ حتی لیست 2-3 هم دوتا سطح انتزاع داره اما شک کردن به لیست 1-3 خیلی سخته، در حالی که میتونیم قسمتی که IF وجود داره رو به شکل یه متد مثلاً با اسم includeSetupsAndTeardownsIfTestPage استخراج کنیم، اما باز کد ما بدون تغییر یک سطح انتزاع رو بیان میکنه.
خوندن کد از بالا به پایین : قانون گام به گام
میخواییم کدی که داریم مثل یه روایت از بالا به پایین خونده بشه؛ درواقع توابع با سطوح انتزاع مختلف پشت سر هم قرار بگیرن( هر تابع با سطح انتزاع بعدی دنبال بشه) به عبارتی وقتی لیست فانکشنها رو میخونیم یکی یکی از سطح انتزاع اونا کم بشه که بفهمیم چی به چیه؛ و اینکه عمو باب به این قاعده میگه گام به گام.
هر تابع، تابع بعدی رو معرفی کنه و در عین حال هر تابع در یک سطح ثابت از انتزاع باقی بمونه.
Switch Statements
خیلی سخته که بخواییم کلاً استفاده از switch statement رو کنار بزاریم چون به هرحال بعضی وقتا نیاز میشه؛ اما وقتی از switch statementاستفاده میکنی یه سری مشکلات به وجود میاد. بیایین اول به این کد یه نگاه بندازین تا مشکلاتش رو براتون بگم :
Listing 3-4
Payroll.java
public Money calculatePay(Employee e) throws InvalidEmployeeType
{
switch (e.type)
{
case COMMISSIONED:
return calculateCommissionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}
توی این فانکشن چندین مشکل وجود داره:
۱. اولین مشکلش بزرگ بودنش هست و خب قطعاً زمانی که انواع جدیدی از کارمندا اضافه بشن این کد رشد پیدا خواهد کرد.
۲.خیلی واضحه که داره بیشتر از یک کارو انجام میده.
۳. اصل single responsibility رو نقض میکنه چون بیش از یک دلیل برای تغییر اون وجود داره.
۴. اصل Open Closed Principle رو زیر سؤال میبره چراکه هر وقت نوع جدیدی اضافه بشه باید تغییر کنه.
۵. و اما بدترین مشکلش اینه که ممکنه تعداد نامحدودی از این تابع با همین ساختار وجود داشته باشه.( البته میشه تاحدودی از این مشکل جلوگیری کرد، اما چطوری؟
راه حل این هست که بیاییم switch رو توی یک ABSTRACT FACTORY قرار بدیم و اجازه ندیم کسی اون رو ببینه. و بعد برای ایجاد شی از پلی مورفیزم استفاده کنیم تا قابل تحملتر بشه ( بنا رو گذاشتم رو اینکه شی گرایی رو بلدین پس توضیح اضافه نمیدم)
Listing 3-5
Employee and Factory
public abstract class Employee {
public abstract boolean isPayday();
public abstract Money calculatePay();
public abstract void deliverPay(Money pay);
}
……………………………………………………….
public interface EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
……………………………………………………….
public class EmployeeFactoryImpl implements EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
switch (r.type) {
case COMMISSIONED:
return new CommissionedEmployee(r) ;
case HOURLY:
return new HourlyEmployee(r);
case SALARIED:
return new SalariedEmploye(r);
default:
throw new InvalidEmployeeType(r.type);
}
}
}
البته باید بگم که بعضی وقتا شرایط خاصه و در صورتی که شرایط خاص باشه میتونین این قانون رو نقض کنین.
استفاده از اسمهایی که تابع رو توصیف کنه
اسمی انتخاب کنین که فانکشنی رو که نوشتین توصیف کنه، از اینکه این اسم طولانی بشه اصلاً نترسین، یه اسم طولانی که بتونه فانکشن رو توصیف کنه بهتر از یه اسم کوتاه و مبهمه. همونطور که توی فصل اسامی معنادار هم گفتم از اینکه وقت خودتون رو صرف انتخاب اسم کنین هراس نداشته باشین، اسمهای مختلفی رو امتحان کنین و همواره اونو تغییر بدین تا به بهترین مورد ممکن برسین.
آرگومانهای فانکشن
شاید باورش مشکل باشه اما باید بدونین که تعداد ایدهآل آرگومان برای یک فانکشن صفر هست(niladic) و بعدم یکی (monadic) و نهایتاً دوتا (dyadic) و تا میتونین باید از سه تا آرگومان (triadic) اجتناب کنین؛ و خب اگه بیش از سه تا شد باید یه دلیل خیلی محکم داشته باشین، اما به هرحال نباید ازشون استفاده کنین.
اما حالا اگه دیگه مجبور شدین و واقعاً دیگه هیچ راهی نبود و خواستین بیشتر از سه تا آرگومان به فانکشنتون پاس بدین میتونین ورودیهارو به شکل آبجکت پاس بدین یا اگه همشون از یک جنس هستن به صورت آرایه این کارو بکنین.
از نظر تست هم که دیگه مشخصه! نوشتن تستی که برای همهی حالتها و ترکیبهای مختلف آرگومانها درست کار کنه سخته. البته اگه آرگومانی نباشه این مسأله دیگه مهم نیست و به همین ترتیب اگه یک آرگومان داشته باشیم کار آسون تره، با دو آرگومان مسأله چالشی تره و خب بیشتر از این دیگه ترسناکه!
( اینو بگم که استفاده از دو آرگومان کار اشتباهی نیس و باید استفاده کنین ولی تا میشه باید از مکانیسمهای موجود استفاده کنین و اونو به تک آرگومانی تبدیل کنین)
نکتهی بعدی اینه که از آرگومانهای فلگ طور به شدت خودداری کنین، درواقع اگه این کارو انجام بدین دارین داد میزنین که آقایون خانوما من بیشتر از یک کارو دارم انجام میدم ( اگه true باشم یه کار و اگه false باشم یه کار دیگه)
پس همانا کار درست این است که برای هریک از کارهای خود یک تابع داشته باشید.(ببینین چند بار تاحالا اینو گفتما پس خدایی رعایت کنین … )
نکتهی آخرم که دیگه تکرار مکرراته : اسم درست حسابی واسه فانکشنتون استفاده کنین.
Have NO Side Effects!
این کار یه جور دورغ گفتنه؛ درواقع شما دارین میگین که تابع من یه کارو انجام میده اما واقعیت اینه که یه سری کاره دیگه رو هم داره قایمکی انجام میده. بزارین یه مثال بزنم :
لیست 6-3 رو یه نگاه بندازین ، ظاهر قضیه اینطوریه که خب مثه یه پارچه آقا داره کارشو انجام میده( درواقع یه الگوریتم استاندارد برای مچ کردن یک نامکاربری به یک پسورد هست که اگه این مچ شدن صورت گرفت true و در غیر این صورت هر اتفاق دیگهای که بیفته false برمیگردونه) اما یه ساید افکت یا عارضه جانبی داره، میتونین تشخیصش بدین؟
Listing 3-6
UserValidator.java
public class UserValidator
{
private Cryptographer cryptographer;
public boolean checkPassword(String userName, String password)
{
User user = UserGateway.findByName(userName);
if (user != User.NULL)
{
String codedPhrase = user.getPhraseEncodedByPassword();
String phrase = cryptographer.decrypt(codedPhrase, password);
if ("Valid Password".equals(phrase))
{
Session.initialize();
return true;
}
}
return false;
}
}
فکر کنم تشخیص دادنش آسون باشه، بله ساید افکت این فانکشن Session.initialize(); هست. این فانکشن همونطور که از اسمش هم پیداست (checkPassword)، پسورد رو چک میکنه اما اسمش نشون نمیده که session هم میسازه. پس هرموقع این تابع رو یه نفر صدا میزنه و فکرم میکنه که فانکشن داره مثل اسمش عمل میکنه، ریسک پاک شدن دیتای session موجود رو هم فراهم میکنه.
اگه بخواییم بیشتر توضیحش بدیم این ساید افکت یه temporal coupling میسازه. فانکشن checkPassword فقط زمانی که ساخت یک session در حالت ایمن هست میتونه فراخوانی بشه و اگه خارج از ترتیب فراخوانی بشه ممکنه دیتای session از بین بره؛ و خب میبینید که چقدر این کار میتونه گیجکننده باشه.
البته که نباید همچین کاری کنین اما اگه مجبور به داشتن temporal coupling شدین حداقل یه اسم خوب و واضح براش انتخاب کنین مثل checkPasswordAndInitializeSession که البته باز با این کار قانون ( انجام فقط یک کار توسط فانکشن ) رو نقض میکنین.
Command query sepration
فانکشن یا باید یه کاری رو انجام بده یا به یه چیزی جواب بده، اما نه هر دوی اینها. یا باید وضعیت یک آبجکت رو تغییر بده یا یک سری اطلاعات مربوط به اون آبجکت رو برگردونه؛ اگه هر دو کار رو انجام بده گیجتون میکنه. برای مثال این فانکشن رو در نظر بگیرین:
public boolean set(String attribute, String value);
این فانکشن میاد مقدار یه attribute مشخص رو تعیین میکنه بعد اگه همه چی اوکی بودtrue و اگه چنین attribute وجود نداشت false برمیگردونه و این کار باعث میشه یه statements عجیب غریب مثل این به وجود بیاد :
if (set("username", "unclebob"))...
حالا فکر کنین که یه بندهخدایی داره اینو میخونه، خب طرز فکر این برنامهنویس نگون بخت که داره کد شمارو میخونه چیه؟
۱. آیا یوزرنیم قبلاً عموباب ست شده؟
۲. آيا ست شدن عموباب به عنوان یوزرنیم موفقیت آمیز بوده؟
و سوالای دیگه ….
یکی از مشکلات اینه که اصلاً مشخص نیست کلمه set فعله یا صفت و دوباره همون مشکل انتخاب اسم و این داستان که تابع داره دوتا کارو انجام میده پیش میاد. چطوری مشکلو حل کنیم؟ اینطوری :
if (attributeExists("username")) {
setAttribute("username", "unclebob");
...
}
Exception هارو به برگردوندن کد خطا ترجیح بدین
به جای اینکه بیایین یه خطا یا ارور در یه سری شرطهای پی در پی که به واسطهی حالتای ناخوشایندی که در اون فانکشن میفته مشخص کنین، خیلی شیک از try/catch استفاده کنین.
مثال :
این تیکه کد رو ببینین که اومده خطاهارو return کرده.
if (deletePage(page) == E_OK) {
if (registry.deleteReference(page.name) == E_OK) {
if (configKeys.deleteKey(page.name.makeKey()) == E_OK) {
logger.log("page deleted");
}
else {
logger.log("configKey not deleted");
}
}
else {
logger.log("deleteReference from registry failed");
}
}
else {
logger.log("delete failed");
return E_ERROR;
}
و اما اینجا از exception ها استفاده میکنیم و میبیند که میشه پردازش خطا رو از مسیر کد جدا کرد و ساده سازی انجام داد.
try
{
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
catch (Exception e)
{
logger.log(e.getMessage());
}
و یه نکتهی مهم دیگه اینکه بلوکهای try/catch رو استخراج کنین و بعد فانکشنهاتون رو بنویسین ( یه جور زیبا سازیه دیگه …)
حالا چطوری؟ مثلاً اینطوری :
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
}
catch (Exception e) {
logError(e);
}
}
private void deletePageAndAllReferences(Page page) throws Exception {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
private void logError(Exception e) {
logger.log(e.getMessage());
}
میبینین که با جدا کردنشون چقدر فهم کد آسونتر شد.
Don’t repeat your self
اینو یه چند بار بلند بلند بخونین، یا نه اصلاً بنویسین قاب کنین بزارین جلوتون؛ نصف مشکلات ما زیر سر همین کاریه که انجام میدیم چون یادگرفتیم هر جا نیاز داشتیم فقط کپی پیست کنیم. خب از دلایل اینکه چرا اینکارو میکنیم میتونیم به نداشتن حوصله اشاره کنیم( چیزی که یکی از معیارهای مهم یک برنامه نویسه خوب هست).
البته که با این کار فقط داریم خوانایی کد رو کم میکنیم و تازه انرژی بیشتریم باید واسه تغییر دادنش و هزارتا مسئله دیگه بزاریم. پس بیایین عهد کنیم که دیگه هیچ وقت تو زندگیمون این کارو نکنیم :)
……..
نوشتن یک نرمافزار دقیقاً مثل نوشتن هر چیز دیگهای هست، مثلاً وقتی شروع میکنین به نوشتن یه مقاله اول اون فکر اولیه رو مینویسید بعد هی شروع میکنین به درست کردنش، مثلاً کلمات درستتری انتخاب میکنین، ساختار بهتر و ….
فانکشن نوشتنم دقیقاً همینطوریه، وقتی نوشتی و تموم شد و کار داد حالا بیا بشین اصلاحش کن و به مرور هی بهترو بهترش کن. نوشتن یه کد با کیفیت فقط با تجربه به دست میاد پس هیچ موقع، توقع نوشتن یه کد ایدهال توی دفعه اول رو از خودتون نداشته باشین.
نتیجهگیری
میدونین برنامهنویسای سنیور چطوری فکر میکنن؟ اونا به نرمافزار مثل یه داستان نگاه میکنن. درواقع از ابزار به بهترین شکل استفاده میکنن و همینطور، تابع یک سری قواعد و اصول هستن تا داستان بهتری رو شکل بدن. فانکشنها هم مثل افعال این داستان میمونن ؛ هر چه که فانکشنای کوتاهتر و خوش اسمتری داشته باشین کد خفنتری خواهید داشت.
کامنتها
آيا تاکنون درباره کامنتها چیزی شنیدهاید؟آیا از فواید و مضرات آن باخبرید؟ چه کامنتی خوب است و چه کامنتی بد؟ چرا وقتی کد هست کامنت میزاریم؟ ویژگیهای یک کامنت خوب چیست؟ اگررر میخواهید یکی از ۱۰ کامنت نویس موفق دنیا باشید ما را دنبال کنید. (صرفا جهت مزاح :)))
بزارین با یکی از جملات بزرگان شروع کنیم، میفرمایند که : کد بد رو کامنت نکنین دوباره بنویسید. در واقع ما از همین جا متوجه میشیم که کامنتها اونقدرام که ازش خوب میگن نیست؛ هیچ چیزی بدتر از یه کامنت قدیمی و ضعیف نمیتونه مارو گمراه کنه، بیایین اینطوری فکر کنیم که اگه زبونهای برنامهنویسی به اندازه کافی رسا بودن و ما ماهرانه برای بیان منظورمون ازشون استفاده میکردیم، خیلی به کامنتها نیاز نداشتیم. شاید هم اصلا نداشتیم!
درواقع ما برای جبران شکست در رسوندن مفهوم کد از کامنت استفاده میکنیم؛ دقت کنین که از واژه شکست استفاده کردم و منظورم اینه که کامنتها همیشه نشونهی شکست هستن. باید از اونها استفاده کنیم چون همیشه راهی به جز این برای توضیح دادن منظور خودمون پیدا نمیکنیم و استفاده ازشون دلیلی بر خوشحالی نیست.
عموباب میفرمایند که هر باری که تونستین منظور خودتون رو فقط با کد برسونین باید درست حسابی از خودتون تقدیر به عمل بیارین و اگه هر بار منظورتون رو توی کامنت توضیح دادین، هر چی از دهنتون درمیاد بار خودتون کنین و احساس شکست بفرمایید.
حالا چرا بزرگوار با کامنت مخالف هستن؟ چون کامنتها دروغ میگن:
Code never lies, comments sometimes do
البته نه همیشه و عمدا، ولی خیلی زیاد این کارو میکنن. هر چقدر کامنتها قدیمیتر باشن و از کدی که توصیفش میکنن دورتر باشن، احتمال اشتباه را بالا میبرن و دلیلش هم خیلی سادس چون برنامهنویس نمیتونه واقع بینانه از اونا استفاده کنه.
کامنتها برای کد بد ساخته نشدن
دوست عزیز اگه فکر میکنی خیلی زرنگی که یه کد بد بنویسی بعد بگی اوه خوبه کامنتش کنم و این داستانا سخت در اشتباهی. همین الان اون کامنتی که برای کد داغونت نوشتی رو پاک کن و وقتی که روی نوشتن کامنت میزاری رو صرف تمیز کردن کدت کن.
منظور خودت رو با کد برسون!
خب مطمئنا مواردی هست که کد وسیلهی قدرتمندی برای توصیف نیست ولی متاسفانه خیلی از برنامه نویسا اینطور برداشت کردن که کد به ندرت میتونه راه خوبی برای توصیف باشه که این کاملا اشتباهه، میگین نه؟ خب خودتون قضاوت کنین دوست دارین کدوم رو ببینید؟ این:
// Check to see if the employee is eligible for full benefits
if ((employee.flags & HOURLY_FLAG) &&
(employee.age > 65))
یا این؟
if (employee.isEligibleForFullBenefits()
فقط چند ثانیه زمان میبره که با کد بیشترین منظور رو منتقل کنین، و این به سادگیِ ایجاد یک فانکشن مثل مورد بالاست که دقیقا چیزی رو میگه که میخواستین با کامنت بگین.
خب حالا با تموم این حرفا شاید سوال بشه که پس آیا همهی کامنتها به درد نخورن؟ باید عرض کنم که خیر اینطوریام نیس، بیایین برای توضیح بیشتر این مسئله کامنتها رو به دو گروه کامنتهای خوب و بد تقسیمشون کنیم:
کامنتهای خوب
همونطور که گفتیم بعضی از کامنتها لازم و مفید هستن، در ادامه مواردی رو بررسی میکنیم که لیاقت بیتهایی که مصرف میکنن رو دارن ولی بازم توجه داشته باشین که کامنتی واقعا خوبه که هیچ راهی برای نوشتنش پیدا نکنیم.
کامنتهای قانونی
یکی از جاهایی که کامنت میتونه خوب عمل کنه برای کپی رایت و حق تالیف و یه سری از این چیزای منطقی و لازم هستن. که معمولا هم در ابتدای سورس کد قرار میگیرن. مثل این :
// Copyright (C) 2003,2004,2005 by Object Mentor, Inc. All rights reserved.
// Released under the terms of the GNU General Public License version 2 or later
کامنتهایی که حاوی اطلاعات مفید هستن
یکی دیگه از جاهایی که اگه کامنت بزارین جاتون وسط بهشته اینجاس! در واقع منظورم کامنت گذاشتن برای اطلاعات دادن یا سهولت در بدست آورن اطلاعات راجع به کد هست. مثلا اینو ببینید:
// format matched kk:mm:ss EEE, MMM dd, yyyy
Pattern timeMatcher = Pattern.compile(
"\\d*:\\d*:\\d* \\w*, \\w* \\d*, \\d*");
با نوشتن این کامنت داریم میگیم که این عبارت منظم برای مطابقت زمان و تاریخ در نظر گرفته شده که با SimpleDateFormat.format فرمت شده. حتی میتونین اگه یه جایی از کدتون یه کار خاص رو انجام میده با اوردن مثال توی کامنت، به برنامهنویس اطلاعات بدین.
شرح نیت
در این مکان هم کامنت گذاشتن کار بسیار پسندیدهای هست؛ جاییه که برنامهنویس میخواد هدف و نیت خودش رو از پیادهسازی اون کد بگه. مثلا به کد زیر دقت کنین، ممکنه با راهحل برنامهنویس موافق نباشین اما حداقلش اینه که میدونین میخواسته چی کار کنه.
public void testConcurrentAddWidgets() throws Exception {
WidgetBuilder widgetBuilder =
new WidgetBuilder(new Class[]{BoldWidget.class});
String text = "'''bold text'''";
ParentWidget parent =
new BoldWidget(new MockWidgetRoot(), "'''bold text'''");
AtomicBoolean failFlag = new AtomicBoolean();
failFlag.set(false);
//This is our best attempt to get a race condition
//by creating large number of threads.
for (int i = 0; i < 25000; i++) {
WidgetBuilderThread widgetBuilderThread =
new WidgetBuilderThread(widgetBuilder, text, parent, failFlag);
Thread thread = new Thread(widgetBuilderThread);
thread.start();
}
assertEquals(false, failFlag.get());
}
شفاف سازی و هشدار دادن درباره عواقب یک کار
بعضی موقعها باید بیاییم بگیم آقا اگه مثلا فلان جای کدو دست بزنی کل برنامه بهم میریزه و توضیح بدیم که چرا دستکاری در اون قسمت از کد خطرناکه؛ خلاصه شفاف سازی این مورد هم به وسیله کامنت بسیار شایسته است.
البته شفاف سازی و ترجمهی یک فانکشن مبهم هم مفیده، ولی همیشه باید راهی پیدا کنین که به خودی خود همه چی واضح و روشن باشه.
یه مثال از هشدار دادن به برنامهنویس (که ممکنه خودمون در آینده باشیم) بزنیم که بهتر جا بیفته:
// Don't run unless you
// have some time to kill.
public void _testWithReallyBigFile()
{
writeLinesToFile(10000000);
response.setBody(testFile);
response.readyToSend(this);
String responseString = output.toString();
assertSubString("Content-Length: 1000000000", responseString);
assertTrue(bytesSent > 1000000000);
}
حالا ممکنه بگین که روشهای بهتری هست و این حرفا ولی در نهایت استفاده از کامنت در این مواقع کاملا منطقیه.
TODO کامنتها
خب این موردم که کاملا واضحه و همگی این کارو بارها و بارها در طول پروژه انجام میدیم، درواقع کاری که میخواییم انجام بدیم رو بنا به دلایل مختلف عقب میندازیم و TODO کامنت میزاریم تا یادمون نره. این روزا IDE های مختلف امکانات مختلفی واسه یادآوری و گم نکردن این موارد ارائه میدن؛ ولی به هر جهت یادتون باشه که TODO هر چی که هست، نباید بهونهای واسه نوشتن کد کثیف توی سیستم باشه!
مثال:
//TODO-MdM these are not needed
// We expect this to go away when we do the checkout model
protected VersionInfo makeVersion() throws Exception
{
return null;
}
تقویت
ممکنه از کامنت برای بالا بردن اهمیت چیزی استفاده کنین که به نظر بقیه کم اهمیت میرسه :
String listItemContent = match.group(3).trim();
// the trim is real important. It removes the starting
// spaces that could cause the item to be recognized
// as another list.
new ListItemWidget(this, listItemContent, this.level + 1);
return buildList(text.substring(match.end()));
خب کامنتهای خوب رو گفتیم میریم که ببینیم داستان کامنتهای بد چیه؟
کامنتهای بد
عرضم به خدمتتون که بیشتر کامنتها توی این دسته قرار میگیرن،که معمولا توجیه و بهونه برای یه کد بد (داغون) هستن، شااایدم صحبت های برنامه نویس باخودش باشن.
کامنتهای اضافی
چه لزومی داره وقتی کد گویای همه چیز هست بیای دوباره تو کامنت بنویسی؟ اضافه کاری دوست داری؟ خیلی وقتا این جور کامنتها به جای اینکه مفید باشه فریب دهنده هست چون دقتشون به اندازه کد نیست و اطلاعات اشتباه منتقل میکنن؛ واسه ملموستر شدن قضیه یه نگا به این کامنتها بندازین :
Listing 4-2
ContainerBase.java (Tomcat)
public abstract class ContainerBase
implements Container, Lifecycle, Pipeline,
MBeanRegistration, Serializable {
/**
* The processor delay for this component.
*/
protected int backgroundProcessorDelay = -1;
/**
* The lifecycle event support for this component.
*/
protected LifecycleSupport lifecycle =
new LifecycleSupport(this);
/**
* The container event listeners for this Container.
*/
protected ArrayList listeners = new ArrayList();
/**
* The Loader implementation with which this Container is
* associated.
*/
protected Loader loader = null;
/**
* The Logger implementation with which this Container is
* associated.
*/
protected Log logger = null;
/**
* Associated logger name.
*/
protected String logName = null;
/**
* The Manager implementation with which this Container is
* associated.
*/
protected Manager manager = null;
/**
* The cluster with which this Container is associated.
*/
protected Cluster cluster = null;
/**
* The human-readable name of this Container.
*/
protected String name = null;
/**
* The parent Container to which this Container is a child.
*/
protected Container parent = null;
/**
* The parent class loader to be configured when we install a
* Loader.
*/
protected ClassLoader parentClassLoader = null;
/**
* The Pipeline object with which this Container is
* associated.
*/
protected Pipeline pipeline = new StandardPipeline(this);
/**
* The Realm with which this Container is associated.
*/
protected Realm realm = null;
/**
* The resources DirContext object with which this Container
* is associated.
*/
protected DirContext resources = null;
}
Mandated Comments
نکنه میخوای مثه جاواداک بیای واسه همه چی هی کامنت بزاری؟
/**
*
* @param title The title of the CD
* @param author The author of the CD
* @param tracks The number of tracks on the CD
* @param durationInMinutes The duration of the CD in minutes
*/
public void addCD(String title, String author,
int tracks, int durationInMinutes) {
CD cd = new CD();
cd.title = title;
cd.author = author;
cd.tracks = tracks;
cd.duration = duration;
cdList.add(cd);
}
کامنتهای ژورنالی
دیده شده بعضی از برنامهنویسام میان تغییراتی که ایجاد میکنن رو با تاریخ و ساعت کامنت میکنن، دفتر خاطراتت که نیست برادر/خواهر من، پس سورس کنترل رو برا چی ساختن؟
Changes (from 11-Oct-2001)
--------------------------
* 11-Oct-2001 : Re-organised the class and moved it to new package
* com.jrefinery.date (DG);
* 05-Nov-2001 : Added a getDescription() method, and eliminated NotableDate
* class (DG);
* 12-Nov-2001 : IBD requires setDescription() method, now that NotableDate
* class is gone (DG); Changed getPreviousDayOfWeek(),
* getFollowingDayOfWeek() and getNearestDayOfWeek() to correct
* bugs (DG);
* 05-Dec-2001 : Fixed bug in SpreadsheetDate class (DG);
* 29-May-2002 : Moved the month constants into a separate interface
* (MonthConstants) (DG);
* 27-Aug-2002 : Fixed bug in addMonths() method, thanks to N???levka Petr (DG);
* 03-Oct-2002 : Fixed errors reported by Checkstyle (DG);
* 13-Mar-2003 : Implemented Serializable (DG);
* 29-May-2003 : Fixed bug in addMonths method (DG);
* 04-Sep-2003 : Implemented Comparable. Updated the isInRange javadocs (DG);
* 05-Jan-2005 : Fixed bug in addYears() method (1096282) (DG);
کامنتهای نویز دار
این کامنتا فقط سرو صدا دارن، مثل اینا :
/**
* Default constructor.
*/
protected AnnualDateRule() {
}
یا این:
/** The day of the month. */
private int dayOfMonth;
و حتی این:
/**
* Returns the day of the month.
*
* @return the day of the month.
*/
public int getDayOfMonth() {
return dayOfMonth;
}
البته که چشم ما عادت کرده اینارو نادیدشون بگیره ولی اگه یه موقع وسوسه شدین ایجاد نویز کنین، بزنین کلهم اون تیکه از کدتونو پاک کنین و دوباره بنویسین و بعد میفهمین که این کار باعث میشه یه برنامهنویس شاد و خندون و خوشحال و ... باشین :))))
وقتی میتونین از یک فانکشن یا متغییر استفاده کنین از کامنت استفاده نکنین
این کد رو در نظر بگیرین :
// does the module from the global list <mod> depend on the
// subsystem we are part of?
if (smodule.getDependSubsystems().contains(subSysMod.getSubSystem()))
حالا این کد رو بدون این دو جلد تفسیر تصور کنین، بله یعنی این:
ArrayList moduleDependees = smodule.getDependSubsystems();
String ourSubSystem = subSysMod.getSubSystem();
if (moduleDependees.contains(ourSubSystem))
Position Markers
یه سری برنامهنویسام میان یه بنر نصب میکنن وسط کد و میرن که مثلا علامت گذاری کنن؛ البته خوبه که خیلی کم و به وقتش به کارشون ببرین؛ ولی راه به راه هی استفاده نکین.
// Actions //////////////////////////////////
کامنتهایی که نشون دهندهی بستن آکولاد هاست
گونهای از برنامهنویسان وجود دارند که کامنتهایی را در آخر آکولادهای بسته مینهند. جا داره به این دوستان بگم اگه خیلی زرنگ و باهوشی برو فانکشنهات رو کوتاهتر بنویس نمیخواد علامت گذاری کنی.
یک نمونه از این کامنتها :
public class wc {
public static void main(String[] args) {
BufferedReader in = new BufferedReader(new InputStreamReader(System.in));
String line;
int lineCount = 0;
int charCount = 0;
int wordCount = 0;
try {
while ((line = in.readLine()) != null) {
lineCount++;
charCount += line.length();
String words[] = line.split("\\W");
wordCount += words.length;
} //while
System.out.println("wordCount = " + wordCount);
System.out.println("lineCount = " + lineCount);
System.out.println("charCount = " + charCount);
} // try
catch (IOException e) {
System.err.println("Error:" + e.getMessage());
} //catch
} //main
}
Attributions and Bylines
/* Added by Fatemeh */
به این عزیزانی که میان بالای یه کد اسم خودشونو میزنن باید بگیم داداش زحمت نکش توروخدا گیت هست ما میفهمیم تو بودی که اینو نوشتی :))))))
کامنتکردن کد
یه موقعهاییم هست میاییم یه عالمه کدو کامنت میکنیم، که اکثرا هم موقع این کار به این فکر میکنیم که شاید بعدا نیاز بشه و خب اگه یه بنده خداییم بیاد پروژه رو ببینه میترسه به اینا دست بزنه که نکنه مشکلی پیش بیاد. نباید اینارو کامنت کنین تا یه گوشه واسه خودشون خاک بخورن؛ پاک کنین بره و بازم تاکید میکنم که در این موارد دوست خوبمون گیت یاری دهنده است.
Nonlocal Information
وقتی یه جایی کامنت میزاری درباره خود اون کد و دورو بریاش بزار، نه که برداری اطلاعات ۶ تا فایلو اون ور ترم بگی.
اطلاعات بیش از اندازه
کتاب تاریخ که نیست میای سرگذشت سلسلههای مختلفو با جزئیات مینویسی، کسی که میاد کد رو بخونه به این اطلاعات مسخره هیچ نیازی نداره.
مثال:
/*
RFC 2045 - Multipurpose Internet Mail Extensions (MIME)
Part One: Format of Internet Message Bodies
section 6.8. Base64 Content-Transfer-Encoding
The encoding process represents 24-bit groups of input bits as output
strings of 4 encoded characters. Proceeding from left to right, a
24-bit input group is formed by concatenating 3 8-bit input groups.
These 24 bits are then treated as 4 concatenated 6-bit groups, each
of which is translated into a single digit in the base64 alphabet.
When encoding a bit stream via the base64 encoding, the bit stream
must be presumed to be ordered with the most-significant-bit first.
That is, the first bit in the stream will be the high-order bit in
the first 8-bit byte, and the eighth bit will be the low-order bit in
the first 8-bit byte, and so on.
*/
فرمتبندی
درود به همهی رفقا امیدوارم خوشحال و خندون باشین. توی این فصل قراره بریم ببینیم فرمت یه کد تمیز باید به چه شکل باشه تا وقتی ملت میان کد مارو میخونن تحت تاثیر نظم و انسجام و توجه ما به جزئیات قرار بگیرن و بگن نه بابا! چه ادم حرفهای پشت این داستان بوده! نه اینکه بگن طرف سرمست و شاد بوده و نمیفهمیده چیکار میکنه.
هدف ما از فرمت بندی چیه؟
این کار خیلی خیلی مهمه و انجام دادنش واااجبه، برای برقراری ارتباط، باید کدتون فرمت بندی بشه و ارتباطات مهم ترین بخش برای برنامهنویسای حرفهایه؛ حالا اگه از یه سری قوانین ساده تبعیت کنین خیلی شیک میتونین کدتون رو فرمت بندی کنین؛ البته لازم به ذکره اگه توی یک تیم هستین و اونجا یه سری قوانین تعریف کردن باید از همونا پیروی کنین تا یه دست باشین؛ در همین راستا داشتن یک ابزار خودکار برای اعمال این فرمتها هم میتونه کمک کننده باشه.
فرمتبندی عمودی یا Vertical
نکتهی مهم در این بخش اینه که هر فایل نباید بیشتر از ۵۰۰ خط کد داشته باشه، این باید خیلی مورد توجه قرار بگیره چراکه درک فایلهای کوچیک معمولا خیلی اسونتر از درک فایلهای بزرگ هست.
The Newspaper Metaphor
خوندن کد از بالا به پایین مثل خوندن یه روزنامه میمونه، درواقع شما وقتی روزنامه میخونید انتظار دارید اول یه عنوان داشته باشه که بگه داستان از چه قراره، بعد مثلا پاراگراف اول یه خلاصه از کل داستانو بگه بدون جزئیات؛ و هر چی به سمت پایین میریم جزئیات کار بیشتر میشه و تا جایی پیش میریم که از ریز ماجرا باخبر میشیم.
یک فایل حاوی کد هم باید به همین صورت باشه، در ابتدا نام باید ساده اما توضیحی باشه و خودکار به ما بگه آيا الان توی فایل یا ماژول درستی هستیم یا نه. بالاترین قسمت فایل باید مفاهیم و الگوریتمهای سطح بالایی رو نشون بده و همینطور که به سمت پایین میریم جزئیات کار زیاد بشه تا جایی که از جزئیات سطح پایین فایل هم باخبر بشیم.
فاصله عمودی بین مفاهیم
بین هر مفهومی که توی کد ما وجود داره باید فاصله بزاریم چون هر چند خط از کد ما نشون دهندهی یک تفکر کامل هست. این مورد از نظر بصری اهمیت بالایی داره،میگید نه؟ خب خودتون قضاوت کنین با کدوم راحت ترین؟
Listing 5-1
BoldWidget.java
package fitnesse.wikitext.widgets;
import java.util.regex.*;
public class BoldWidget extends ParentWidget {
public static final String REGEXP = "'''.+?'''";
private static final Pattern pattern = Pattern.compile("'''(.+?)'''",
Pattern.MULTILINE + Pattern.DOTALL
);
public BoldWidget(ParentWidget parent, String text) throws Exception {
super(parent);
Matcher match = pattern.matcher(text);
match.find();
addChildWidgets(match.group(1));
}
public String render() throws Exception {
StringBuffer html = new StringBuffer("<b>");
html.append(childHtml()).append("</b>");
return html.toString();
}
}
یا این یکی ؟
Listing 5-2
BoldWidget.java
package fitnesse.wikitext.widgets;
import java.util.regex.*;
public class BoldWidget extends ParentWidget {
public static final String REGEXP = "'''.+?'''";
private static final Pattern pattern = Pattern.compile("'''(.+?)'''",
Pattern.MULTILINE + Pattern.DOTALL);
public BoldWidget(ParentWidget parent, String text) throws Exception {
super(parent);
Matcher match = pattern.matcher(text);
match.find();
addChildWidgets(match.group(1));
}
public String render() throws Exception {
StringBuffer html = new StringBuffer("<b>");
html.append(childHtml()).append("</b>");
return html.toString();
}
}
تراکم عمودی
به این نمونهای که در ادامه براتون میزارم دقت کنین، اگه قراره فاصله بین هر چند خط، مفاهیم مختلف رو از هم جدا کنه، پس تراکم بین چیزایی که به هم مرتبط هستن هم مهمه. مثلا اگه یه متغییر مربوط به فانکشنه نرین ۵ خط بالاتر تعریفش کنین همون نزدیک این کارو انجام بدین( البته این شامل تعریف متغییرهای سراسری که اول کار تعریف میکنین نمیشه).
بریم سراغ مثال :
به این کد دقت کنین.
Listing 5-3
/**
* The class name of the reporter listener
*/
private String m_className;
/**
* The properties of the reporter listener
*/
private List<Property> m_properties = new ArrayList<Property>();
public void addProperty(Property property) {
m_properties.add(property);
}
}
حالا به این یکی نگاه کنین که چقدر راحت تره و همه چی رو توی یک نگاه متوجه میشین.
Listing 5-4
public class ReporterConfig {
private String m_className;
private List<Property> m_properties = new ArrayList<Property>();
public void addProperty(Property property) {
m_properties.add(property);
}
}
فاصله عمودی
اقاجون مفاهیمی که باهم ارتباط تنگاتنگ دارن رو باید نزدیک به هم نگه دارین تا نخوایین کل فایل رو مدام بالا پایین کنین. البته که این قانون برای مفاهیمی که توی فایلهای جداگونه هستن صدق نمیکنه ولی مفاهیم مرتبطی که توی یک فایل هستن باید نزدیک هم باشن مگه اینکه دلیل خیلی خوبی برای جداییشون داشته باشین.
یه مثال خوشگل و کوچیک :
public int countTestCases() {
int count= 0;
for (Test each : tests)
count += each.countTestCases();
return count;
}
ترتیب عمودی
باید تابعی که فراخوانی میشه زیر تابعی باشه که فراخوانی رو انجام میده این یه جریان قشنگِ سطح بالا به سطح پایین رو شکل میده.
فرمتبندی افقی
فاصله افقی هم باید منطقی باشه و ترازبندی درستی داشته باشه. ( که الان IDE ها این کارو برامون انجام میدن)
Indentation یا تورفتگی
اگه این مورد رو رعایت نمیکنین خواهشا برنامهنویسی رو ببوسین بزارین کنار شما به درد این کارا نمیخورین؛ یه Indentation دیگه برادر من رعایت کن تا نفرینت نکنن.
قوانین تیم
درسته که هر کسی استایل کدنویسی خودش رو داره ولی وقتی توی یک تیم قرار میگیرین باید طبق قانونهای تیم عمل کنین تا همه یک دست پیش برین.
قوانین فرمتبندی عموباب
در آخرم این کد رو به عنوان نمونه ببینین که یه مثال استاندارد هست و قوانینیه که خود عموباب ازشون استفاده میکنه:
Listing 5-6
CodeAnalyzer.java
public class CodeAnalyzer implements JavaFileAnalysis {
private int lineCount;
private int maxLineWidth;
private int widestLineNumber;
private LineWidthHistogram lineWidthHistogram;
private int totalChars;
public CodeAnalyzer() {
lineWidthHistogram = new LineWidthHistogram();
}
public static List<File> findJavaFiles(File parentDirectory) {
List<File> files = new ArrayList<File>();
findJavaFiles(parentDirectory, files);
return files;
}
private static void findJavaFiles(File parentDirectory, List<File> files) {
for (File file : parentDirectory.listFiles()) {
if (file.getName().endsWith(".java"))
files.add(file);
else if (file.isDirectory())
findJavaFiles(file, files);
}
}
public void analyzeFile(File javaFile) throws Exception {
BufferedReader br = new BufferedReader(new FileReader(javaFile));
String line;
while ((line = br.readLine()) != null)
measureLine(line);
}
private void measureLine(String line) {
lineCount++;
int lineSize = line.length();
totalChars += lineSize;
lineWidthHistogram.addLine(lineSize, lineCount);
recordWidestLine(lineSize);
}
private void recordWidestLine(int lineSize) {
if (lineSize > maxLineWidth) {
maxLineWidth = lineSize;
widestLineNumber = lineCount;
}
}
public int getLineCount() {
return lineCount;
}
public int getMaxLineWidth() {
return maxLineWidth;
}
public int getWidestLineNumber() {
return widestLineNumber;
}
public LineWidthHistogram getLineWidthHistogram() {
return lineWidthHistogram;
}
public double getMeanLineWidth() {
return (double)totalChars/lineCount;
}
public int getMedianLineWidth() {
Integer[] sortedWidths = getSortedWidths();
int cumulativeLineCount = 0;
for (int width : sortedWidths) {
cumulativeLineCount += lineCountForWidth(width);
if (cumulativeLineCount > lineCount/2)
return width;
}
throw new Error("Cannot get here");
}
private int lineCountForWidth(int width) {
return lineWidthHistogram.getLinesforWidth(width).size();
}
private Integer[] getSortedWidths() {
Set<Integer> widths = lineWidthHistogram.getWidths();
Integer[] sortedWidths = (widths.toArray(new Integer[0]));
Arrays.sort(sortedWidths);
return sortedWidths;
}
}
خب این فصل خیلی کوتاه بود و تموم شد،امیدوارم این موارد رو رعایت کنین و خوندن این مقاله هم براتون مفید بوده باشه. اگه هر نظر یا انتقادی دارین در بخش نظرات با ما درمیون بزارین و در نهایت از وقتی که برای مطالعه گذاشتید ممنونم.
دیدگاه و پرسش
در حال دریافت نظرات از سرور، لطفا منتظر بمانید
در حال دریافت نظرات از سرور، لطفا منتظر بمانید