خلاصه کتاب کد تمیز – (فصل سوم : فانکشن‌ها - قسمت اول)

 خلاصه کتاب کد تمیز – (فصل سوم : فانکشن‌ها - قسمت اول)
03 فروردین 1400, خواندن در 10 دقیقه

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

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

فصل سوم : فانکشن‌ها

اون اوایل برنامه‌نویسی، ما سیستم‌های خودمون رو با 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);
                }
        }
}

البته باید بگم که بعضی وقتا شرایط خاصه و در صورتی که شرایط خاص باشه می‌تونین این قانون رو نقض کنین.

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

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

چه امتیازی به این مقاله می دید؟
خیلی بد
بد
متوسط
خوب
عالی

دیدگاه‌ها و پرسش‌ها

برای ارسال دیدگاه لازم است، ابتدا وارد سایت شوید.

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

در حال دریافت نظرات از سرور، لطفا منتظر بمانید

آفلاین
user-avatar
فاطمه شیرزادفر @Fatemeh.shirzadfar
تجربه کلمه‌ای هست که همه برای توصیف اشتباهاتشون ازش استفاده میکنن، و من همیشه دنبال اشتباهات جدیدم! برنامه‌نویس هستم و لینوکس‌ دوست
دنبال کردن

گفتگو‌ برنامه نویسان

بخشی برای حل مشکلات برنامه‌نویسی و مباحث پیرامون آن وارد شو