Skip to content

Latest commit

 

History

History
305 lines (206 loc) · 20.1 KB

File metadata and controls

305 lines (206 loc) · 20.1 KB

فصل سیزدهم نوشتن کد کمتر

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

کلید طلایی: کدی با حداکثر خوانایی، اصلا کد نیست.

برای پیاده سازی برخی ویژگی‌ها خود را به زحمت نیندازید، به آن نیازی ندارید

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

از سوی دیگر برنامه‌نویسان مایل‌اند مقدار تلاشی که برای پیاده سازی یک ویژگی نیاز است را دست کم بگیرند. آن‌ها در خوش‌بینانه‌ترین حالت می‌توانند تخمین بزنند که چه مدت طول می‌کشد تا یک نمونه اولیه1 خام را پیاده سازی کنند، اما بی شک مدت زمان اضافه‌ای که درگیر نگه‌داری2 برنامه در آینده و مستندسازی3 و افزایش حجم کدپایه را فراموش می‌کنند.

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

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

مثال: فروشگاه یاب

فرض کنید که در حال نوشتن یک برنامه فروشگاه یاب برای یک کسب و کار هستید. در ابتدا شما فکر می‌کنید که برای پیاده سازی این برنامه باید هر طول5 و عرض6 جغرافیایی داده شده کاربر، نزدیک‌ترین فروشگاه به آن را پیدا کرد. بنابراین برای انجام چنین کاری به صحیح‌ترین شکل ممکن، باید موارد زیر را مدیریت کنید:

  • وقتی که مکان‌ها7 در هر دو طرف خط بین‌المللی زمان8 قرار دارند.
  • وقتی که مکان‌ها نزدیک قطب شمال یا قطب جنوب قرار دارند.
  • برای تنظیم انحنا یا خمیدگی کره زمین، درجه‌های طولی به ازاء هر مایل، تغییر می‌کنند.

مدیریت همه این موارد، نیازمند مقداری دقت و چشم پوشی از برخی نقاط مرزی دارد. حال اگر برای اپلیکیشن شما، تنها ۳۰ فروشگاه در ایالت Texas وجود داشته باشد. سه مشکل بیان شده در لیست بالا برای این منطقه کوچک، مهم نیستند. بنابراین می‌توانید نیازمندی‌های خود را به صورت زیر کاهش دهید:

  • برای یک کاربر نزدیک Texas، نزدیکترین فروشگاه در Texas را پیدا کن.

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

مثال: افزودن حافظه نهان یا Cache

زمانی یک برنامه Java داشتیم که به شکل متناوب شئ‌ها را از روی دیسک می‌خواند و سرعت برنامه محدود به سرعت خواندن دیسک شده بود، بنابراین ما می‌خواستیم یک نوع مرتب‌سازی سیستم حافظه موقت(Cache) را پیاده سازی کنیم. یک دنباله معمولی از خواندن‌ها شبیه زیر بود:


read Object A
read Object A
read Object A
read Object B
read Object B
read Object C
read Object D
read Object D

همان گونه که می‌توانید ببینید، دسترسی‌های مکرر به یک شئِ مشابه انجام شده است، بنابراین cache کردن اطلاعات، قطعا به ما کمک می‌کرد.

زمانی که با این مشکل روبرو شدیم، اولین حس غریزی ما این بود که از یک cache‌ استفاده کنیم که آیتم‌های کمتر استفاده شده‌ی اخیر را رها1 کند. هر چند در کتابخانه خود چنین سیستمی را نداشتیم و مجبور بودیم خودمان آن را پیاده سازی کنیم. ولی با مشکلی چندانی روبرو نبودیم، چرا که قبلا چنین ساختارداده‌ای2 را که شامل دو جدول هش3 و یک لیست پیوندی4 تکی بود (و شاید در کل ۱۰۰ خط کد بود) را پیاده سازی کرده بودیم.

در عین حال متوجه شدیم که دسترسی‌های مکرر همیشه به یک سطر انجام می‌شود، بنابراین به جای پیاده سازی یک LRU cache، فقط یک cache تک آیتمی را پیاده سازی کردیم:


DiskObject lastUsed;  // class member
DiskObject lookUp(String key) {
    if (lastUsed == null || !lastUsed.key().equals(key)) {
        lastUsed = loadDiskObject(key);
    }
    return lastUsed;
}

بدون اینکه کدنویسی زیادی را انجام دهیم، این پیاده سازی سبب بهبود ۹۰ درصدی بود، و برنامه نیز از مقدار حافظه کمی استفاده می‌کرد.

فواید «حذف نیازمندی‌ّها» و «حل مسئله‌های ساده‌تر» نباید اغراق‌آمیز باشد. نیازمندی‌ها اغلب با شیوه‌های ظریفی با یکدیگر تداخل دارند. این یعنی، مدت زمان حل نیمی از مسئله، نسبت کدنویسی، حدودا یک چهارم است.

کدپایه خود را کوچک نگه دارید

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

با رشد پروژه، رفته رفته دایرکتوری پروژه شما با فایل‌های منبعِ بیشتری پر می‌شود. به زودی به چندین دایرکتوری برای سازماندهی همه آن‌ها نیاز پیدا خواهید کرد. یادآوری اینکه کدام توابع چه توابع دیگری را فراخوانی می‌کنند سخت‌تر شده و همچنین ردیابی و برطرف کردن اشکالات، کار بیشتری را می‌طلبد.

در نهایت، شما کد منبع بزرگی دارید که در تعداد بسیاری از دایرکتوری‌های مختلف پخش شده‌اند. این پروژه خیلی بزرگ است و هیچ کس به تنهایی همه آن را نمی‌فهمد. افزودن ویژگی‌های جدید دردناک می‌شود و کار با کد، دست و پا گیر و ناخوشایند خواهد بود.

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

  • تا جای امکان باید کد «کاربردی3» بیشتری ایجاد کنید تا بتوانید کدهای تکراری را حذف کنید(فصل ۱۰ را مشاهده کنید).
  • کدهای استفاده نشده یا ویژگی‌های بی‌فایده را حذف کنید( در ادامه توضیح می‌دهیم).
  • پروژه خود را به صورت جدا در دسته‌بندی‌های مختلف، در زیر پروژه‌های جداگانه نگه دارید.
  • به طور کلی، از وزن (حجم) کدپایه خود آگاه باشید و آن را سبک و چابک4 نگه دارید.

حذف کدهای بی‌فایده

باغبان‌ها اغلب برای زنده نگه‌داشتن و رشد گیاهان، آن‌ها را هرس می‌کنند. در برنامه‌نویسی هم این ایده خوبی است که کدهای بی فایده را هرس کنیم.

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

حذف توابع مجزا آسان است، اما گاهی «کد استفاده نشده» در کل پروژه تنیده شده و برای شما مجهول است. در اینجا چند مثال آورده‌ایم:

  • شما سیستم اصلی خود را برای مدیریت نام‌های بین المللی برای فایل‌ها طراحی کرده‌اید و اکنون کد توسط تبدیلات، کدهای جدیدتری تولید کرده است. با این حال، این کد کاملا کاربردی نیست و برنامه شما هیچ گاه با نام‌های بین المللی، مورد استفاده قرار نگرفته است. چرا چنین قابلیتی را حذف نکنیم؟
  • شما می‌خواهید حتی اگر سیستم با کمبود حافظه5 مواجه شد، برنامه کار کند، بنابراین منطق‌های هوشمند زیادی نوشته‌اید که سعی می‌کند برنامه را در شرایط کمبود حافظه بازیابی کند. این ایده خوب است اما در عمل وقتی برنامه در شرایط کمبود حافظه اجرا شود، برنامه شما به هر حال به یک زامبی ناپایدار6 تبدیل می‌شود. در این حالت همه ویژگی‌های هسته غیر قابل استفاده شده و فاصله برنامه تا مرگ آن تنها یک کلیک موس خواهد بود. پس چرا برنامه را تنها با یک پیام ساده «متاسفیم! سیستم دچار کمبود حافظه شده است» خاتمه ندهیم و همه کد مربوط به کمبود حافظه را حذف نکنیم؟

با کتابخانه‌های موجود در اطراف خود آشنا باشید

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

در اینجا یک پیشنهاد خوب داریم: هر چند وقت یک‌بار، ۱۵ دقیقه از وقت خود را برای خواندن نام همه توابع، ماژول‌ها و نوع‌ها در کتابخانه استاندارد خود صرف کنید. این شامل کتابخانه الگوی استاندارد C++، Java API، ماژول‌های داخلی Python و بقیه موارد می‌شود.

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

مثال: لیست‌ها و مجموعه‌ها در Python

فرض کنید که شما یک لیست در Python دارید(مثلا [2, 1, 2]) و می‌خواهید لیستی از عناصر بدون تکرار را استخراج کنید(در این مورد [2,1]). می‌توانید این کار را با استفاده از یک دیکشنری، که دارای یک لیست از key‌هایی است که تضمین می‌کند منحصر به فرد باشند، به شکل زیر پیاده سازی کنید:


def unique(elements):
    temp = {}
    for element in elements:
        temp[element] = None  # The value doesn't matter.
    return temp.keys()
unique_elements = unique([2,1,2])

از سوی دیگر می‌توانید فقط از Data Type کمتر شناخته شده استفاده کنید:


unique_elements = set([2,1,2])  # Remove duplicates

این object دقیقا مثل یک لیست عادی قابل تکرار است. اگر واقعا یک لیست object دیگر را خواستید، فقط کافی است به صورت زیر از آن استفاده کنید:


unique_elements = list(set([2,1,2]))  # Remove duplicates

بدیهی است که set ابزار مناسبی برای کار در اینجا است. اما اگر از این نوع‌داده set آگاه نبودید، احتمالا کدی شبیه تابع unique() را خودتان تولید می‌کردید.

چرا استفاده مجدد از کتابخانه‌ها چنین موفقیتی دارد؟

طبق آمار مهندسین نرم افزار به طور میانگین روزانه ده خط کد قابل حمل1 تولید می‌کنند. هنگامی که برنامه‌نویسان برای اولین بار این جمله را می‌شنوند، از قبول کردن آن طفره رفته و می‌گویند: «ده خط کد؟ من می‌توانم آن را در یک دقیقه بنویسم!». کلمه کلیدی در اینجا قابل‌حمل است. هر خط از کد در یک کتابخانه بالغ، نشان‌دهنده مقدار مناسبی از طراحی، اشکال‌زدایی، بازنویسی، مستندسازی، بهینه‌سازی و انجام تست است. هر خط از کد که از این فرآیندهای داروینی2 جان سالم به در ببرد، ارزش زیادی دارد. به همین دلیل است که استفاده مجدد از کتابخانه‌ها یک پیروزی محسوب می‌شود، هم در صرفه جویی زمان و هم نوشتن کد کمتر.

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

زمانی که یک سرور وب به طور متناوب کدهای پاسخ 4xx HTTP یا 5xx HTTP را بر می‌گرداند، این نشان دهنده یک مشکل است(4xx خطایی از سمت کلاینت است و 5xx مربوط به زمانی است که سرور دچار خطا شده است). ما می‌خواهیم برنامه‌ای بنویسیم که لاگ‌های3 دسترسی به یک وب سرور را بررسی4 کرده و تشخیص دهد که کدام URLها، علت بیشترین خطاها هستند.

لاگ‌های دسترسی معمولا چیزی شبیه به این است:


1.2.3.4 example.com [24/Aug/2010:01:08:34] "GET /index.html HTTP/1.1" 200 ...
2.3.4.5 example.com [24/Aug/2010:01:14:27] "GET /help?topic=8 HTTP/1.1" 500 ...
3.4.5.6 example.com [24/Aug/2010:01:15:54] "GET /favicon.ico HTTP/1.1" 404 ...
...

به طور کلی، آن‌ها شامل خط‌هایی از این موارد هستند:


browser-IP    host    [date]    "GET /url-path HTTP/1.1"    HTTP-response-code ...

نوشتن برنامه‌ای برای پیدا کردن بیشترین url-pathها با کد پاسخ 4xx یا 5xx احتمالا به سادگی ۲۰ خط کدنویسی در یک زبان شبیه C++ یا Java است.

به جای این کار، شما می‌توانید در Unix از دستور زیر در خط فرمان استفاده کنید:


cat access.log | awk '{ print $5 " " $7 }' | egrep "[45]..$" \
| sort | uniq -c | sort -nr

که خروجی زیر را تولید می‌کند:


95 /favicon.ico 404
13 /help?topic=8 500
11 /login 403
...
<count> <path> <http response code>

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

خلاصه فصل

**ماجراجویی، هیجان!! یک Jedi1 این چیزها را آرزو نمی‌کند.(Yoda)**

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

  • از بین بردن ویژگی‌های غیر ضروری و بدون استفاده حتی اگر پیشرفته باشند.
  • بازنگری در مورد نیازمندی‌ها برای حل مشکل با ساده‌ترین نسخه از کد.
  • آگاهی داشتن در مورد کتابخانه‌های استاندارد با خواندن دوره‌ای کل API‌ها.
[1]:
[2]:
[3]:
[4]:
[5]:
[6]:
[7]:
[8]:
[9]:
[10]:
[11]:
[12]:
[13]:
[14]:
[15]:
[16]:
[17]:
[18]:
[19]:
[20]:
[21]:
[22]:
[23]:
[24]:
[25]:
[26]:
[27]:
[28]: