رفتن به نوشته‌ها

Clean Code – قسمت سوم:‌ متدها – ابزارهای تک کاره

Last updated on جولای 20, 2021

اینبار سعی کردم مطلب رو طوری بنویسم که با یکبار مرورش بتونید مطالب رو درک کنید و نیاز به خوندن طولانی مدت نباشه. 


سایز مهمه!

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

در واقع مثل همیشه بخش اول رو اگر متوجه بشید و دقیق انجام بدید، بقیه قوانین خود به خود اعمال میشن. نکته این فصل همینه : هیچ فانکشنی نباید از ۲۰ خط بیشتر باشه.

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

بلاک‌های داخل یک فانکشن

فانکشنی که تعداد خط‌هاش زیاد نباشه در نتیجه فرصت کار اضافی هم نداره. نتیجه این میشه که در هر فانکشن تعداد بلاک‌هایی که ایجاد میشه (if, else, while, for … ) به حداقل میرسه. فانکنشی بهتر هست که نهایت تو رفتگی (indent) کدش ۱ یا ۲ سطح باشه. در نتیجه فانکشن خیلی راحت تر خونده میشه و به راحتی فهمیده هم میشه. وقتی چند سطح تو رفتگی و بلاک کد برای تصمیم گیری وجود داره کی میتونه کد رو به راحتی تحلیل کنه؟

یک وظیفه (کار) رو درست انجام بده

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

برای اینکه بتونیم تشخیص بدیم این اصل درست انجام شده یا نه، نباید بتونیم فانکشن جدیدی از دل کد ایجاد شده دربیاریم (که تکراری نباشه).

هم سطح بودن دستورات یک فانکشن

در صورتی که بخواهیم قاعده فقط یک وظیفه رو به درستی انجام بدیم، باید بدونیم که مثلا صدا زدن یک متد با نام ()getHtml و بعد استفاده از این خط :‌ (“/)print باعث نقص قانون می‌شود. چرا که دریافت محتوای صفحه به صورت getHtml در این پروسه بسیار سطح بالا و دستور چاپ به لحاظ اجرایی در سطح پایینی از abstraction قرار خواهد گرفت. در واقع بهتر هست که تمام کدهای داخل یک فانکشن وظایفی شبیه و یا در سطح یک دیگر داشته باشند.

خواندن روزنامه

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

کد شما باید روایت‌گر داستان سیستمتون باشه. باید بتونید براساس فانکشن‌ها lifecycle یک ورودی رو تعیین کنید و براساس اون خروجی‌ای که انتظار میره تولید بشه رو پیش‌بینی کنید.

استفاده از Switch

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

برای اینکه بهتر متوجه بشیم اول کد زیر رو ببینید:


        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); }
            }
        

این کد درسته که بسیار کوتاه به نظر میرسه اما تله بسیار خوبی برای برنامه‌نویس‌ها هم هست. در واقع Type هایی که وارد Switch میشن، میتونه بیشتر و بیشتر بشه و هم چنین براین اساس تعداد خروجی‌ها هم بیشتر بشه. این در قسمت اول مخالف داشتن یک وظیفه است. هم چنین SRP و OCP رو که قوانین دیگری در نوشتن کد هستند رو نقض خواهد کرد. (این دو قانون در Scope این کتاب نیستند).  حالا چطوری میتونیم این کد رو بهتر بکنیم؟

سه کلاس متفاوت خواهیم نوشت و بعد این بلاک Switch رو در داخل یک متد کوچکتر دفن میکنیم. درسته که همچنان Switch داریم اما فقط و فقط یک وظیفه داره اون هم ایجاد یک Instance از آبجکت Employee هست (Factory Design Pattern). اینجوری هر بار براساس یک شرایط در بلاک Switch کار متفاوتی انجام نمیشه و هر برنامه‌نویسی هم دیگه امکان اضافه کردن یک وظیفه جدید رو نخواهد داشت.


        public abstract class Employee {
            public abstract public abstract public abstract
            boolean isPayday();
            Money calculatePay();
            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); }
            } }
        

استفاده از اسم‌های توصیف کننده

قسمت قبل در مورد اینکه چقدر نامگذاری مهم هست براتون نوشتم. همون حساسیت در مورد نامگذاری فانکشن‌ها هم قطعا صادق هست.

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

باید از فعل در ابتدای اسم‌ها استفاده کنید اما چیزی که باید به خاطر داشته باشید این هست که ارتباط معنایی این نامگذاری‌ها هم بسیار مهمه. در واقع سعی کنید اونها رو به هم مرتبط کنید تا توسعه‌دهنده‌های دیگه بتونن ارتباط منطقی بین اونها رو راحت‌تر بفهمند.

آرگومان‌های فانکشن

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

    • اگر با یک ورودی کار میکنید بهتر هست که ورودی و اسم متد همخوانی داشته باشن و معنای درستی رو القا کنن. مثلا InputStram fileOpen(“MyFile”) به صورت کاملا مشخصی وظیفه باز کردن یک فایل با نام مشخص و بازگشت دادن محتویات رو داره.
    • اگر دارید در مورد Event ها کد میزنید میتونید به شکل void passwordAttemptFailedNtimes(int attempts) کد رو بنویسید. دقت کنید که نامگذاری و استفاده از فانکشن باید نشون دهنده این موضوع باشه که این فانکشن در زمان یک Event صدا زده میشه.
    • از آرگومان‌های خروجی (output arguments) خودداری کنید. void includeSetupPageInto(StringBuffer pageText) نشون میده که محتویات صفحه به صورت ورودی به فانکشن ارائه میشه اما همین ورودی بعد از پایان فانکشن به صورت خروجی در نظر گرفته میشه. اگر تغییری بر روی ورودی داده میشه، انتظار میره که فانکشن خروجی مشخصی هم داشته باشه. استفاده از خروجی توسط ورودی تغییر کرده روش درستی برای کار کردن با فانکشن نیست.
    • از ورودی‌های Boolean برای تصمیم‌گیری در داخل بدنه فانکشن پرهیز کنید. این به این معناست که یک فانکشن در حال انجام بیشتر از یک کار هست. در نتیجه بهترین روش جدا کردن اون فانکشن و تبدیلش به دو فانکشن جداگانه خواهد بود.
    • داشتن بیشتر از یک ورودی در فانکشن‌ها اصولا باعث کثیفی کد و نامفهوم بودن کار خواهد شد. اگر مجبور هستید که همچین فانکشن‌هایی داشته باشید، فانکشن و ورودی‌ها رو شفاف کنید. اینکار رو میتونید با نامگذاری بهتر در نام فانکشن و هم در نام ورودی‌ها انجام بدید. هم چنین میتونید در صورت امکان برای هر کدوم از ورودی‌های مرتبط به هم یک کلاس جدید تشکیل بدید و به جای valueها یک Object از یک کلاس رو به عنوان ورودی در نظر بگیرید.

        Circle makeCircle(double x, double y, double radius);
        Circle makeCircle(Point center, double radius);
        
  • در نامگذاری‌های فانکشن دقت کنید که میتونید به ورودی‌ها هم اشاره‌ای داشته باشید مثلا به جای write(name) میتونید از writeField(name) استفاده کنید. اینطوری همه متوجه خواهند شد که دقیقا ورودی شما چه چیزی باید باشه. به عنوان مثال در writeField ورودی فانکشن یک Field از یک کلاس هست.

پنهان کاری نکنید

در هنگامی که فانکشن یا متد خودتون رو مینویسید اگر دایما به این موضوع که فقط و فقط باید یک وظیفه داشته باشه فکر کنید، ممکن نیست که کار دومی رو انجام بدید. در صورتی که این کار رو انجام بدید، باعث پنهان کاری و یا اصطلاحا side effect (تاثیرات جانبی) میشید. این موضوع قبل از هرچیز قانون تنها یک وظیفه رو نقض میکنه ولی از اون مهمتر باعث عدم فهم فانکشن و کد از طرف دیگران میشه. سوالی که ممکنه پیش بیاد اینه که اگر مثلا فانکنشی وظیفه احراز هویت کاربران رو داره چرا باید session رو هم برای کاربر ایجاد کنه؟ به کد زیر دقت کنید. اگر اسم فانکشن رو تغییر بدیم قطعا یک بخشی از مشکل حل خواهد شد (مثلا checkPasswordAndInitializeSession) اما توجه کنید که قانون تنها یک وظیفه دوباره نقض خواهد شد. در نتیجه این قسمت از کار باید از این فانکشن خارج و به جای دیگه انتقال داده بشه.


        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;
            }
        }
        

Exception Handling

کتاب بخشی در مورد این قسمت برای متدها و فانکشن‌ها داره اما با توجه به اینکه تصمیم دارم این بخش رو بعدا به صورت جداگانه پوشش بدم، توضیحش نمیدم.

حرف آخر

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

 

عکس از ‌:

Barn Images

منتشر شده در clean codeرشته بلاگ

یک دیدگاه

دیدگاه‌ها غیرفعال هستند.

Translate »