زمانی شما واقعا چیزی را فهمیدهاید که بتوانید آن را به مادربزرگ خود توضیح دهید.
(آلبرت انیشتین)
هنگام توضیح یک ایده پیچیده برای یک شخص، بیان همه جزئیات به راحتی او را دچار سردرگمی میکند.تسلط بر توضیح یک ایده به زبان ساده به گونهای که شخصی که از شما دانش کمتری دارد، بتواند آن را درک کند، مهارتی ارزشمند است. لازمه این کار، توان خلاصه کردن یک ایده در مهمترین مفاهیم آن است. انجام این کار علاوه بر کمک به درک دیگران برای شما نیز این فایده را دارد که میتوانید در مورد ایده خود با وضوح بیشتری فکر کنید.
در هنگام ارائه کد به خواننده باید از این مهارت استفاده کنید. معتقدیم مهمترین وسیله برای توضیح نحوه کارکرد برنامه شما، سورس کدتان است، بنابراین باید کد را به زبان انگلیسی ساده بنویسید.
در این فصل از فرآیندی ساده استفاده خواهیم کرد که میتواند به شما در نوشتن یک کد واضحتر کمک کند:
- به عنوان یک همکار آنچه را کد برای انجام نیاز دارد به زبان ساده، توضیح دهید.
- به کلمات کلیدی و عبارتهای استفاده شده در این توضیحات توجه کنید.
- کد خود را مطابق با این توضیحات بنویسد.
در اینجا قطعهای از یک کد به زبان PHP از یک صفحه وب داریم. این کد در بالای یک صفحه امن قرار دارد که بررسی میکند آیا کاربر مجاز1 به دیدن این صفحه است یا نه؟ اگر مجاز نباشد، بلافاصله صفحهای را نشان میدهد که به کاربر میگوید اجازه دیدن صفحه را ندارد:
$is_admin = is_admin_request();
if ($document) {
if (!$is_admin && ($document['username'] != $_SESSION['username'])) {
return not_authorized();
}
} else {
if (!$is_admin) {
return not_authorized();
}
}
// continue rendering the page ...
منطق این کد هنوز به طور کامل، تکمیل نشده است. همان گونه که از بخش دوم(ساده سازی حلقهها و منطق) به یاد دارید، درک چنین منطقهای تودرتویی ساده نیست. احتمالا منطق این کد میتواند سادهتر شود، اما چگونه؟ اجازه دهید با توصیف منطق به زبان ساده شروع کنیم:
دو روش برای مجاز بودن شما جهت دیدن صفحه وب وجود دارد:
- شما admin هستید.
- شما مالک سند فعلی هستید(اگر سندی وجود داشته باشد) در غیر این صورت، شما مجاز نیستید.
روشی جایگزین با الهام از این توضیحات این است:
if (is_admin_request()) {
// authorized
} elseif ($document && ($document['username'] == $_SESSION['username'])) {
// authorized
} else {
return not_authorized();
}
// continue rendering the page ...
هر چند این نسخه به دلیل وجود دو قسمت خالی کمی غیرمعمول است، اما دارای کدی کوتاهتر و منطقی سادهتر است، زیرا هیچ نفی1 کردنی وجود ندارد(در حالی که راه حل قبلی دارای سه «not» بود). همچنین درک آن نیز آسانتر میباشد.
زمانی ما یک وبسایتی داشتیم که شامل یک «جعبه نکات1» بود و پیشنهادات مفیدی را به کاربر نشان میداد. مانند:
Tip: Log in to see your past queries. [Show me another tip!]
در آن جا هزاران نکته وجود داشت که همگی در داخل کد HTML مخفی شده بود:
<div id="tip-1" class="tip">Tip: Log in to see your past queries.</div>
<div id="tip-2" class="tip">Tip: Click on a picture to see it close up.</div>
...
هنگامی بازدید یک کاربر از صفحه، یکی از این divها به صورت تصادفی قابل نمایش(visible) شده و بقیه divها در حالت مخفی(hidden) باقی میمانند و اگر روی لینک «Show me another tip!» کلیک شود، نکته بعدی نمایش داده خواهد داد. در اینجا برخی از کدهایی که برای پیاده سازی این ویژگی با استفاده از کتابخانه jQuery نوشته شده بود، آورده شده است:
var show_next_tip = function () {
var num_tips = $('.tip').size();
var shown_tip = $('.tip:visible');
var shown_tip_num = Number(shown_tip.attr('id').slice(4));
if (shown_tip_num === num_tips) {
$('#tip-1').show();
} else {
$('#tip-' + (shown_tip_num + 1)).show();
}
shown_tip.hide();
};
هرچند این کد خوبی است اما میتواند بهتر از این شود. اجازه دهید ابتدا در جملاتی توصیف کنیم که این کد(هنگام کلیک کاربر) تلاش میکند چه کاری انجام دهد:
نکته فعلی قابل مشاهده را پیدا کرده و آن را مخفی کنید. سپس نکته بعدی را پیدا کرده و آن را نمایش دهید. اگر نکته بعدی وجود نداشت به ابتدای چرخه بر میگردیم.
راه حل جدید، مبتنی بر این توضیحات به این صورت است:
var show_next_tip = function () {
var cur_tip = $('.tip:visible').hide(); // find the currently visible tip and hide it
var next_tip = cur_tip.next('.tip'); // find the next tip after it
if (next_tip.size() === 0) { // if we've run out of tips,
next_tip = $('.tip:first'); // cycle back to the first tip
}
next_tip.show(); // show the new tip
};
این راه حل علاوه بر داشتن خطوط کمتر، نیازی به دستکاری مستقیم مقادیر integer ندارد.
همچنین تطابق بیشتری با نحوه تفکر یک شخص در مورد آن دارد.در این راهکار استفاده از متدی به نام next()، که متعلق jQuery است، کمک کننده است. به یاد داشته باشید که یکی از ابزارهای مختصر نویسی کد این است که، از آنچه که کتابخانه شما عرضه کرده است، آگاه باشید.
مثالهای قبلی فرآیند ما را در بلوکهای کوچکی از کد اعمال کرده اند. در مثال بعدی، ما آنها را در یک تابع بزرگتر اعمال میکنیم. همان گونه که خواهید دید، این متد میتواند با شناسایی بخشی از کد، که میتواند شکسته شود، به شما کمک کند. تصور کنید که سیستمی برای ثبت خرید سهام داریم. هر تراکنش چهار قسمت از داده را دارد:
- time (یک تاریخ دقیق و زمان خرید)
- ticker_symbol (e.g., GOOG)
- price (e.g., $600)
- number_of_shares (e.g., 100)
به دلایل عجیبی، دادهها در سه جدول جداگانه دیتابیس، بصورت زیر پخش شده و در هر یک از آنها، time کلید اصلی1 منحصر به فرد2 است.
حال ما باید برنامهای برای پیوستن1 سه جدول به یکدیگر بنویسیم(همان گونه که یک عملیات JOIN در SQL انجام میشود). این مرحله باید ساده باشد زیرا سطرها همه بر اساس time مرتب شدهاند اما متاسفانه برخی از سطرها مفقود شدهاند. شما میخواهید همه سطرهایی که timeهای هر سه آنها با یکدیگر مطابقت دارد را پیدا کنید، و هر سطری را که نمیتوان مرتب کرد را نادیده2 بگیرید.
در اینجا کدی به زبان Python داریم که همه سطرهای منطبق را پیدا میکند:
def PrintStockTransactions():
stock_iter = db_read("SELECT time, ticker_symbol FROM ...")
price_iter = ...
num_shares_iter = ...
# Iterate through all the rows of the 3 tables in parallel.
while stock_iter and price_iter and num_shares_iter:
stock_time = stock_iter.time
price_time = price_iter.time
num_shares_time = num_shares_iter.time
# If all 3 rows don't have the same time, skip over the oldest row
# Note: the "<=" below can't just be "<" in case there are 2 tied-oldest.
if stock_time != price_time or stock_time != num_shares_time:
if stock_time <= price_time and stock_time <= num_shares_time:
stock_iter.NextRow()
elif price_time <= stock_time and price_time <= num_shares_time:
price_iter.NextRow()
elif num_shares_time <= stock_time and num_shares_time <= price_time:
num_shares_iter.NextRow()
else:
assert False # impossible
continue
assert stock_time == price_time == num_shares_time
# Print the aligned rows.
print "@", stock_time,
print stock_iter.ticker_symbol,
print price_iter.price,
print num_shares_iter.number_of_shares
stock_iter.NextRow()
price_iter.NextRow()
num_shares_iter.NextRow()
هر چند این کد کار میکند، اما چیزهای زیادی در مورد چگونگی گذر حلقه از سطرهای غیرمنطبق1 وجود دارد. ممکن است برخی از علائم هشدار در ذهن شما خاموش شده باشد: آیا این کد میتواند برخی از سطرها را از دست بدهد؟ آیا ممکن است که مقدار قبلی، پایان یک جریان را در هر تکرار بخواند؟ چگونه میتوانیم این کد را خواناتر کنیم؟
یک توضیح به زبان ساده از راه حل
بار دیگر، بیایید به عقب بازگشته و به زبان ساده توضیح دهیم که در صدد انجام چه کاری هستیم:
ما در حال خواندن سه سطر تکرار شونده به شکل موازی هستیم.
هر زمان که time سطرها منطبق نیستند، سطرها را پیش ببرید تا آنها منطبق شوند.
سپس سطرهای همتراز شده را چاپ کرده و دوباره سطرها را پیش ببرید.
این کار را تا زمانی که هیچ سطر منطبقی باقی نماند، ادامه دهید.
به عقب برگردید و به کد اصلی نگاه کنید. آشفتهترین قسمت، بلوک تعامل با «قسمت پیشروی سطرها تا آنها با هم منطبق شوند» بود. برای نمایش کد با شفافیت بیشتر، میتوانیم منطق آشفته را از کد استخراج کرده و در تابع جدیدی به نام AdvanceToMatchingTime() قرار دهیم.
در اینجا نسخه جدیدی از کد وجود دارد که از این تابع جدید استفاده میکند:
def PrintStockTransactions():
stock_iter = ...
price_iter = ...
num_shares_iter = ...
while True:
time = AdvanceToMatchingTime(stock_iter, price_iter, num_shares_iter)
if time is None:
return
# Print the aligned rows.
print "@", time,
print stock_iter.ticker_symbol,
print price_iter.price,
print num_shares_iter.number_of_shares
stock_iter.NextRow()
price_iter.NextRow()
num_shares_iter.NextRow()
همان گونه که مشاهده میکنید، درک این کد بسیار سادهتر است، زیرا ما همه جزئیات مبهم، در مورد تطابق سطرها را مخفی کردیم.
تصور اینکه چگونه AdvanceToMatchingTime() را مینویسید ساده است. در بدترین حالت، بسیار شبیه به بلوک کد زشتی است که در نسخه اول موجود است:
def AdvanceToMatchingTime(stock_iter, price_iter, num_shares_iter):
# Iterate through all the rows of the 3 tables in parallel.
while stock_iter and price_iter and num_shares_iter:
stock_time = stock_iter.time
price_time = price_iter.time
num_shares_time = num_shares_iter.time
# If all 3 rows don't have the same time, skip over the oldest row
if stock_time != price_time or stock_time != num_shares_time:
if stock_time <= price_time and stock_time <= num_shares_time:
stock_iter.NextRow()
elif price_time <= stock_time and price_time <= num_shares_time:
price_iter.NextRow()
elif num_shares_time <= stock_time and num_shares_time <= price_time:
num_shares_iter.NextRow()
else:
assert False # impossible
continue
assert stock_time == price_time == num_shares_time
return stock_time
حال بیایید این کد را با اعمال شیوه خودمان در AdvanceToMatchingTime() بهبود دهیم. در اینجا توضیحی در مورد آنچه که این متد باید انجام دهد را داریم:
به زمان هر سطر فعلی نگاه کنید: اگر آنها تراز1 شدهاند، کار ما تمام است. در غیر این صورت، به سمت سطرهای قبلی بروید. این کار را تا زمانی که همه سطرها تراز شده باشند(یا یکی از این تکرارها به پایان برسد) ادامه دهید.
این توضیحات بسیار واضح، زیبا و موزونتر از کد قبلی است. نکته قابل توجه این است که این توضیحات هرگز اشارهای به stock_iter یا دیگر جزئیات خاص در مورد مشکل ما نمیکند. این بدان معنی است که ما میتوانیم متغیرها را نیز به شکل سادهتر و عمومیتری تغییر نام دهیم. در اینجا نتیجه این کد را میبینیم:
def AdvanceToMatchingTime(row_iter1, row_iter2, row_iter3):
while row_iter1 and row_iter2 and row_iter3:
t1 = row_iter1.time
t2 = row_iter2.time
t3 = row_iter3.time
if t1 == t2 == t3:
return t1
tmax = max(t1, t2, t3)
# If any row is "behind," advance it.
# Eventually, this while loop will align them all.
if t1 < tmax: row_iter1.NextRow()
if t2 < tmax: row_iter2.NextRow()
if t3 < tmax: row_iter3.NextRow()
return None # no alignment could be found
این کد خیلی شفافتر از کد قبلی است. الگوریتم آن سادهتر شده و شرطهای پیچیده کمتری دارد. ما از نامهای کوتاهی مانند t1 استفاده کردیم که دیگر نیازی به فکر کردن در مورد ستونهای1 خاص دیتابیس ندارد.
در این فصل در مورد تکنیک ساده توصیف برنامه به زبان ساده و استفاده از این توصیفها برای کمک به نوشتن کد سادهتر بحث کردیم. این تکنیک به شکل فریبندهای ساده، اما بسیار مفید است. نگاه کردن به کلمات و عبارتهای استفاده شده در توصیفات میتواند در تشخیص اینکه کدام زیرمسئلهها باید شکسته شوند، به شما کمک کند.
فرآیند «بیان کردن موارد به زبان ساده» کاربردهای دیگری نیز غیر از کمک در نوشتن کد دارد، به عنوان مثال یکی از همکاران ما در آزمایشگاه کامپیوتر بیان میکرد که، هر گاه یک دانشجو برای اشکالزدایی برنامهاش به کمک نیاز داشت، ابتدا باید مشکل را به یک خرس عروسکی که در گوشه اتاق بود، توضیح میداد. با کمال تعجب در اکثر موارد توصیف مسئله با صدای بلند، به دانشجو کمک میکرد تا متوجه راه حل شود. این تکنیک به اسم اردک پلاستیکی2 معروف است.
به یاد داشته باشید که اگر نمیتوانید مشکل یا طراحی خود را با کلمات توصیف کنید، احتمالا چیزی را در نظر نگرفتهاید یا در برنامه تعریف نشده است. بیان یک برنامه یا یک ایده با کلمات، میتواند آن را مجبور کند که به خودش شکل بگیرد.
[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]: