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

Clean Code: قسمت دوم – نامگذاری، اسم بچه‌های خانواده

Last updated on آوریل 29, 2020

مقدمه

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

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

اسم متغیر باید هدف اون رو برسونه

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


int d; // elapsed time in days

این کد به طور کامل کار خواهد کرد و شکی در اون نیست. اما آیا اگر نفر دومی هم این کد رو بخونه حتی با وجود داشتن یک خط کامنت، میتونه منظور شما رو بفهمه؟ میتونه استفاده درست این متغیر رو متوجه بشه؟ ممکن نیست که استفاده نابجا از این مقدار بکنه؟ حالا به کدهای زیر نگاه کنید:


int elapsedTimeInDays;
int daysSinceCreation;
int daysSinceModification;
int fileAgeInDays;

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

حالا بیاید یه نمونه کد دیگه با هم ببینیم:


public List<int[]> getThem(){
    List<int[]> list1 = new ArrayList<int[]>();
    for(int[] x : theList){
        if(x[0]==4)
            list1.add(x);
        return list1;
    }
}

کد بالا رو ببینید. این کد هم کار میکنه. اما اگر اولین بار این متد رو وسط یک کلاس ببینم باید این سوال ها رو بپرسم:

  1. اسم getThem یعنی چی؟ تو این متد چه کاری میخوایم دقیقا انجام بدیم؟
  2. لیستی که تعریف شده دقیقا چه محتوایی داره؟ list1 دقیقا میخواد چی رو برسونه؟ 
  3. چه چیزی تو خونه [0] آرایه‌ها ذخیره شده که امکان تصمیم گیری به ما میده؟

حالا کد اول رو با کد زیر جایگزین می کنیم:


public List<int[]> getFlaggedCells(){
    List<int[]> flaggedCells = new ArrayList<>();
    for(int[] cell: gameBoard){
        if(cell[STATUS_VALUE] == FLAGGED)
            flaggedCells.add(cell);
        return flaggedCells;
    }
}

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


public List getFlaggedCells(){
    List flaggedCells = new ArrayList<>();
    for(Cell cell: gameBoard){
        if(cell.isFlagged())
            flaggedCells.add(cell);
        return flaggedCells;
    }
}

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

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

این کار شاید همیشه آسون هم نباشه، پس بهتره یک سری قوانین تعیین بشن. در ادامه با اون‌ها آشنا میشیم. 

پرهیز از ارسال اطلاعات نادرست

تقریبا همه برنامه‌نویس‌ها در کدهای ساده خودشون از اسم‌هایی مثل sso, hp, ps یا اسم‌هایی شبیه این انتخاب کردن. در یک پروژه شخصی شاید شما تا ابد به مشکلی نخورید اما تصور کنید در یک محصول، یک برنامه‌نویس نام‌هایی این چنینی رو انتخاب کرده. در صورت refactor توسط یک برنامه نویس دیگه پروژه ممکنه به سادگی از کار بیفته و یا دچار مشکل بشه.

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

بعضی‌ها تصور میکنند که تمام اطلاعات باید در اسم متد آورده بشه. مثلا اسم پروژه و بعد توضیح اینکه چی کار میکنه و بعد تازه به خروجی هم میرسه. مثلا XYZContollerForEfficientHanlingOfStrings  و XYZContolleForEfficientManagingOfStore رو مقایسه کنیم. به وضوح مشخصه که نمیتونیم در مدت کوتاهی تفاوت‌ها رو حس کنیم. هرچند که امروز اکثر افراد از یک IDE یا editor باهوش استفاده میکنن اما باز هم استفاده از این شکل نامگذاری ایده خوبی نیست. 

تعریف نام‌ها با هدف مشخص

تمام اسم‌هایی که تعریف میشن باید هدف استفاده از اون متغیر یا متد یا کلاس رو مشخص کنند. مثلا به کد زیر دقت کنید:


public static void copyChars(String[] a1, String[] a2){
//omitted code
}

فرض کنید بدنه این کد قراره که یکی از این دو رشته رو در دیگری کپی کنه، کدوم یکی source و کدوم یکی destination هست؟ اصلا a1 و a2 چه مفهومی رو القا میکنه؟ در نتیجه بهتر هست که نام‌ها هدف رو هم نشون بدن. 

نامگذاری‌ها قابل تلفظ باشند

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

در طول مکالمات روزمره خودتون با هم‌تیمی و همکاراتون نیاز دارید تا اسم متغیرها و موجودیت‌های مختلف رو به کار ببرید. حالا فرض کنید که یک اسم عجیب و غریب در قسمت مهم کد وجود داره و شما هربار یک کلمه بی معنی و غیر قابل تلفظ رو وسط یک بحث منطقی وارد میکنید!‌ نتیجه شاید تغییری نکنه اما قطعا در روند کار تاثیر خواهد داشت. پس بهتره همیشه از اسم‌هایی با تلفظ راحت و قابل ادا شدن استفاده کنید. یادتون باشه که بعضی از متغیرها قرار هست تا ۱۰ سال در یک پروژه حضور داشته باشن.

نام‌های قابل جست و جو انتخاب کنید

خیل از اوقات وقتی یک سورس از برنامه‌نویس دیگه‌ای به دستم میرسه و برای اینکه بتونم کاربردهای یک موجودیت رو در سورس بررسی کنم قطعا اسمش رو جست و جو خواهم میکنم. اما در نظر بگیرید که مثل مثال اولی که بررسی کردیم حرف d به عنوان نام استفاده شده باشه! اگر به دنبال این حرف الفبا در سورس بگردیم حداقل ۹۰ درصد نتایج اصلا ربطی به موضوع مورد جست و جو نداره. در نتیجه نامگذاری تک حرفی رو محدود به یک بلاک ساده کد کنید در غیر اینصورت دردسر بزرگی برای خودتون و دیگران خواهید ساخت (مثل i در بلوک for).

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

عدم استفاده از Hungarian Notation

در برخی از زبان‌ها که ممکن است Typed نباشند و تایپ متغیرها چندان مهم نباشن (مثل جاوا اسکریپت) برخی از برنامه‌نویس‌ها از روشی به نام Hungarian Notation استفاده میکنند که به عنوان مثال برای یک متغیر از نام زیر استفاده میشه. 


PhoneNumber phoneString;

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

عدم استفاده از پیشوندهای ثابت

خیلی قبلتر ممکن بود که کدی به شکل زیر ببینید. 


public class Part{
    private String m_dsc;
}

اما امروز در دنیایی زندگی میکنیم که ابزارها به شدت قوی هستن. دیگه نیازی نیست که در لحظه به برنامه‌نویس‌ها یادآوری بشه که متغیری که در حال استفاده هستند مثلا member ای از یک class هست. 


public class Part{
    private String description;
}

 

از نگاشت ذهنی خودداری کنید

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

Interface ها

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

نام کلاس‌ها

 کلاس باید به صورت اسم (noun) ایجاد بشن. استفاده از فعل در نام کلاس غلط هست. هم چنین کلماتی که ممکنه مشکلاتی ایجاد کنند رو استفاده نکنید مثل (Manager یا Data یا Info) 

 

نام متد

نام متد حتما باید با فعل شروع بشه و دقیقا مشخص کنه که برای چه کاری ساخته شده. متدهایی که برای encapsulation استفاده می شوند باید با قوانین get / set / is  همخوانی داشته باشن. هم چنین اگر متدهای constructor به صورت overload استفاده شدن بهتره که به جای استفاده از این متدها، از static factory متدها استفاده بشه. برای این روش هم میتونید از constructorهای private استفاده کنید. در نتیجه کد شما به این شکل تغییر خواهد کرد:


Complex faulcrumPoint = new Complex(23.0) //public constructor
Complex faulcrumPoint = Complex.fromRealNumber(23.0) //private constructor

بامزه نباشید!

یادتون باشه که هر اسمی که انتخاب میکنید تاثیر خودش رو میذاره. در نتیجه اسم‌هایی که با شوخی همراه هستند رو انتخاب نکنید. اگر به عنوان مثال برای exception ها اسم handGrenade یا bomb میذارید فقط زحمت دیگران رو برای توسعه بیشتر میکنید و هیچ قسمت fun ای تولید نکردید. کد جای نمک ریختن نیست!

با توجه به Conecpt نامگذاری کنید

اگر در حال طراحی ساختار کد خود هستید باید به این نکته دقت کنید که ممکنه در برخی از کلاس‌ها متدهایی با کارکرد تکراری داشته باشید. مثلا متدی با نام save برای ذخیره در پایگاه داده، ممکنه در تمامی کلاس‌های dao شما تکرار بشه. حالا اگر یک برنامه‌نویس دیگه همچین اسمی رو برای متدی با کارکرد متفاوت (مثلا ذخیره در فایل) بکار ببره بعد از مدتی دیگه هیچ کدوم از اعضای تیم مطمئن نیستند که save دقیقا چه کاری انجام خواهد داد. پس در هر موضوعی با یک نام ثابت پیشروی کنید.شاید این موضوع بدیهی باشه اما در پروژه‌های شخصی خودم جاهایی بوده که این اشتباه رو انجام دادم و در بعضی از کلاس‌ها از find به عنوان پیشوند متد استفاده کردم ولی جای دیگه از get برای همون کارکرد استفاده کردم. طولی نکشیده که اشتباهم دردسر بزرگی برام ایجاد کرده. 

استفاده از نام‌‌های مرتبط با بیزینس پروژه

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


Long sarjameMaliSal;
String shomareShenasName;
String shomareId;

برای هر قسمت یک context ایجاد کنید

اگر در حال نوشتن یک بلاک کد هستید بهتر هست در نامگذاری‌ها ارتباط بین اجزا به لحاظ معنایی حفظ بشه. مثلا اگر در مورد کلاس Person صحبت میکنیم، ارتباط معنایی بین name, family, zipcode بسیار مشخصه اما اگر فقط یک کلمه state به این جمع اضافه بشه، دیگه معلوم نیست که داریم در مورد چه چیزی حرف میزنیم. state میتونه وضعیت یک موجودیت رو نشون بده. 

برای اینکه بهتر این موضوع رو درک کنیم یک کد بد رو ببینیم. 


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

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


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

       

فکر میکنم دیگه واضح هست که چه اتفاقی افتاده. حالا هر کسی این کد رو بخونه متوجه خواهد شد که دقیقا داره چه اتفاقی می افته و این متغیرها برای چی تعریف شدند. 

 

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

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

یک دیدگاه

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

Translate »