فصل دوم - رویکردی عملگرایانه
نکتهها و ترفندهای خاصی وجود دارند که در تمام سطوح توسعه نرمافزار کاربرد دارند؛ فرآیندهایی که تقریباً جهانشمولاند و ایدههایی که تقریبا بدیهی (اصل موضوع) به شمار میآیند. با این حال، این رویکردها به ندرت به این شکل مستند شدهاند؛ شما اغلب آنها را به صورت جملاتی پراکنده در بحثهای طراحی، مدیریت پروژه یا کدنویسی مییابید. اما برای راحتی شما، ما این ایدهها و فرآیندها را در اینجا گردآوری کردهایم.
اولین و شاید مهمترین موضوع، به قلب توسعه نرمافزار میپردازد: مبحث ۸، جوهرهی طراحی خوب. همه چیز از این موضوع نشأت میگیرد.
دو بخش بعدی، مبحث ۹، DRY—شرارتهای دوبارهکاری و مبحث ۱۰، تعامد (Orthogonality)، ارتباط تنگاتنگی با هم دارند. اولی به شما هشدار میدهد که دانش را در سراسر سیستمهای خود تکرار نکنید، و دومی میگوید که هیچ تکه دانشی را بین چندین جزء سیستم پخش نکنید.
با افزایش سرعت تغییرات، حفظ کارآمدی برنامههایمان دشوارتر و دشوارتر میشود. در مبحث ۱۱، برگشتپذیری (Reversibility)، نگاهی خواهیم داشت به تکنیکهایی که به ایزوله کردن پروژههای شما در برابر محیط در حال تغییرشان کمک میکنند.
دو بخش بعدی نیز مرتبط هستند. در مبحث ۱۲، گلولههای ردیاب (Tracer Bullets)، درباره سبکی از توسعه صحبت میکنیم که به شما اجازه میدهد نیازمندیها را گردآوری کنید، طراحیها را تست کنید و همزمان کد را پیادهسازی نمایید. این تنها راه همگام ماندن با سرعت زندگی مدرن است. مبحث ۱۳، پروتوتایپها و کاغذهای یادداشت (Prototypes and Post-it Notes) به شما نشان میدهد چگونه از پروتوتایپسازی (نمونهسازی اولیه) برای تست معماریها، الگوریتمها، رابطها و ایدهها استفاده کنید. در دنیای مدرن، حیاتی است که ایدهها را تست کنید و بازخورد بگیرید، پیش از آنکه با تمام وجود به آنها متعهد شوید.
همزمان با بلوغ تدریجی علوم کامپیوتر، طراحان زبانهایی با سطح بالاتر تولید میکنند. اگرچه کامپایلری که دستور «انجامش بده» (Make it so) را بپذیرد هنوز اختراع نشده است، اما در مبحث ۱۴، زبانهای دامنه (Domain Languages)، پیشنهادهای متواضعانهتری ارائه میکنیم که میتوانید خودتان پیادهسازی کنید.
در نهایت، همه ما در دنیایی با زمان و منابع محدود کار میکنیم. شما میتوانید بهتر از پس این کمبودها برآیید (و رئیسها یا مشتریان خود را راضیتر نگه دارید) اگر در محاسبه اینکه کارها چقدر طول میکشند مهارت پیدا کنید، که این موضوع را در مبحث ۱۵، تخمین زدن (Estimating) پوشش میدهیم.
این اصول بنیادین را در حین توسعه در ذهن داشته باشید تا کدی بنویسید که بهتر، سریعتر و قویتر باشد. حتی میتوانید کاری کنید که این فرآیند ساده به نظر برسد.
مبحث ۸: جوهرهی طراحی خوب
دنیا پر از مرشدان و صاحبنظرانی است که همگی مشتاقند خردِ بهسختیبهدستآمدهی خود را در مورد «چگونه نرمافزار طراحی کنیم» منتقل کنند. مخففها، لیستها (که انگار عاشق ۵تایی بودن هستند)، الگوها، نمودارها، ویدیوها، سخنرانیها و (چون اینترنت، اینترنت است) احتمالاً یک سری ویدیوی جذاب درباره «قانون دِمتر» با حرکات موزون وجود دارد. و ما، نویسندگان مهربان شما، نیز در این زمینه مقصریم.
اما میخواهیم با توضیح چیزی که تنها اخیراً برای خودمان آشکار شده است، جبران مافات کنیم. ابتدا، بیانیه کلی:
نکته ۱۴: تغییر طراحی خوب آسانتر از طراحی بد است.
چیزی خوب طراحی شده است که با افرادی که از آن استفاده میکنند سازگار شود. برای کد، این یعنی باید با «تغییر کردن» سازگار شود. بنابراین ما به اصل ETC معتقدیم: سادهتر برای تغییر (Easier to Change). ETC. همین.
تا جایی که ما میتوانیم بگوییم، هر اصل طراحی که وجود دارد، حالت خاصی از ETC است.
- چرا کاهش وابستگی (Decoupling) خوب است؟ چون با ایزوله کردن دغدغهها، تغییر هر کدام را آسانتر میکنیم. ETC.
- چرا اصل تکمسئولیتی (Single Responsibility Principle) مفید است؟ چون تغییر در نیازمندیها تنها با تغییر در یک ماژول منعکس میشود. ETC.
- چرا نامگذاری مهم است؟ چون نامهای خوب کد را خواناتر میکنند، و شما برای تغییر دادن کد باید آن را بخوانید. ETC!
ETC یک ارزش است، نه یک قانون
ارزشها چیزهایی هستند که به شما در تصمیمگیری کمک میکنند: آیا باید این کار را انجام دهم یا آن کار را؟ وقتی نوبت به تفکر درباره نرمافزار میرسد، ETC یک راهنماست که به شما کمک میکند بین مسیرها انتخاب کنید. درست مانند تمام ارزشهای دیگرتان، این اصل باید درست پشت فکر خودآگاه شما شناور باشد و به آرامی شما را به مسیر درست هل دهد.
اما چطور این اتفاق را رقم میزنید؟ تجربه ما نشان میدهد که این کار نیازمند مقداری تقویت آگاهانهی اولیه است. ممکن است لازم باشد حدود یک هفته عمداً از خود بپرسید: «آیا کاری که الان انجام دادم کل سیستم را برای تغییر سادهتر کرد یا سختتر؟»
این کار را وقتی فایلی را ذخیره میکنید انجام دهید. وقتی تستی مینویسید انجام دهید. وقتی باگی را رفع میکنید انجام دهید.
یک پیشفرض ضمنی در ETC وجود دارد. فرض بر این است که یک شخص میتواند تشخیص دهد کدامیک از مسیرهای متعدد در آینده برای تغییر آسانتر خواهد بود. در بیشتر مواقع، عقل سلیم درست عمل میکند و میتوانید حدس هوشمندانهای بزنید.
البته گاهی اوقات، هیچ ایدهای نخواهید داشت. اشکالی ندارد. در آن موارد، فکر میکنیم میتوانید دو کار انجام دهید.
اول، با توجه به اینکه مطمئن نیستید تغییر چه شکلی خواهد داشت، همیشه میتوانید به مسیر نهاییِ «تغییر آسان» پناه ببرید: سعی کنید آنچه مینویسید قابل جایگزینی باشد. به این ترتیب، هر اتفاقی که در آینده بیفتد، این تکه کد مانعی بر سر راه نخواهد بود. این شاید افراطی به نظر برسد، اما در واقع کاری است که باید همیشه انجام دهید. این در واقع همان فکر کردن به نگه داشتنِ کد به صورت «جداسازی شده» (Decoupled) و «منسجم» (Cohesive) است.
دوم، با این موضوع به عنوان راهی برای توسعه غرایز خود برخورد کنید. موقعیت را در دفترچه مهندسی روزانه خود یادداشت کنید: انتخابهایی که دارید و برخی حدسها درباره تغییر. یک برچسب (Tag) در سورسکد بگذارید. سپس، بعداً، وقتی این کد مجبور به تغییر شد، میتوانید به عقب نگاه کنید و به خودتان بازخورد دهید. این کار ممکن است دفعه بعد که به دوراهی مشابهی رسیدید، کمکتان کند.
بقیه بخشهای این فصل ایدههای خاصی درباره طراحی دارند، اما انگیزه همه آنها همین یک اصل است.
بخشهای مرتبط شامل
- مبحث ۹: DRY—شرارتهای دوبارهکاری
- مبحث ۱۰: تعامد (Orthogonality)
- مبحث ۱۱: برگشتپذیری
- مبحث ۱۴: زبانهای دامنه
- مبحث ۲۸: کاهش وابستگی (Decoupling)
- مبحث ۳۰: تغییر شکل برنامهنویسی
- مبحث ۳۱: مالیات وراثت
چالشها
-
به یک اصل طراحی فکر کنید که مرتباً از آن استفاده میکنید. آیا هدف آن این است که چیزها را «سادهتر برای تغییر» (easy-to-change) کند؟
-
همچنین به زبانها و پارادایمهای برنامهنویسی فکر کنید (شیگرا، تابعی، واکنشی و غیره). آیا هیچکدام در زمینه کمک به نوشتن کد ETC، نکات مثبت بزرگ یا نکات منفی بزرگی دارند؟ آیا هیچکدام هر دو را دارند؟ هنگام کدنویسی، چه کاری میتوانید انجام دهید تا منفیها را حذف و مثبتها را برجسته کنید؟
-
بسیاری از ادیتورها (چه به صورت داخلی و چه از طریق افزونهها) از اجرای دستورات هنگام ذخیره فایل پشتیبانی میکنند. ادیتور خود را تنظیم کنید تا هر بار که ذخیره میکنید، یک پیغام ?ETC پاپآپ کند و از آن به عنوان نشانهای برای فکر کردن به کدی که تازه نوشتهاید استفاده کنید. آیا تغییر آن آسان است؟*
مبحث ۹: DRY — شرارتهای دوبارهکاری
دادن دو تکه دانشِ متضاد به یک کامپیوتر، روش مورد علاقهی «کاپیتان جیمز تی. کرک» برای از کار انداختن هوش مصنوعیِ غارتگر بود. متأسفانه، همین اصل میتواند در زمینگیر کردن کدِ شما نیز مؤثر باشد.
به عنوان برنامهنویس، ما دانش را گردآوری، سازماندهی، نگهداری و مهار میکنیم. ما دانش را در مستندات (Specifications) ثبت میکنیم، در کدِ در حال اجرا به آن جان میبخشیم، و از آن برای فراهم کردن بررسیهای لازم در حین تست استفاده میکنیم.
متأسفانه، دانش پایدار نیست. تغییر میکند—اغلب هم با سرعت. درک شما از یک نیازمندی ممکن است پس از جلسه با مشتری تغییر کند. دولت قانونی را تغییر میدهد و بخشی از منطق تجاری (Business Logic) قدیمی میشود. تستها ممکن است نشان دهند که الگوریتم انتخاب شده کار نمیکند.
تمام این بیثباتیها به این معنی است که ما بخش بزرگی از زمان خود را در حالت نگهداری (Maintenance mode) سپری میکنیم؛ در حال سازماندهی مجدد و بیان دوبارهی دانش در سیستمهایمان. اکثر مردم فرض میکنند که نگهداری زمانی آغاز میشود که یک برنامه عرضه شده باشد، و نگهداری به معنای رفع باگها و ارتقای ویژگیهاست. ما فکر میکنیم این افراد در اشتباهند.
برنامهنویسان دائماً در حالت نگهداری هستند. درک ما روز به روز تغییر میکند. نیازمندیهای جدید از راه میرسند و همزمان که ما سرگرم کار روی پروژه هستیم، نیازمندیهای موجود تکامل مییابند. شاید محیط تغییر کند. دلیلش هرچه باشد، نگهداری یک فعالیت گسسته نیست، بلکه بخشی روتین از کل فرآیند توسعه است.
وقتی ما نگهداری انجام میدهیم، باید بازنماییِ چیزها—آن کپسولهای دانشِ تعبیه شده در برنامه—را پیدا کرده و تغییر دهیم. مشکل اینجاست که در مستندات، فرآیندها و برنامههایی که توسعه میدهیم، به سادگی ممکن است دانش را تکرار کنیم (Duplicate)، و وقتی چنین میکنیم، خود را به یک کابوسِ نگهداری دعوت کردهایم—کابوسی که خیلی پیش از عرضه برنامه شروع میشود.
ما احساس میکنیم تنها راه برای توسعهی مطمئن نرمافزار، و برای اینکه فهم و نگهداری توسعههایمان آسانتر شود، پیروی از اصلی است که ما آن را DRY مینامیم:
هر تکه از دانش باید یک بازنمایی واحد، بدون ابهام و معتبر در یک سیستم داشته باشد.
چرا آن را DRY مینامیم؟
نکته ۱۵: DRY — خودتان را تکرار نکنید (Don’t Repeat Yourself)
جایگزینِ این اصل، داشتنِ همان چیز است که در دو یا چند جا بیان شده باشد. اگر یکی را تغییر دهید، باید یادتان باشد که بقیه را هم تغییر دهید، وگرنه مثل کامپیوترهای بیگانهی فضایی، برنامه شما با یک تناقض به زانو در میآید. مسئله این نیست که آیا یادتان خواهد ماند یا نه؛ مسئله این است که کی فراموش خواهید کرد.
شما خواهید دید که اصل DRY بارها و بارها در طول این کتاب ظاهر میشود، اغلب در زمینههایی که هیچ ربطی به کدنویسی ندارند. ما احساس میکنیم که این یکی از مهمترین ابزارها در جعبهابزارِ «برنامهنویس عملگرا» است.
در این بخش، مشکلات دوبارهکاری (Duplication) را ترسیم میکنیم و استراتژیهای عمومی برای مقابله با آن پیشنهاد میدهیم.
DRY فراتر از کد است
بیایید همین ابتدا یک موضوع را روشن کنیم. در ویرایش اول این کتاب، ما در توضیح اینکه دقیقاً منظورمان از «خودتان را تکرار نکنید» چیست، ضعیف عمل کردیم. بسیاری از مردم برداشت کردند که این فقط به کد اشاره دارد: فکر کردند DRY یعنی «خطوط کد منبع را کپی-پیست نکنید».
این بخشی از DRY هست، اما بخشی بسیار کوچک و نسبتاً بدیهی.
اصل DRY درباره تکرارِ دانش است، تکرارِ نیت. درباره بیان کردن یک چیزِ واحد در دو جای متفاوت است، که احتمالاً به دو روش کاملاً متفاوت بیان شدهاند.
محکِ اصلی اینجاست: وقتی یک جنبهی واحد از کد باید تغییر کند، آیا خودتان را در حال ایجاد آن تغییر در چندین مکان و با چندین فرمت مختلف میبینید؟ آیا مجبورید هم کد و هم مستندات را تغییر دهید، یا هم اسکیمای دیتابیس و هم ساختاری که آن را نگه میدارد، یا...؟ اگر چنین است، کد شما DRY نیست.
پس بیایید به برخی نمونههای معمولِ دوبارهکاری نگاه کنیم.
دوبارهکاری در کد
شاید پیشپاافتاده باشد، اما دوبارهکاری در کد بسیار رایج است. به این مثال دقت کنید:
def print_balance(account)
printf "Debits: %10.2f\n", account.debits
printf "Credits: %10.2f\n", account.credits
if account.fees < 0
printf "Fees: %10.2f-\n", -account.fees
else
printf "Fees: %10.2f\n", account.fees
end
printf " ———-\n"
if account.balance < 0
printf "Balance: %10.2f-\n", -account.balance
else
printf "Balance: %10.2f\n", account.balance
end
end
فعلاً از این موضوع بگذرید که داریم مرتکب اشتباهِ تازهکارها میشویم و مقادیر ارزی را در متغیرهای اعشاری (float) ذخیره میکنیم. در عوض ببینید آیا میتوانید دوبارهکاریها را در این کد پیدا کنید. (ما حداقل سه مورد را میبینیم، اما شما ممکن است بیشتر ببینید.)
چه پیدا کردید؟ لیست ما اینجاست.
اول، واضح است که یک کپی-پیست در مدیریت اعداد منفی وجود دارد. میتوانیم با اضافه کردن یک تابع دیگر آن را اصلاح کنیم:
def format_amount(value)
result = sprintf("%10.2f", value.abs)
if value < 0
result + "-"
else
result + " "
end
end
def print_balance(account)
printf "Debits: %10.2f\n", account.debits
printf "Credits: %10.2f\n", account.credits
printf "Fees: %s\n", format_amount(account.fees)
printf " ———-\n"
printf "Balance: %s\n", format_amount(account.balance)
end
دوبارهکاری دیگر، تکرارِ پهنای فیلد (field width) در تمام فراخوانیهای printf است. میتوانستیم با معرفی یک ثابت و پاس دادن آن به هر فراخوانی، این را اصلاح کنیم، اما چرا از تابع موجود استفاده نکنیم؟
def format_amount(value)
result = sprintf("%10.2f", value.abs)
if value < 0
result + "-"
else
result + " "
end
end
def print_balance(account)
printf "Debits: %s\n", format_amount(account.debits)
printf "Credits: %s\n", format_amount(account.credits)
printf "Fees: %s\n", format_amount(account.fees)
printf " ———-\n"
printf "Balance: %s\n", format_amount(account.balance)
end
چیز دیگری هم هست؟ خب، اگر مشتری بخواهد یک فضای خالیِ اضافه بین برچسبها و اعداد باشد چه؟ مجبوریم پنج خط را تغییر دهیم. بیایید آن تکرار را هم حذف کنیم:
def format_amount(value)
result = sprintf("%10.2f", value.abs)
if value < 0
result + "-"
else
result + " "
end
end
def print_line(label, value)
printf "%-9s%s\n", label, value
end
def report_line(label, amount)
print_line(label + ":", format_amount(amount))
end
def print_balance(account)
report_line("Debits", account.debits)
report_line("Credits", account.credits)
report_line("Fees", account.fees)
print_line("", "———-")
report_line("Balance", account.balance)
end
اگر مجبور باشیم فرمتبندیِ مبالغ را تغییر دهیم، format_amount را تغییر میدهیم. اگر بخواهیم فرمت برچسب را تغییر دهیم، report_line را تغییر میدهیم.
هنوز یک نقضِ ضمنیِ DRY وجود دارد: تعداد خطتیرهها در خط جداکننده، به پهنای فیلدِ مبلغ وابسته است. اما این تطابق دقیق نیست: در حال حاضر یک کاراکتر کوتاهتر است، بنابراین هر علامت منفیِ انتهایی از ستون بیرون میزند. این نیت مشتری است، و این نیتی متفاوت از فرمتبندی واقعی مبالغ است.
هر تکرارِ کدی، تکرارِ دانش نیست
به عنوان بخشی از اپلیکیشن سفارش آنلاین نوشیدنی، شما سن کاربر را همراه با تعدادی که سفارش میدهند دریافت و اعتبارسنجی میکنید. طبق گفته صاحب سایت، هر دو باید عدد باشند و هر دو بزرگتر از صفر. پس شما اعتبارسنجیها را مینویسید:
def validate_age(value):
validate_type(value, :integer)
validate_min_integer(value, 0)
def validate_quantity(value):
validate_type(value, :integer)
validate_min_integer(value, 0)
در طول بازبینی کد (Code Review)، «عقلکلِ» تیم به این کد گیر میدهد و ادعا میکند که این نقض قانون DRY است: بدنهی هر دو تابع یکسان است.
آنها اشتباه میکنند. کد یکسان است، اما دانشی که بازنمایی میکنند متفاوت است. این دو تابع دو چیز جداگانه را اعتبارسنجی میکنند که از قضا قوانین یکسانی دارند. این یک تصادف است، نه یک دوبارهکاری.
دوبارهکاری در مستندات
معلوم نیست چطور، اما این افسانه متولد شد که باید برای تمام توابع خود کامنت (توضیحات) بنویسید. کسانی که به این جنون باور دارند، سپس چیزی شبیه به این تولید میکنند:
# Calculate the fees for this account.
#
# * Each returned check costs $20
# * If the account is in overdraft for more than 3 days,
# charge $10 for each day
# * If the average account balance is greater that $2,000
# reduce the fees by 50%
def fees(a)
f = 0
if a.returned_check_count > 0
f += 20 * a.returned_check_count
end
if a.overdraft_days > 3
f += 10*a.overdraft_days
end
if a.average_balance > 2_000
f /= 2
end
f
end
نیت این تابع دو بار بیان شده است: یک بار در کامنت و بار دیگر در کد. مشتری یک کارمزد را تغییر میدهد، و ما باید هر دو را بهروزرسانی کنیم. با گذشت زمان، تقریباً میتوانیم تضمین کنیم که کامنت و کد با هم ناهماهنگ خواهند شد.
از خودتان بپرسید کامنت چه چیزی به کد اضافه میکند؟ از دیدگاه ما، صرفاً دارد نامگذاری و چیدمانِ بد را جبران میکند. این یکی چطور است:
def calculate_account_fees(account)
fees = 20 * account.returned_check_count
fees += 10 * account.overdraft_days
if account.overdraft_days > 3
fees /= 2
end
if account.average_balance > 2_000
fees
end
end
نام تابع میگوید چه کار میکند، و اگر کسی نیاز به جزئیات داشته باشد، در سورسکد مشخص شده است. این یعنی DRY!
نقض DRY در دادهها
ساختارهای دادهی ما بازنمای دانش هستند و میتوانند دچار نقض اصل DRY شوند. بیایید به کلاسی نگاه کنیم که یک خط را نمایش میدهد:
class Line {
Point start;
Point end;
double length;
};
در نگاه اول، این کلاس معقول به نظر میرسد. یک خط قطعاً یک شروع و پایان دارد، و همیشه یک طول خواهد داشت (حتی اگر صفر باشد). اما ما دوبارهکاری داریم. طول توسط نقاط شروع و پایان تعریف میشود: یکی از نقاط را تغییر دهید و طول تغییر میکند. بهتر است طول را به یک فیلد محاسبهشدنی تبدیل کنیم:
class Line {
Point start;
Point end;
double length() { return start.distanceTo(end); }
};
بعدها در فرآیند توسعه، ممکن است تصمیم بگیرید به دلایل پرفورمنس (کارایی) اصل DRY را نقض کنید. این اتفاق معمولاً زمانی رخ میدهد که نیاز دارید دادهها را کَش (Cache) کنید تا از تکرار عملیاتهای سنگین جلوگیری کنید. ترفند کار این است که اثر آن را محدود (Localize) کنید. این نقض قانون نباید به دنیای بیرون درز کند: تنها متدهای درون کلاس باید نگران هماهنگ نگه داشتن چیزها باشند:
class Line {
private double length;
private Point start;
private Point end;
public Line(Point start, Point end) {
this.start = start;
this.end = end;
calculateLength();
}
// public void setStart(Point p) {
this.start = p;
calculateLength();
}
void setEnd(Point p) {
this.end = p;
calculateLength();
}
Point getStart() { return start; }
Point getEnd() { return end; }
double getLength() { return length; }
private void calculateLength() {
this.length = start.distanceTo(end);
}
};
این مثال همچنین یک مسئله مهم را نشان میدهد: هرگاه یک ماژول یک ساختار داده را در معرض دید قرار میدهد (Expose میکند)، شما تمام کدهایی که از آن ساختار استفاده میکنند را به پیادهسازیِ آن ماژول وابسته (Couple) میکنید.
هرجا ممکن است، همیشه از توابع دسترسی (Accessor functions) برای خواندن و نوشتن ویژگیهای اشیاء استفاده کنید. این کار اضافه کردن قابلیتها در آینده را آسانتر میکند.
این استفاده از توابع دسترسی با «اصل دسترسی یکنواخت» مایر (Meyer’s Uniform Access principle) که در کتاب Object-Oriented Software Construction [Mey97] توصیف شده، همخوانی دارد؛ اصلی که بیان میکند: تمام خدماتی که توسط یک ماژول ارائه میشوند باید از طریق یک نمادگذاریِ یکنواخت در دسترس باشند، که لو ندهند آیا از طریق ذخیرهسازی پیادهسازی شدهاند یا از طریق محاسبات.
دوبارهکاریِ بازنمودی (Representational Duplication)
کد شما با دنیای بیرون رابط دارد: کتابخانههای دیگر از طریق APIها، سرویسهای دیگر از طریق فراخوانیهای راه دور، دادهها در منابع خارجی و غیره. و تقریباً هر بار که این کار را میکنید، نوعی نقض DRY را معرفی میکنید: کد شما باید دانشی را داشته باشد که در آن موجودیتِ خارجی هم وجود دارد. باید API، یا اسکیمای داده، یا معنی کدهای خطا، یا هر چیز دیگری را بداند.
تکرار در اینجا این است که دو چیز (کد شما و موجودیت خارجی) باید دانشی از بازنمایی (Representation) رابطشان داشته باشند. آن را در یک طرف تغییر دهید، و طرف دیگر میشکند. این تکرار اجتنابناپذیر است، اما میتواند کاهش یابد. در اینجا چند استراتژی وجود دارد.
دوبارهکاری در APIهای داخلی
برای APIهای داخلی، دنبال ابزارهایی بگردید که به شما اجازه میدهند API را در نوعی فرمت خنثی مشخص کنید. این ابزارها معمولاً مستندات، APIهای ماک (Mock)، تستهای عملکردی و کلاینتهای API را تولید میکنند (مورد آخر به زبانهای مختلف). در حالت ایدهآل، این ابزار تمام APIهای شما را در یک مخزن مرکزی ذخیره میکند و اجازه میدهد بین تیمها به اشتراک گذاشته شوند.
دوبارهکاری در APIهای خارجی
[۱۵] بهطور فزایندهای، خواهید دید که APIهای عمومی به طور رسمی با استفاده از چیزی مثل OpenAPI مستند شدهاند. این به شما اجازه میدهد مشخصات (Spec) آن API را به ابزارهای API محلی خود وارد کنید و با قابلیت اطمینان بیشتری با سرویس یکپارچه شوید. اگر نمیتوانید چنین مشخصاتی را پیدا کنید، به ایجاد و انتشار یکی فکر کنید. نه تنها دیگران آن را مفید خواهند یافت؛ بلکه ممکن است حتی در نگهداری آن کمک دریافت کنید.
دوبارهکاری با منابع داده (Data Sources)
بسیاری از منابع داده به شما اجازه میدهند که روی اسکیمای دادهی آنها دروننگری (Introspection) کنید. این میتواند برای حذف بخش زیادی از دوبارهکاریِ بین آنها و کدِ شما استفاده شود. به جای اینکه دستی کدی بنویسید که حاوی این دادههای ذخیره شده باشد، میتوانید ظروف (Containers) نگهدارندهی داده را مستقیماً از روی اسکیما تولید کنید. بسیاری از فریمورکهای Persistence (لایه ماندگاری داده) این کار سنگین را برای شما انجام میدهند.
گزینه دیگری هم هست، و گزینهای که ما اغلب ترجیح میدهیم. به جای نوشتن کدی که دادههای خارجی را در یک ساختار ثابت (مثلاً نمونهای از یک struct یا class) نمایش میدهد، فقط آن را در یک ساختار دادهی کلید/مقدار بریزید (زبان شما ممکن است به آن map، hash، dictionary یا حتی object بگوید). به خودی خود این کار پرخطر است: شما امنیتِ دانستنِ اینکه دقیقاً با چه دادهای کار میکنید را از دست میدهید. بنابراین توصیه میکنیم یک لایه دوم به این راهکار اضافه کنید: یک مجموعه اعتبارسنجیِ سادهی مبتنی بر جدول که تایید کند Mapی که ساختهاید حداقل حاوی دادههایی که نیاز دارید هست، و با فرمتی که نیاز دارید. ابزار مستندسازی API شما ممکن است بتواند این را تولید کند.
دوبارهکاریِ بینِ توسعهدهندگان (Interdeveloper Duplication)
شاید سختترین نوعِ دوبارهکاری برای تشخیص و مدیریت، بین توسعهدهندگان مختلف در یک پروژه رخ دهد. کلِ مجموعههایی از عملکردها ممکن است ناخواسته تکرار شوند، و آن تکرار ممکن است سالها کشفنشده باقی بماند و منجر به مشکلات نگهداری شود.
ما دستاول شنیدیم که سیستمهای کامپیوتری دولتی یکی از ایالتهای آمریکا برای سازگاری با مشکل سال ۲۰۰۰ (Y2K) بررسی شدند. ممیزی بیش از ۱۰,۰۰۰ برنامه را پیدا کرد که هر کدام حاوی نسخهی متفاوتی از کدِ اعتبارسنجیِ «شماره تأمین اجتماعی» بودند.
در سطح بالا، با ساختن تیمی قوی و منسجم با ارتباطات خوب با این مشکل برخورد کنید.
با این حال، در سطح ماژول، مشکل موذیانهتر است. عملکردها یا دادههای مورد نیازِ عمومی که در یک حوزه مسئولیتِ مشخص قرار نمیگیرند، ممکن است بارها و بارها پیادهسازی شوند. ما احساس میکنیم بهترین راه برای مقابله با این موضوع، تشویق به ارتباط فعال و مکرر بین توسعهدهندگان است.
شاید یک جلسه روزانه اسکرام (Standup) برگزار کنید. تالارهای گفتگویی (مانند کانالهای Slack) برای بحث درباره مشکلات مشترک راه بیندازید. این روش راهی غیرمخل برای ارتباط فراهم میکند—حتی بین سایتهای کاری متعدد—در حالی که تاریخچهای دائمی از هر چه گفته شده حفظ میکند.
یکی از اعضای تیم را به عنوان «کتابدار پروژه» منصوب کنید، که شغلش تسهیل تبادل دانش باشد. مکانی مرکزی در درختِ سورسکد داشته باشید که روتینهای کاربردی (Utility) و اسکریپتها بتوانند در آن قرار گیرند. و خود را مقید کنید که سورسکد و مستندات دیگران را بخوانید، چه غیررسمی و چه در طول بازبینی کد (Code Review). شما فضولی نمیکنید—دارید از آنها یاد میگیرید. و یادتان باشد، این دسترسی دوطرفه است—اگر دیگران هم کد شما را زیر و رو کردند، ناراحت نشوید.
نکته ۱۶: استفادهی مجدد را آسان کنید.
آنچه تلاش دارید انجام دهید، پرورش محیطی است که در آن پیدا کردن و استفادهی مجدد از چیزهای موجود آسانتر از نوشتنِ خودتان باشد. اگر آسان نباشد، مردم انجامش نخواهند داد. و اگر در استفادهی مجدد شکست بخورید، ریسکِ تکرارِ دانش را پذیرفتهاید.
بخشهای مرتبط شامل
- مبحث ۸: جوهرهی طراحی خوب
- مبحث ۲۸: کاهش وابستگی (Decoupling)
- مبحث ۳۲: پیکربندی (Configuration)
- مبحث ۳۸: برنامهنویسی بر اساس تصادف
- مبحث ۴۰: ریفکتورینگ (بازسازی کد)
مبحث ۱۰: تعامد (Orthogonality)
«تعامد» (Orthogonality) مفهومی حیاتی است اگر میخواهید سیستمهایی تولید کنید که طراحی، ساخت، تست و توسعه آنها آسان باشد. با این حال، مفهوم تعامد به ندرت به صورت مستقیم آموزش داده میشود. اغلب، این ویژگی به صورت ضمنی در دلِ سایر متدها و تکنیکهایی است که یاد میگیرید. این یک اشتباه است. وقتی یاد بگیرید اصل تعامد را مستقیماً به کار ببرید، متوجه بهبود فوری در کیفیت سیستمهایی که تولید میکنید خواهید شد.
تعامد چیست؟
«تعامد» اصطلاحی است که از هندسه وام گرفته شده است. دو خط متعامد هستند اگر با زاویه قائمه یکدیگر را قطع کنند، مانند محورها در یک نمودار. به زبان برداری، این دو خط مستقل هستند. همانطور که عدد ۱ در نمودار به سمت شمال حرکت میکند، میزانِ شرق یا غرب بودنِ آن تغییر نمیکند. عدد ۲ به سمت شرق حرکت میکند، اما نه به شمال یا جنوب.
در علوم کامپیوتر، این اصطلاح به معنای نوعی استقلال یا کاهش وابستگی (Decoupling) به کار میرود. دو یا چند چیز متعامد هستند اگر تغییرات در یکی بر دیگری تأثیر نگذارد. در یک سیستم با طراحی خوب، کدِ پایگاه داده نسبت به رابط کاربری (UI) متعامد خواهد بود: شما میتوانید رابط کاربری را بدون تأثیر بر دیتابیس تغییر دهید، و دیتابیسها را بدون تغییر رابط کاربری تعویض کنید.
قبل از اینکه به مزایای سیستمهای متعامد بپردازیم، بیایید ابتدا به سیستمی نگاه کنیم که متعامد نیست.
یک سیستم غیرمتعامد
شما در یک تور هلیکوپتری بر فراز گرند کانیون هستید که خلبان، که اشتباهِ فاحشِ خوردن ماهی برای ناهار را مرتکب شده، ناگهان ناله میکند و از هوش میرود. خوشبختانه، او شما را در حالت معلق ۱۰۰ فوت بالاتر از زمین رها کرده است.
از شانس خوبتان، شب قبل یک صفحه ویکیپدیا درباره هلیکوپترها خوانده بودید. میدانید که هلیکوپترها چهار کنترل اصلی دارند. اهرم کنترل (Cyclic) دستهای است که در دست راست نگه میدارید. آن را حرکت دهید، و هلیکوپتر در جهت متناظر حرکت میکند. دست چپ شما اهرم گام جمعی (Collective Pitch Lever) را نگه میدارد. این را بالا بکشید و زاویه همه پرهها را افزایش میدهید که نیروی بالابر (Lift) تولید میکند. در انتهای اهرم گام، تراتل (گاز) قرار دارد. در نهایت دو پدال پایی دارید که میزان رانش روتور دُم را تغییر میدهند و به چرخیدن هلیکوپتر کمک میکنند.
با خود فکر میکنید: «آسان است! به آرامی اهرم گام جمعی را پایین بیاور و مثل یک قهرمان با وقار روی زمین فرود بیا.»
اما وقتی امتحان میکنید، متوجه میشوید که زندگی به این سادگی نیست. دماغه هلیکوپتر پایین میافتد و شروع به چرخش مارپیچی به سمت چپ میکنید. ناگهان کشف میکنید که دارید با سیستمی پرواز میکنید که هر ورودیِ کنترل، اثرات ثانویه دارد. اهرم دست چپ را پایین بیاورید و مجبورید حرکت جبرانی به عقب را به دسته راست اضافه کنید و پدال راست را فشار دهید. اما بعد هر کدام از این تغییرات دوباره روی تمام کنترلهای دیگر اثر میگذارد.
ناگهان دارید با یک سیستم باورنکردنی پیچیده دست و پنجه نرم میکنید، جایی که هر تغییر بر تمام ورودیهای دیگر تأثیر میگذارد. بار کاری شما خارقالعاده است: دستها و پاهایتان مدام در حرکتند و تلاش میکنند تمام نیروهای در تعامل را متعادل کنند. کنترلهای هلیکوپتر قطعاً متعامد نیستند.
مزایای تعامد
همانطور که مثال هلیکوپتر نشان میدهد، سیستمهای غیرمتعامد ذاتاً برای تغییر و کنترل پیچیدهتر هستند. وقتی اجزای هر سیستمی به شدت به هم وابسته باشند، چیزی به نام «اصلاحِ محلی» (Local Fix) وجود ندارد.
نکته ۱۷: اثرات جانبی بین چیزهای نامرتبط را حذف کنید.
ما میخواهیم اجزایی طراحی کنیم که خودکفا (Self-contained) باشند: مستقل، و با یک هدف واحد و بهخوبی تعریفشده (چیزی که یوردون و کنستانتین در طراحی ساختیافته [YC79] به آن چسبندگی یا Cohesion میگویند).
وقتی اجزا از یکدیگر ایزوله باشند، میدانید که میتوانید یکی را بدون نگرانی درباره بقیه تغییر دهید. تا زمانی که رابطهای خارجیِ آن جزء را تغییر ندهید، میتوانید مطمئن باشید که مشکلاتی ایجاد نمیکنید که در کل سیستم موج بزند.
اگر سیستمهای متعامد بنویسید، دو مزیت اصلی به دست میآورید: افزایش بهرهوری و کاهش ریسک.
افزایش بهرهوری
- تغییرات محلی میشوند، بنابراین زمان توسعه و زمان تست کاهش مییابد. نوشتن اجزای نسبتاً کوچک و خودکفا آسانتر از نوشتن یک بلوک کد بزرگ و یکپارچه است.
- اجزای ساده میتوانند طراحی، کدنویسی و تست شوند و سپس فراموش شوند—نیازی نیست همزمان با افزودن کد جدید، کدِ موجود را مدام تغییر دهید.
- رویکرد متعامد استفاده مجدد را ترویج میدهد. اگر اجزا مسئولیتهای مشخص و بهخوبی تعریفشده داشته باشند، میتوانند با اجزای جدید به روشهایی ترکیب شوند که توسط پیادهسازان اصلیشان پیشبینی نشده بود.
- هرچه سیستمهای شما وابستگی کمتری داشته باشند (Loosely Coupled)، پیکربندی مجدد و مهندسی مجدد آنها آسانتر است.
یک افزایش بهرهوریِ نسبتاً ظریف هم هنگام ترکیب اجزای متعامد وجود دارد. فرض کنید یک جزء کارهای M را انجام میدهد و دیگری کارهای N. اگر آنها متعامد باشند و ترکیبشان کنید، نتیجه کارهای M × N را انجام میدهد. اما اگر دو جزء متعامد نباشند، همپوشانی وجود خواهد داشت و نتیجه کمتر خواهد بود. شما با ترکیب اجزای متعامد، عملکرد (Functionality) بیشتری به ازای واحد تلاش دریافت میکنید.
کاهش ریسک
رویکرد متعامد ریسکهای ذاتیِ هر توسعهای را کاهش میدهد.
- بخشهای بیمار کد ایزوله میشوند. اگر یک ماژول بیمار باشد، احتمال کمتری دارد که علائم را به بقیه سیستم پخش کند. همچنین بریدن و دور انداختنِ آن و پیوند زدنِ چیزی جدید و سالم آسانتر است.
- سیستم حاصل کمتر شکننده است. تغییرات و اصلاحات کوچک را در یک ناحیه خاص اعمال کنید؛ هر مشکلی که ایجاد کنید به همان ناحیه محدود خواهد شد.
- یک سیستم متعامد احتمالاً بهتر تست خواهد شد، زیرا طراحی و اجرای تستها روی اجزای آن آسانتر خواهد بود.
- شما به شدتِ قبل به یک فروشنده، محصول یا پلتفرم خاص وابسته نخواهید بود، زیرا رابطهای این اجزای شخص ثالث (Third-party) به بخشهای کوچکتری از کل توسعه محدود خواهد شد.
بیایید به برخی از روشهایی که میتوانید اصل تعامد را در کار خود به کار ببرید نگاه کنیم.
طراحی (Design)
بیشتر توسعهدهندگان با نیاز به طراحی سیستمهای متعامد آشنا هستند، اگرچه ممکن است از کلماتی مثل ماژولار (Modular)، مبتنی بر کامپوننت (Component-based) و لایهبندی شده (Layered) برای توصیف فرآیند استفاده کنند. سیستمها باید از مجموعهای از ماژولهای همکاریکننده تشکیل شوند که هر کدام عملکردی مستقل از دیگران را پیادهسازی میکنند.
گاهی اوقات این اجزا در لایهها سازماندهی میشوند که هر کدام سطحی از انتزاع (Abstraction) را فراهم میکنند. این رویکرد لایهبندی راه قدرتمندی برای طراحی سیستمهای متعامد است. از آنجا که هر لایه فقط از انتزاعهای ارائهشده توسط لایههای زیرین خود استفاده میکند، شما انعطافپذیری زیادی در تغییر پیادهسازیهای زیرین بدون تأثیر بر کد دارید. لایهبندی همچنین ریسکِ وابستگیهایِ مهارنشدنی بین ماژولها را کاهش میدهد.
[۱۶] یک آزمون آسان برای طراحی متعامد وجود دارد. وقتی نقشهی اجزای خود را کشیدید، از خود بپرسید: اگر نیازمندیهای پشتِ یک تابع خاص را بهطور چشمگیری تغییر دهم، چند ماژول تحت تأثیر قرار میگیرند؟ در یک سیستم متعامد، پاسخ باید «یکی» باشد.
جابهجا کردن یک دکمه روی پنل GUI نباید نیازمند تغییر در اسکیمای پایگاه داده باشد. افزودن راهنمای حساس به متن (Context sensitive help) نباید زیرسیستم صورتحساب را تغییر دهد.
بیایید یک سیستم پیچیده برای نظارت و کنترل یک تاسیسات گرمایشی را در نظر بگیریم. نیازمندی اصلی خواستار یک رابط کاربری گرافیکی بود، اما نیازمندیها تغییر کردند تا یک رابط موبایل اضافه شود که به مهندسان اجازه دهد مقادیر کلیدی را رصد کنند. در یک سیستم با طراحی متعامد، شما باید فقط ماژولهای مرتبط با رابط کاربری را برای مدیریت این موضوع تغییر دهید: منطقِ زیرینِ کنترلِ تاسیسات دستنخورده باقی میماند. در واقع، اگر سیستم خود را با دقت ساختاردهی کنید، باید بتوانید از هر دو رابط با همان کد بیسِ زیرین (Code base) پشتیبانی کنید.
همچنین از خود بپرسید طراحی شما چقدر از تغییرات دنیای واقعی جدا (Decoupled) است. آیا از شماره تلفن به عنوان شناسه مشتری استفاده میکنید؟ چه اتفاقی میافتد وقتی شرکت مخابرات پیششمارهها را تغییر دهد؟ کدهای پستی، شمارههای تأمین اجتماعی یا شناسههای دولتی، آدرسهای ایمیل و دامنهها همگی شناسههای خارجی هستند که شما کنترلی روی آنها ندارید و میتوانند هر زمانی به هر دلیلی تغییر کنند. به ویژگیهای چیزهایی که نمیتوانید کنترل کنید وابسته نشوید.
جعبهابزارها و کتابخانهها (Toolkits and Libraries)
هنگام معرفی جعبهابزارها و کتابخانههای شخص ثالث (Third-party)، مراقب حفظ تعامد سیستم خود باشید. تکنولوژیهای خود را عاقلانه انتخاب کنید.
وقتی یک جعبهابزار (یا حتی کتابخانهای از دیگر اعضای تیمتان) را وارد میکنید، از خود بپرسید آیا تغییراتی را بر کد شما تحمیل میکند که نباید آنجا باشند؟ اگر یک مکانیزمِ ماندگاریِ شیء (Object persistence) شفاف باشد، پس متعامد است. اگر شما را ملزم میکند که اشیاء را به روشی خاص بسازید یا دسترسی پیدا کنید، پس متعامد نیست. ایزوله نگه داشتن چنین جزئیاتی از کدتان، مزیت اضافه کردنِ امکان تغییر فروشنده در آینده را نیز دارد.
سیستم Enterprise Java Beans (EJB) نمونه جالبی از تعامد است. در اکثر سیستمهای تراکنشمحور، کدِ اپلیکیشن باید شروع و پایان هر تراکنش را تعیین کند. با EJB، این اطلاعات به صورت اعلانی (Declarative) با Annotationها، بیرون از متدهایی که کار را انجام میدهند، بیان میشود. همان کدِ اپلیکیشن میتواند در محیطهای تراکنشیِ مختلفِ EJB بدون تغییر اجرا شود.
به نوعی، EJB نمونهای از الگوی دکوراتور (Decorator Pattern) است: اضافه کردن عملکرد به چیزها بدون تغییر دادن آنها. این سبک برنامهنویسی میتواند تقریباً در هر زبان برنامهنویسی استفاده شود و لزوماً به یک فریمورک یا کتابخانه نیاز ندارد. فقط کمی انضباط در هنگام برنامهنویسی میطلبد.
کدنویسی (Coding)
هر بار که کد مینویسید، ریسکِ کاهشِ تعامدِ اپلیکیشن خود را میپذیرید. مگر اینکه دائماً نه تنها کاری را که انجام میدهید، بلکه بافتار (Context) بزرگتر اپلیکیشن را نیز رصد کنید، ممکن است ناخواسته عملکردی را در ماژول دیگری تکرار کنید، یا دانشِ موجود را دو بار بیان کنید.
چندین تکنیک وجود دارد که میتوانید برای حفظ تعامد استفاده کنید:
- کد خود را کموابسته (Decoupled) نگه دارید. کدِ خجالتی (Shy code) بنویسید—ماژولهایی که هیچ چیزِ غیرضروری را به دیگر ماژولها فاش نمیکنند و به پیادهسازیِ دیگر ماژولها تکیه نمیکنند. قانون دِمتر (Law of Demeter) را امتحان کنید که در مبحث ۲۸، کاهش وابستگی درباره آن بحث میکنیم. اگر نیاز دارید وضعیتِ (State) یک شیء را تغییر دهید، از خودِ شیء بخواهید این کار را برایتان انجام دهد. اینگونه کد شما از پیادهسازیِ کدِ دیگر ایزوله میماند و شانس متعامد ماندنِ شما افزایش مییابد.
- از دادههای سراسری (Global Data) اجتناب کنید. هر بار که کد شما به دادهی سراسری ارجاع میدهد، خود را به اجزای دیگری که آن داده را به اشتراک میگذارند گره میزند. حتی سراسریهایی که قصد دارید فقط بخوانید میتوانند منجر به دردسر شوند (مثلاً اگر ناگهان نیاز پیدا کنید کدتان را چندریسمانی (Multithreaded) کنید). به طور کلی، فهم و نگهداری کد شما آسانتر است اگر هر بافتارِ (Context) مورد نیاز را صریحاً به ماژولهای خود پاس دهید. در اپلیکیشنهای شیگرا، بافتار اغلب به عنوان پارامتر به سازندههای اشیاء (Constructors) پاس داده میشود. در کدهای دیگر، میتوانید ساختارهایی (Structures) حاوی بافتار ایجاد کنید و ارجاعهایی به آنها را دستبهدست کنید. الگوی Singleton در Design Patterns [GHJV95] راهی برای اطمینان از این است که تنها یک نمونه از یک شیءِ کلاسِ خاص وجود دارد. بسیاری از افراد از این اشیاءِ سینگلتون به عنوان نوعی متغیر سراسری استفاده میکنند. مراقب سینگلتونها باشید—آنها نیز میتوانند منجر به اتصالاتِ (Linkage) غیرضروری شوند.
- از توابع مشابه پرهیز کنید. اغلب با مجموعهای از توابع برخورد میکنید که همه شبیه هم هستند—شاید کد مشترکی در شروع و پایان دارند، اما هر کدام الگوریتم مرکزی متفاوتی دارند. کد تکراری نشانهای از مشکلات ساختاری است. برای پیادهسازی بهتر، نگاهی به الگوی استراتژی (Strategy pattern) در Design Patterns بیندازید.
عادت کنید دائماً منتقد کد خود باشید. دنبال هر فرصتی بگردید تا آن را برای بهبود ساختار و تعامدش سازماندهی مجدد کنید. این فرآیند ریفکتورینگ (Refactoring) نامیده میشود و آنقدر مهم است که بخشی را به آن اختصاص دادهایم (نگاه کنید به مبحث ۴۰، ریفکتورینگ).
تست (Testing)
سیستمی که به صورت متعامد طراحی و پیادهسازی شده، برای تست آسانتر است. چون تعاملات بین اجزای سیستم رسمی و محدود شدهاند، بخش بیشتری از تست سیستم میتواند در سطح ماژولِ فردی انجام شود. این خبر خوبی است، چون تست سطح ماژول (یا Unit Test) به مراتب برای تعیین و اجرا آسانتر از تست یکپارچگی (Integration Testing) است. در واقع، ما پیشنهاد میکنیم که این تستها به صورت خودکار به عنوان بخشی از فرآیندِ بیلدِ (Build) منظم انجام شوند (نگاه کنید به مبحث ۴۱، تست به سمت کد).
نوشتن یونیت تستها خود آزمونی جالب برای تعامد است. چه چیزی لازم است تا یک یونیت تست بیلد و اجرا شود؟ آیا مجبورید درصد بزرگی از بقیه کد سیستم را ایمپورت کنید؟ اگر چنین است، ماژولی پیدا کردهاید که بهخوبی از بقیه سیستم جدا (Decoupled) نشده است.
رفع باگ نیز زمان خوبی برای ارزیابی تعامدِ کل سیستم است. وقتی با مشکلی برخورد میکنید، ارزیابی کنید که اصلاح آن چقدر محلی (Localized) است. آیا فقط یک ماژول را تغییر میدهید، یا تغییرات در سراسر سیستم پراکنده شدهاند؟ وقتی تغییری ایجاد میکنید، آیا همه چیز را درست میکند، یا مشکلات دیگری مرموزانه سر بر میآورند؟
این فرصت خوبی است تا اتوماسیون را به کار بگیرید. اگر از سیستم کنترل نسخه (Version Control) استفاده میکنید (و پس از خواندن مبحث ۱۹، کنترل نسخه استفاده خواهید کرد)، وقتی کد را پس از تست Check-in میکنید، رفعِ باگها را تگ کنید. سپس میتوانید گزارشهای ماهانهای بگیرید که روندهای تعداد فایلهای منبعِ تحت تأثیرِ هر رفع باگ را تحلیل میکنند.
مستندات (Documentation)
شاید تعجببرانگیز باشد، اما تعامد در مستندات نیز صدق میکند. محورها در اینجا محتوا و ارائه (Presentation) هستند. در مستنداتِ واقعاً متعامد، باید بتوانید ظاهر را به طور چشمگیری تغییر دهید بدون اینکه محتوا تغییر کند. پردازشگرهای کلمه (Word processors) استایلشیتها و ماکروهایی دارند که کمک میکنند. ما شخصاً ترجیح میدهیم از یک سیستم نشانهگذاری مثل Markdown استفاده کنیم: هنگام نوشتن فقط روی محتوا تمرکز میکنیم، و ارائه را به هر ابزاری که برای رندر کردن آن استفاده میکنیم میسپاریم. [۱۷]
زندگی با تعامد
تعامد ارتباط نزدیکی با اصل DRY دارد. با DRY، شما به دنبال به حداقل رساندن دوبارهکاری درون یک سیستم هستید، در حالی که با تعامد، وابستگی متقابلِ (Interdependency) میان اجزای سیستم را کاهش میدهید.
شاید کلمهی بدقلق و زمختی باشد، اما اگر از اصل تعامد، در ترکیب نزدیک با اصل DRY استفاده کنید، خواهید دید سیستمهایی که توسعه میدهید منعطفتر، قابلفهمتر و برای دیباگ، تست و نگهداری آسانتر هستند.
اگر وارد پروژهای شدید که افراد دیوانهوار تلاش میکنند تغییرات ایجاد کنند، و جایی که هر تغییر به نظر میرسد باعث خراب شدن چهار چیز دیگر میشود، کابوس هلیکوپتر را به یاد بیاورید. آن پروژه احتمالا به صورت متعامد طراحی و کدنویسی نشده است. زمان ریفکتورینگ است.
و اگر خلبان هلیکوپتر هستید، ماهی نخورید...
بخشهای مرتبط شامل
- مبحث ۳: آنتروپی نرمافزار
- مبحث ۸: جوهرهی طراحی خوب
- مبحث ۱۱: برگشتپذیری
- مبحث ۲۸: کاهش وابستگی
- مبحث ۳۱: مالیات وراثت
- مبحث ۳۳: شکستن وابستگی زمانی
- مبحث ۳۴: وضعیت اشتراکی، وضعیت نادرست است
- مبحث ۳۶: تختهسیاهها (Blackboards)
تمرینها
تمرین ۱: تفاوت بین ابزارهایی که رابط کاربری گرافیکی (GUI) دارند و ابزارهای خط فرمان (Command-line) کوچک اما قابلترکیب در شِل (Shell) را در نظر بگیرید. کدام مجموعه متعامدتر است و چرا؟ کدام برای دقیقاً همان هدفی که برایش در نظر گرفته شده آسانتر است؟ کدام مجموعه برای ترکیب با ابزارهای دیگر جهت مواجهه با چالشهای جدید آسانتر است؟ کدام مجموعه برای یادگیری آسانتر است؟
تمرین ۲: سیپلاسپلاس (C++) از وراثت چندگانه پشتیبانی میکند، و جاوا به یک کلاس اجازه میدهد چندین اینترفیس را پیادهسازی کند. روبی Mixinها را دارد. استفاده از این امکانات چه تأثیری بر تعامد دارد؟ آیا تفاوتی در تأثیرِ استفاده از وراثت چندگانه و اینترفیسهای چندگانه وجود دارد؟ آیا تفاوتی بین استفاده از نمایندگی (Delegation) و استفاده از وراثت وجود دارد؟
(پاسخ احتمالی): از شما خواسته شده فایلی را خط به خط بخوانید. برای هر خط، باید آن را به فیلدها تقسیم کنید. کدامیک از مجموعههای تعاریف شبهکلاس (Pseudo class) زیر احتمالا متعامدتر است؟
class Split1 {
constructor(fileName) # فایل را برای خواندن باز میکند
def readNextLine() # به خط بعدی میرود
def getField(n) # انامین فیلد در خط جاری را برمیگرداند
}
یا
class Split2 {
constructor(line) # یک خط را تقسیم میکند
def getField(n) # انامین فیلد در خط جاری را برمیگرداند
}
تمرین ۳: چه تفاوتهایی در تعامد بین زبانهای شیگرا و تابعی (Functional) وجود دارد؟ آیا این تفاوتها ذاتیِ خود زبانها هستند، یا فقط در نحوه استفاده مردم از آنها؟
مبحث ۱۱: برگشتپذیری (Reversibility)
«هیچ چیز خطرناکتر از یک ایده نیست، اگر آن تنها ایدهای باشد که دارید.»
— امیل آگوست شارتیه (آلن)، گفتارهایی در باب مذهب، ۱۹۳۸
مهندسان راهحلهای ساده و تکی را برای مسائل ترجیح میدهند. آزمونهای ریاضی که به شما اجازه میدهند با اعتمادبهنفس بالا اعلام کنید ۴ = ۲ + ۲، بسیار راحتتر از مقالات مبهم و گرمونرم درباره هزاران دلیلِ وقوعِ انقلاب فرانسه هستند. مدیریت معمولاً با مهندسان همنظر است: پاسخهای تکی و آسان، بهخوبی در صفحات گسترده (Spreadsheets) و برنامههای پروژه جا میگیرند.
کاش دنیای واقعی هم همکاری میکرد! متأسفانه، با اینکه امروز ۴ = ۲ + ۲ است، فردا ممکن است ۱-۵ باشد و هفته بعد چیز دیگری. هیچ چیز همیشگی نیست—و اگر به شدت به یک واقعیت تکیه کنید، تقریباً میتوانید تضمین کنید که تغییر خواهد کرد.
همیشه بیش از یک راه برای پیادهسازی چیزی وجود دارد، و معمولاً بیش از یک فروشنده (Vendor) برای ارائه یک محصول شخص ثالث در دسترس است. اگر با این تصور کوتهبینانه وارد پروژه شوید که «فقط یک راه برای انجام کار وجود دارد»، ممکن است با یک سورپرایز ناخوشایند روبرو شوید.
چشمانِ بسیاری از تیمهای پروژه، با آشکار شدن آینده، به زور باز شده است:
برنامهنویس اعتراض میکند: «اما شما گفتید ما از دیتابیس XYZ استفاده میکنیم! ما ۸۵٪ کدنویسی پروژه را تمام کردهایم، نمیتوانیم الان تغییرش دهیم!»
«متاسفم، اما شرکت ما تصمیم گرفته به جای آن روی دیتابیس PDQ استانداردسازی کند—برای تمام پروژهها. از دست من خارج است. فقط باید دوباره کد بزنیم. همه شما تا اطلاع ثانوی آخر هفتهها هم کار خواهید کرد.»
تغییرات حتماً نباید انقدر ظالمانه (Draconian) یا حتی انقدر فوری باشند. اما با گذشت زمان، و با پیشرفت پروژه، ممکن است خود را در موقعیتی غیرقابل دفاع بیابید.
با هر تصمیمِ حیاتی، تیم پروژه متعهد به هدف کوچکتری میشود—نسخهای باریکتر از واقعیت که گزینههای کمتری دارد. زمانی که بسیاری از تصمیمات حیاتی گرفته شدند، هدف آنقدر کوچک میشود که اگر حرکت کند، یا جهت باد تغییر کند، یا پروانهای در توکیو بالهایش را تکان دهد، شما به هدف نخواهید زد. و ممکن است با اختلاف زیادی هم خطا بروید. [۱۸]
مشکل اینجاست که تصمیمات حیاتی به سادگی برگشتپذیر نیستند. وقتی تصمیم میگیرید از دیتابیسِ فلان فروشنده، یا فلان الگوی معماری، یا یک مدل استقرار (Deployment) خاص استفاده کنید، متعهد به مسیری از اقدامات شدهاید که بازگرداندن آن، مگر با هزینهای گزاف، ممکن نیست.
برگشتپذیری
بسیاری از مباحث این کتاب با هدف تولید نرمافزارهای منعطف و سازگارپذیر تنظیم شدهاند. با پایبندی به توصیههای آنها—بهویژه اصل DRY، کاهش وابستگی (Decoupling) و استفاده از پیکربندی خارجی—مجبور نیستیم تصمیمات حیاتی و غیرقابلبازگشتِ زیادی بگیریم.
این چیز خوبی است، زیرا ما همیشه در بارِ اول بهترین تصمیمات را نمیگیریم. ما متعهد به یک تکنولوژی خاص میشویم، فقط برای اینکه بفهمیم نمیتوانیم افراد کافی با مهارتهای لازم را استخدام کنیم. ما با یک فروشندهی شخص ثالثِ خاص قرارداد میبندیم، درست قبل از اینکه توسط رقیبشان خریداری شوند. نیازمندیها، کاربران و سختافزار سریعتر از آنکه بتوانیم نرمافزار را توسعه دهیم تغییر میکنند.
فرض کنید در ابتدای پروژه تصمیم میگیرید از یک دیتابیس رابطهای (Relational) از فروشنده A استفاده کنید. خیلی بعدتر، در حین تست کارایی، متوجه میشوید که این دیتابیس خیلی کند است، اما دیتابیسِ سند-محور (Document database) از فروشنده B سریعتر است. در اکثر پروژههای متعارف، بدشانسی آوردهاید. اغلبِ اوقات، فراخوانیها به محصولات شخص ثالث در سرتاسر کد درهمتنیده شدهاند.
اما اگر واقعاً ایدهی «پایگاه داده» را انتزاع (Abstract) کرده باشید—تا جایی که صرفاً «ماندگاری داده» (Persistence) را به عنوان یک سرویس ارائه دهد—آنگاه این انعطاف را دارید که در میانهی رودخانه اسب خود را عوض کنید.
به طور مشابه، فرض کنید پروژه به عنوان یک اپلیکیشن مبتنی بر مرورگر شروع میشود، اما بعد، در اواخر بازی، بخش مارکتینگ تصمیم میگیرد که آنچه واقعاً میخواهند یک اپلیکیشن موبایل است. این تغییر چقدر برای شما سخت خواهد بود؟ در یک دنیای ایدهآل، نباید تأثیر زیادی روی شما بگذارد، حداقل در سمت سرور. شما باید مقداری رندر HTML را دور بریزید و آن را با یک API جایگزین کنید.
اشتباه در این فرض نهفته است که هر تصمیمی بر سنگ حک شده است—و در عدم آمادگی برای پیشامدهایی که ممکن است رخ دهند. به جای حک کردن تصمیمات بر سنگ، به آنها به چشم نوشتههایی روی شنهای ساحل نگاه کنید. یک موج بزرگ میتواند هر لحظه بیاید و آنها را پاک کند.
نکته ۱۸: هیچ تصمیم نهایی وجود ندارد.
معماری منعطف
در حالی که بسیاری از افراد سعی میکنند کد خود را منعطف نگه دارند، شما باید به حفظ انعطافپذیری در حوزههای معماری، استقرار و یکپارچهسازی با فروشندگان نیز فکر کنید.
ما این متن را در سال ۲۰۱۹ مینویسیم. از آغاز قرن تا کنون، ما شاهد «بهترین روشهای» (Best Practices) معماری سمت سرورِ زیر بودهایم:
- تکههای بزرگ آهن (سرورهای غولپیکر فیزیکی)
- فدراسیونهایی از آهنهای بزرگ
- کلاسترهای Load-balance شده از سختافزارهای معمولی
- ماشینهای مجازی ابری که اپلیکیشن اجرا میکنند
- ماشینهای مجازی ابری که سرویس اجرا میکنند
- نسخههای کانتینریشده (Containerized) از موارد بالا
- اپلیکیشنهای بدون سرور (Serverless) با پشتیبانی ابری
- و به ناچار، بازگشتی ظاهری به «تکههای بزرگ آهن» برای برخی وظایف
بفرمایید و جدیدترین و بهترین مُدها را هم به این لیست اضافه کنید، و سپس با حیرت به آن نگاه کنید: معجزه است که اصلاً چیزی کار کرده است.
چگونه میتوانید برای این نوع نوسانات معماری برنامهریزی کنید؟ نمیتوانید. کاری که میتوانید بکنید این است که تغییر را آسان کنید. APIهای شخص ثالث را پشت لایههای انتزاعی خودتان مخفی کنید. کدتان را به اجزای (Components) کوچک بشکنید: حتی اگر در نهایت آنها را روی یک سرور عظیمِ واحد مستقر کنید، این رویکرد بسیار آسانتر از آن است که یک اپلیکیشن یکپارچه (Monolithic) را بگیرید و تکهتکه کنید. (ما زخمهایش را خوردهایم که این را ثابت کنیم.)
و با اینکه این موضوع لزوماً مسئلهی برگشتپذیری نیست، یک نصیحت نهایی:
نکته ۱۹: از دنبال کردن مُدها دست بردارید.
هیچ کس نمیداند آینده چه در چنته دارد، مخصوصاً ما! پس کدتان را طوری توانمند کنید که بتواند راک-اند-رول کند: وقتی میتواند محکم باشد (Rock)، و وقتی مجبور است با ضربات بچرخد و انعطاف داشته باشد (Roll).
بخشهای مرتبط شامل
- مبحث ۸: جوهرهی طراحی خوب
- مبحث ۱۰: تعامد (Orthogonality)
- مبحث ۱۹: کنترل نسخه
- مبحث ۲۸: کاهش وابستگی
- مبحث ۴۵: چاهِ نیازمندیها
- مبحث ۵۱: کیت شروع عملگرا
چالشها
زمان کمی مکانیک کوانتوم با گربه شرودینگر:
فرض کنید یک گربه در یک جعبهی بسته دارید، به همراه یک ذره رادیواکتیو. این ذره دقیقاً ۵۰٪ شانس دارد که به دو قسمت شکافته شود. اگر شکافته شود، گربه کشته میشود. اگر نشود، گربه سالم میماند. پس، آیا گربه مرده است یا زنده؟
طبق گفته شرودینگر، پاسخ صحیح «هر دو» است (حداقل تا زمانی که جعبه بسته بماند). هر بار که یک واکنش زیرهستهای رخ میدهد که دو نتیجه ممکن دارد، جهان شبیهسازی (تکثیر) میشود. در یکی، رویداد رخ داده، در دیگری نه. گربه در یک جهان زنده است و در دیگری مرده. تنها زمانی که جعبه را باز میکنید میفهمید در کدام جهان هستید.
عجبی نیست که کدنویسی برای آینده دشوار است. اما به تکاملِ کد به همان شکلِ جعبهی پر از گربههای شرودینگر فکر کنید: هر تصمیم منجر به نسخه متفاوتی از آینده میشود. کد شما چند آیندهی ممکن را میتواند پشتیبانی کند؟ کدامیک محتملترند؟ وقتی زمانش برسد، پشتیبانی از آنها چقدر سخت خواهد بود؟
آیا جرأت دارید جعبه را باز کنید؟
موضوع ۱۲ – گلولههای ردیاب
«آماده، شلیک، هدفگیری...»
نقل از یک ناشناس
ما وقتی نرمافزار میسازیم، خیلی وقتها از «زدن به هدف» حرف میزنیم. البته در میدان تیر واقعی شلیک نمیکنیم، ولی این تشبیه خیلی مفید و تصویری است. بهخصوص جالب است که فکر کنیم چطور میشود در دنیایی پیچیده و مدام در حال تغییر، به هدف خورد.
جواب، بستگی به ابزاری دارد که با آن نشانه میگیری. با خیلی از ابزارها فقط یک شانس داری که نشانه بگیری، بعد میمانی ببینی به وسط هدف خورده یا نه. اما راه بهتری هم هست.
یادتونه تو فیلمها و سریالها و بازیهای ویدیویی، وقتی با مسلسل تیرباران میکنند، مسیر گلولهها را به صورت خطوط نورانی در هوا نشان میدهند؟ این خطوط نورانی از «گلولههای ردیاب» (Tracer Bullets) میآیند. گلولههای ردیاب را با فاصلههای منظم بین فشنگهای معمولی میگذارند. وقتی شلیک میشوند، فسفر داخلشان شعلهور میشود و یک رد نوری پیوسته از دهانه تفنگ تا جایی که میخورند، در هوا میماند. اگر ردیابها به هدف بخورند، یعنی گلولههای معمولی هم خوردهاند. سربازها از همین ردیابها استفاده میکنند تا در همان لحظه و در شرایط واقعی، نشانهگیریشان را اصلاح کنند: بازخورد عملی، لحظهای و در دل میدان جنگ.
دقیقاً همین اصل در پروژهها هم صدق میکند، بهخصوص وقتی چیزی میسازید که قبلاً کسی نساخته. ما به این روش میگوییم «توسعه با گلولههای ردیاب» (Tracer Bullet Development)؛ یک تصویر بصری قوی برای نشان دادن نیاز به «بازخورد فوری در شرایط واقعی، وقتی هدف هم مدام در حال حرکت است».
مثل تیربارچی که در تاریکی شلیک میکند. چون کاربران شما تا حالا همچین سیستمی ندیدهاند، نیازمندیهایشان مبهم است. چون شاید از الگوریتمها، تکنیکها، زبانها یا کتابخانههایی استفاده میکنید که با آنها آشنا نیستید، تعداد ناشناختههایتان زیاد است. و چون پروژه طول میکشد، تقریباً میشود تضمین کرد که تا وقتی تمامش کنید، محیط و شرایط عوض شده.
پاسخ کلاسیک این است که «سیستم را تا حد مرگ مشخصاتنویسی کنیم». صدها صفحه کاغذ پر کنیم، هر نیازمندی را میخکوب کنیم، هر ناشناخته را ببندیم، محیط را محدود کنیم. بعد با یک محاسبه بزرگ در ابتدا، شلیک کنیم و امیدوار باشیم بخورد. تیراندازی با حساب مرده (Dead Reckoning).
اما برنامهنویسان عملگرا (Pragmatic Programmer) ترجیح میدهد از معادل نرمافزاری گلولههای ردیاب استفاده کند.
کدی که در تاریکی میدرخشد
گلوله ردیاب کار میکند چون دقیقاً در همان محیط و با همان محدودیتهای گلوله واقعی عمل میکند. خیلی سریع به هدف میرسد، پس تیرانداز فوراً بازخورد میگیرد. و از نظر عملی هم راهحل ارزانی است.
برای رسیدن به همین اثر در کد، دنبال چیزی میگردیم که ما را خیلی سریع، قابل مشاهده و قابل تکرار، از یک نیازمندی به بخشی از سیستم نهایی برساند. اول نیازمندیهای مهم را پیدا کنید؛ همانهایی که هویت سیستم را تعریف میکنند. بعد جاهایی را پیدا کنید که شک دارید، جاهایی که بیشترین ریسک را میبینید. سپس توسعه را طوری اولویتبندی کنید که همین بخشها اولین چیزهایی باشند که کد میزنید.
نکته ۲۰ – از گلولههای ردیاب استفاده کن تا هدف را پیدا کنی
در واقع، با پیچیدگی پروژههای امروزی – که پر است از وابستگیهای خارجی، ابزارهای رنگارنگ و محیطهای متنوع – گلولههای ردیاب حتی مهمتر از قبل شدهاند.
برای ما، اولین گلوله ردیاب خیلی ساده است: پروژه را بساز، یک «Hello World!» بگذار، مطمئن شو که کامپایل و اجرا میشود. بعد به سراغ نقاط مبهم و پرریسک کل برنامه میرویم و اسکلت لازم را میسازیم تا حداقل یک مسیر کامل کار کند.
به این دیاگرام نگاه کن:
این سیستم پنج لایه معماری دارد. ما نگران بودیم که این لایهها چطور با هم جفتوجور شوند، پس یک فیچر ساده پیدا کردیم که همه لایهها را همزمان به کار بگیرد. خط مورب، مسیر عبور آن فیچر در کد است. برای اینکه کار کند، فقط باید بخشهای تیرهرنگ هر لایه را پیاده کنیم؛ بخشهای موجدار بعداً پر میشوند.
چند وقت پیش یک پروژه پیچیده کلاینت-سرور برای بازاریابی پایگاهدادهای داشتیم. یکی از نیازمندیها، امکان نوشتن و اجرای پرس کوئریهای زمانی (temporal queries) بود. سرورها ترکیبی از دیتابیسهای رابطهای و تخصصی بودند. رابط کاربری کلاینت با زبان A نوشته شده بود و از کتابخانههایی به زبان کاملاً متفاوت برای ارتباط با سرورها استفاده میکرد. کوئری کاربر به صورت یک نوتاسیون شبیه Lisp روی سرور ذخیره میشد و درست قبل از اجرا به SQL بهینه تبدیل میشد. همهچیز پر از ناشناخته بود، محیطها متفاوت بودند و هیچکس دقیقاً نمیدانست رابط کاربری باید چه شکلی باشد.
این موقعیت ایدهآل برای استفاده از کد ردیاب بود.
ما فریمورک فرانتاند، کتابخانههای نمایش کوئری و ساختاری برای تبدیل کوئری ذخیرهشده به کوئری مخصوص دیتابیس را ساختیم. بعد همه را به هم وصل کردیم و تست کردیم. در اولین بیلد فقط میتوانستیم یک کوئری ساده بزنیم که تمام ردیفهای یک جدول را لیست کند، اما همین کافی بود تا ثابت کنیم:
رابط کاربری میتواند با کتابخانهها حرف بزند،
کتابخانهها میتوانند کوئری را سریالایز و آنسریالایز کنند،
سرور میتواند از روی نتیجه، SQL بسازد.
در ماههای بعد، همین ساختار اولیه را به تدریج پر کردیم؛ هر وقت رابط کاربری یک نوع کوئری جدید اضافه میکرد، کتابخانه بزرگتر میشد و تولید SQL پیچیدهتر میشد. همه چیز به صورت موازی رشد میکرد.
کد ردیاب دورانداختنی نیست.
کد ردیاب را برای همیشه مینویسید. تمام بررسی خطاها، ساختاردهی، مستندات و خود-آزماییای که در کد تولیدی نهایی هست، در کد ردیاب هم هست. فقط هنوز همه فیچرها را ندارد.
اما وقتی یک مسیر کامل از ابتدا تا انتها وصل شد، میتوانید ببینید چقدر به هدف نزدیکید و اگر لازم بود، جهت را تنظیم کنید. وقتی روی هدف قفل کردید، اضافه کردن فیچرهای جدید خیلی راحت میشود.
توسعه با گلوله ردیاب با این ایده کاملاً سازگار است که «یک پروژه هیچوقت تمام نمیشود»؛ همیشه تغییر و فیچر جدید خواهد بود. این روش، رویکردی افزایشی (incremental) است.
جایگزین کلاسیک، مهندسی سنگین است: کد را به ماژولهای جدا تقسیم میکنید، هر ماژول را در خلأ مینویسید، بعد ماژولها را به زیرسیستم تبدیل میکنید، زیرسیستمها را به هم وصل میکنید تا یک روزی ناگهان یک برنامه کامل داشته باشید. تازه آن موقع میتوانید برنامه را به کاربر نشان دهید و تست کنید!
مزایای رویکرد گلوله ردیاب خیلی زیاد است:
-
کاربران زود چیزی کارکردن میبینند
اگر خوب توضیح داده باشید که این هنوز نسخه خام است (نکته ۵۲)، کاربر نه تنها ناامید نمیشود، بلکه از دیدن پیشرفت واقعی ذوق میکند و حس مالکیتش بیشتر میشود. همین کاربران به شما میگویند هر بار چقدر به هدف نزدیکتر شدهاید. -
برنامهنویسان یک ساختار آماده برای کار کردن دارند
ترسناکترین کاغذ، کاغذ سفید خالی است. وقتی تعاملات انتها-تا-انتها را در کد پیاده کرده باشید، تیم دیگر مجبور نیست همهچیز را از هیچ بسازد. بهرهوری بالا میرود و یکدستی حفظ میشود. -
یک پلتفرم یکپارچهسازی دائمی دارید
چون سیستم از ابتدا تا انتها وصل است، هر قطعه جدیدی که واحد-تست شد را میتوانید فوراً به آن اضافه کنید. به جای یکپارچهسازی «بیگبنگ» در آخر پروژه، هر روز (حتی چند بار در روز) یکپارچهسازی میکنید. تأثیر هر تغییر مشخصتر است، دیباگ و تست سریعتر و دقیقتر. -
همیشه چیزی برای دمو کردن دارید
مدیرها و اسپانسرها عادت دارند درست در بدترین لحظه بگویند «بیا یه دمو ببینیم». با کد ردیاب همیشه چیزی آماده برای نشان دادن دارید. -
حس واقعی پیشرفت دارید
در روش ردیاب، فیچر به فیچر پیش میروید. وقتی یکی تمام شد، سراغ بعدی میروید. اندازهگیری پیشرفت خیلی راحتتر است و دیگر خبری از آن ماژولهای غولپیکر «۹۵٪ کامل» هفته به هفته نیست.
گلولههای ردیاب همیشه به هدف نمیخورند – و این خوب است!
دقیقاً هدف ردیاب این است که نشان بدهد الان کجا داری میزنی. اگر نخورد، نشانهگیری را تنظیم میکنی. در کد هم همینطور است. وقتی مطمئن نیستی، اولین تلاشها ممکن است خطا بروند: کاربر بگوید «منظورم این نبود»، دادهای که لازم داری در دسترس نباشد، یا مشکل عملکردی پیش بیاید. مشکلی نیست! چون بدنه کد کوچک و سبک است، تغییرش سریع و ارزان است. بازخورد میگیری، نسخه دقیقتری میسازی و دوباره شلیک میکنی. و چون تمام اجزای اصلی سیستم از همان اول در کد ردیاب حضور دارند، کاربر مطمئن است چیزی که میبیند واقعی است، نه فقط یک مشخصات کاغذی.
تفاوت کد ردیاب با پروتوتایپ (Prototype)
شاید فکر کنی «این که همون پروتوتایپه، فقط اسمشو عوض کردن». نه، تفاوت اساسی دارد.
-
پروتوتایپ برای کاوش جنبههای خاص سیستم است و معمولاً دورانداختنی است.
مثلاً رابط کاربری را با یک ابزار سریع میسازی، کاربر تأیید میکند، بعد کلش را دور میاندازی و از نو با زبان اصلی مینویسی. یا الگوریتم پیچیده را با پایتون تست میکنی، بعد از نو با C++ پیاده میکنی. -
کد ردیاب برای این است که بفهمی کل برنامه چطور به هم وصل میشود.
یک پیادهسازی خیلی ساده (مثلاً الگوریتم «اول رسید، اول جا شد») مینویسی، رابط کاربری واقعی ولی مینیمال میسازی، همه را به هم وصل میکنی. این اسکلت میماند و بعداً پر میشود. چیزی دورانداخته نمیشود.
به عبارت دیگر:
پروتوتایپ = شناسایی و جمعآوری اطلاعات قبل از شلیک اولین گلوله ردیاب
کد ردیاب = اسکلت واقعی و دائمی سیستم که از همان اول روشن است و بزرگ میشود.
بخشهای مرتبط
موضوع ۱۳ (پروتوتایپ و یادداشتهای چسبی)، موضوع ۲۷ (نور چراغ جلو را پشت سر نگذار)، موضوع ۴۰ (رفاکتورینگ)، موضوع ۴۹ (تیمهای عملگرا)، موضوع ۵۰، ۵۱، ۵۲ (لذت دادن به کاربر) و …
حالا سؤالم از تو این است:
در پروژه فعلیات، اولین «گلوله ردیاب» چه میتوانست باشد؟ کدام مسیر انتها-تا-انتها را میتوانستی با کمترین کد، ولی به صورت واقعی، روشن کنی تا همه ببینند سیستم زنده است و جهت درست را نشان دهد؟
موضوع ۱۳ – پروتوتایپها و یادداشتهای چسبی
در خیلی از صنایع، برای تست کردن یک ایده خاص از «پروتوتایپ» استفاده میکنند؛ چون ساختن پروتوتایپ خیلی خیلی ارزانتر از تولید کامل است.
شرکتهای خودروسازی مثلاً برای یک مدل جدید، دهها پروتوتایپ مختلف میسازد: یکی فقط برای تست آیرودینامیک در تونل باد (شاید با خاک رس)، یکی با چوب بالسا و چسب نواری برای بخش طراحی ظاهری، یکی کاملاً کامپیوتری یا در واقعیت مجازی. اینطوری عناصر پرریسک یا نامطمئن را بدون اینکه به ساخت ماشین واقعی متعهد شوند، امتحان میکنند.
ما هم دقیقاً به همین دلیل و به همین شکل، در نرمافزار پروتوتایپ میسازیم: برای تحلیل و خنثی کردن ریسکها، با هزینهای بسیار کمتر و امکان اصلاح سریع.
مثل خودروسازها، ما هم میتوانیم پروتوتایپ را فقط برای تست یک یا چند جنبه خاص هدفگذاری کنیم.
اغلب فکر میکنیم پروتوتایپ حتماً باید کد باشد کد، ولی اصلاً لازم نیست. مواد ساخت پروتوتایپ میتواند هر چیزی باشد:
- یادداشتهای چسبی (Post-it) عالیاند برای پروتوتایپ کردن جریان کار (workflow)، منطق برنامه یا حتی معماری کلی.
- رابط کاربری را میشود روی وایتبرد کشید، یا با برنامه نقاشی یک ماکآپ غیرقابلکلیک ساخت، یا با ابزارهای Interface Builder یک نسخه تعاملی ولی بدون منطق واقعی درست کرد.
پروتوتایپ فقط برای پاسخ دادن به چند سؤال خاص طراحی میشود، بنابراین خیلی سریعتر و ارزانتر از برنامهای است که قرار است به تولید برود.
در کد پروتوتایپ میتوانیم تمام جزئیاتی که «الان» برای ما مهم نیستند را کاملاً نادیده بگیریم (هرچند همان جزئیات بعداً برای کاربر حیاتی خواهند بود).
مثلاً:
- اگر داریم رابط کاربری را پروتوتایپ میکنیم، نتیجه غلط یا داده تقلبی کاملاً قابل قبول است.
- اگر داریم عملکرد محاسباتی یا سرعت را تست میکنیم، رابط کاربری زشت یا حتی بدون UI هم مشکلی ندارد.
اما اگر دیدید نمیتوانید از جزئیات چشمپوشی کنید، احتمالاً دیگر در حال ساخت پروتوتایپ واقعی نیستید؛ در این صورت بهتر است سراغ روش «گلوله ردیاب» بروید (موضوع ۱۲).
چه چیزهایی را باید پروتوتایپ کنیم؟
هر چیزی که ریسک دارد. هر چیزی که تا حالا امتحان نشده، یا برای سیستم نهایی حیاتی است، یا اثباتنشده، آزمایشی یا مشکوک است. هر چیزی که با آن راحت نیستید.
بهطور مشخص میتوانید پروتوتایپ کنید:
- معماری کلی
- عملکرد جدید در یک سیستم موجود
- ساختار یا محتوای دادههای خارجی
- ابزارها یا کامپوننتهای شخص ثالث
- مسائل عملکرد (Performance)
- طراحی رابط کاربری
پروتوتایپ یک تجربه یادگیری است. ارزش واقعی آن نه در کدی که تولید میکند، بلکه در درسهایی است که از آن میگیرید. این دقیقاً جوهره پروتوتایپ کردن است.
نکته ۲۱ – پروتوتایپ بساز تا یاد بگیری
وقتی پروتوتایپ میسازیم، از چه جزئیاتی میتوانیم چشمپوشی کنیم؟
- درستی (Correctness) → میتوانید از داده تقلبی استفاده کنید.
- کامل بودن (Completeness) → ممکن است فقط با یک ورودی از پیش تعیینشده کار کند.
- استحکام (Robustness) → بررسی خطا تقریباً وجود ندارد؛ اگر از مسیر اصلی خارج شوید ممکن است با شکوه تمام کرش کند. اشکالی ندارد!
- سبک کدنویسی و مستندات → معمولاً کامنت و داکیومنت کمی دارد (هرچند ممکن است بعدش کلی داکیومنت از تجربهتان بنویسید).
پروتوتایپها جزئیات را نادیده میگیرند و فقط روی جنبه خاصی تمرکز میکنند. به همین دلیل بهتر است از زبانهای سطح بالا و سریع مثل Python، Ruby یا حتی یک اسکریپتشل استفاده کنید – زبانی که «جلوی راهتان را نگیرد». میتوانید بعداً تصمیم بگیرید همان زبان را ادامه دهید یا عوض کنید؛ به هر حال قرار است پروتوتایپ را دور بیندازید.
برای رابط کاربری از ابزارهایی استفاده کنید که فقط به ظاهر و تعامل بپردازند، نه کد واقعی.
اسکریپتنویسی همچنین عالی است برای «چسباندن» کامپوننتهای سطح پایین به هم و دیدن اینکه ترکیب جدید چطور کار میکند.
پروتوتایپ معماری
خیلی از پروتوتایپها برای مدل کردن کل سیستم ساخته میشوند. برخلاف گلوله ردیاب، هیچکدام از ماژولها لازم نیست واقعاً کار کند. حتی ممکن است اصلاً کد نزنید و فقط روی وایتبرد یا با کارتهای فیش یا Post-it کار کنید. هدف این است که ببینید سیستم بهطور کلی چطور به هم وصل میشود و جزئیات را به تعویق بیندازید.
سؤالهای خوبی که در پروتوتایپ معماری باید جواب بدهید:
- آیا مسئولیتهای بخشهای اصلی بهخوبی تعریف شده و مناسب است؟
- آیا همکاری بین کامپوننتهای اصلی شفاف است؟
- آیا کوپلینگ حداقل است؟
- آیا نقاط احتمالی تکرار کد مشخص شدهاند؟
- آیا تعریف رابطها و محدودیتها قابل قبول است؟
- آیا هر ماژول در زمان اجرا به دادهای که لازم دارد دسترسی دارد – و دقیقاً وقتی لازم دارد؟
آخرین مورد معمولاً بیشترین غافلگیری و ارزشمندترین نتیجه را از تجربه پروتوتایپ به همراه دارد.
چطور نباید از پروتوتایپ استفاده کرد
قبل از اینکه حتی یک خط کد پروتوتایپ بنویسید، مطمئن شوید همه (همه!) میدانند که این کد دورانداختنی است.
پروتوتایپ برای کسانی که نمیدانند فقط پروتوتایپ است، به طرز فریبندهای جذاب به نظر میرسد. اگر انتظارات را درست تنظیم نکنید، ممکن است مدیر یا اسپانسر بگوید «همین رو بفرستیم تولید!»
یادشان بیاورید که میشود یک ماشین فوقالعاده را با چوب بالسا و چسب نواری پروتوتایپ کرد، ولی هیچکس با آن در ترافیک ساعت شلوغی رانندگی نمیکند!
اگر در محیط یا فرهنگ سازمانتان احتمال سوءتفاهم بالاست، بهتر است مستقیم سراغ روش گلوله ردیاب بروید تا از همان اول یک اسکلت محکم و غیرقابلانکار داشته باشید.
اگر درست استفاده شوند، پروتوتایپها میتوانند صدها ساعت زمان، میلیونها تومان پول و کلی دردسر را با شناسایی زودهنگام مشکلات، صرفهجویی کنند – دقیقاً وقتی که اصلاح اشتباه هنوز ارزان و آسان است.
بخشهای مرتبط
موضوع ۱۲ (گلولههای ردیاب)، ۱۴، ۱۷، ۲۷ (نور چراغ جلو را پشت سر نگذار)، ۳۷، ۴۵، ۵۲ (لذت دادن به کاربر) و …
تمرین ۳ (پاسخ پیشنهادی)
بخش بازاریابی میخواهد همین الان بنشیند و چند طرح صفحه وب را با هم brainstorm کند. ایدهشان نقشه تصویری قابلکلیک است که به صفحات دیگر برود، ولی هنوز نمیدانند تصویر اصلی ماشین باشد یا تلفن یا خانه. شما لیست صفحات هدف و محتواها را دارید و فقط ۱۵ دقیقه وقت دارید چند پروتوتایپ نشان بدهید.
بهترین ابزارها در این ۱۵ دقیقه چیست؟
حالا نوبت توست:
اگر همین الان فقط ۱۰-۱۵ دقیقه وقت داشتی، با چه ابزاری میتوانستی چند مدل مختلف از این صفحه را به بازاریابی نشان بدهی که هم سریع باشد، هم کاملاً قابل فهم، و هم کاملاً مشخص باشد که «این فقط یک پروتوتایپ است و قرار نیست همین بماند»؟
چه چیزی را انتخاب میکردی و چرا؟
مبحث ۱۴: زبانهای دامنه (Domain Languages)
«مرزهای زبان من، مرزهای دنیای من هستند.»
— لودویگ ویتگنشتاین
زبانهای کامپیوتری بر نحوه تفکر شما درباره یک مسئله و همچنین نحوه تفکر شما درباره ارتباط برقرار کردن تأثیر میگذارند. هر زبانی با لیستی از ویژگیها همراه است: واژههای دهنپرکنی مانند نوعدهی ایستا در برابر پویا (Static vs Dynamic typing)، اتصال زودرس در برابر دیررس (Early vs Late binding)، تابعی (Functional) در برابر شیءگرا (OO)، مدلهای ارثبری، میکسینها (Mixins) و ماکروها—که همگی ممکن است راهحلهای خاصی را پیشنهاد دهند یا آنها را پنهان کنند.
طراحی یک راهحل با ذهنیت C++ نتایجی متفاوت از راهحلی بر پایه تفکر به سبک Haskell به بار میآورد و بالعکس.
در مقابل، و به اعتقاد ما مهمتر از آن، زبانِ خودِ دامنه مسئله نیز میتواند راهکار برنامهنویسی را پیشنهاد دهد. ما همیشه سعی میکنیم کد را با استفاده از واژگان دامنه برنامه بنویسیم (نگاه کنید به: یک واژهنامه را حفظ کنید). در برخی موارد، «برنامهنویسان عملگرا» میتوانند یک مرحله فراتر رفته و عملاً با استفاده از واژگان، نحو (Syntax) و معناشناسی (Semantics)—یعنی همان زبان—دامنه کدنویسی کنند.
نکته ۲۲: نزدیک به دامنه مسئله برنامهنویسی کنید
برخی زبانهای دامنه در دنیای واقعی
بیایید به چند نمونه نگاه کنیم که افراد دقیقاً همین کار را انجام دادهاند.
RSpec [19]
آر-اسپک (RSpec) یک کتابخانه تست برای Ruby است. این کتابخانه الهامبخش نسخههایی برای اکثر زبانهای مدرن دیگر شده است. یک تست در RSpec به گونهای طراحی شده که بازتابدهنده رفتاری باشد که از کد خود انتظار دارید.
describe BowlingScore do
it "totals 12 if you score 3 four times" do
score = BowlingScore.new
4.times { score.add_pins(3) }
expect(score.total).to eq(12)
end
end
Cucumber [20]
کامبر (Cucumber) روشی خنثی نسبت به زبان برنامهنویسی برای مشخص کردن تستهاست. شما تستها را با استفاده از نسخهای از Cucumber که مناسب زبان مورد استفادهتان است اجرا میکنید. برای پشتیبانی از نحوی که شبیه زبان طبیعی باشد، شما باید «تطبیقدهندههای» (Matchers) خاصی بنویسید که عبارات را تشخیص داده و پارامترها را برای تست استخراج کنند.
Feature: Scoring
Background:
Given an empty scorecard
Scenario: bowling a lot of 3s
Given I throw a 3
And I throw a 3
And I throw a 3
And I throw a 3
Then the score should be 12
تستهای Cucumber با این هدف طراحی شده بودند که توسط مشتریان نرمافزار خوانده شوند (اگرچه در عمل این اتفاق به ندرت میافتد؛ کادر حاشیه زیر بررسی میکند که چرا ممکن است چنین باشد).
چرا بسیاری از کاربران تجاری ویژگیهای Cucumber را نمیخوانند؟
یکی از دلایلی که رویکرد کلاسیکِ «جمعآوری نیازمندیها، طراحی، کدنویسی، تحویل» کار نمیکند، این است که این رویکرد بر این مفهوم استوار است که ما میدانیم نیازمندیها چه هستند. اما ما به ندرت میدانیم.
کاربران تجاری شما ایده مبهمی از آنچه میخواهند به دست آورند دارند، اما نه جزئیات را میدانند و نه اهمیتی به آن میدهند. این بخشی از ارزش ماست: ما نیت آنها را درک کرده و به کد تبدیل میکنیم.
بنابراین وقتی شما یک فرد تجاری را مجبور میکنید که یک سند نیازمندیها را امضا کند، یا آنها را وادار میکنید تا با مجموعهای از ویژگیهای Cucumber موافقت کنند، دقیقاً مثل این است که از آنها بخواهید غلط املایی یک مقاله نوشته شده به خط میخی (سومری) را بگیرند. آنها چند تغییر تصادفی ایجاد میکنند تا حفظ آبرو کنند و آن را امضا میکنند تا فقط شما را از دفترشان بیرون کنند.
اما به آنها کدی بدهید که اجرا میشود، و آنها میتوانند با آن بازی کنند. آنجاست که نیازهای واقعی آنها نمایان میشود.
Phoenix Routes
بسیاری از فریمورکهای وب دارای امکانات مسیریابی (Routing) هستند که درخواستهای HTTP ورودی را به توابع مدیریتکننده (Handler) در کد نگاشت میکنند. در اینجا مثالی از Phoenix [21] آورده شده است:
scope "/", HelloPhoenix do
pipe_through :browser # Use the default browser stack
get "/", PageController, :index
resources "/users", UserController
end
این کد میگوید که درخواستهایی که با / شروع میشوند، از طریق مجموعهای از فیلترهای مناسب برای مرورگرها عبور داده شوند. یک درخواست به خودِ / توسط تابع index در ماژول PageController مدیریت خواهد شد. UserController توابعی را پیادهسازی میکند که برای مدیریت منبعی که از طریق آدرس /users قابل دسترسی است، مورد نیاز هستند.
Ansible [22] [23]
انسیبل (Ansible) ابزاری است که نرمافزار را پیکربندی میکند، معمولاً روی دستهای از سرورهای راه دور. این کار با خواندن مشخصاتی (Specification) که شما ارائه میدهید انجام میشود، سپس هر کاری که لازم باشد روی سرورها انجام میدهد تا آنها را با آن مشخصات منطبق کند. این مشخصات میتواند به زبان YAML نوشته شود؛ زبانی که ساختارهای داده را از توضیحات متنی میسازد:
- name: install nginx
apt: name=nginx state=latest
- name: ensure nginx is running (and enable it at boot)
service: name=nginx state=started enabled=yes
- name: write the nginx config file
template: src=templates/nginx.conf.j2 dest=/etc/nginx/nginx.conf
notify:
- restart nginx
این مثال اطمینان حاصل میکند که آخرین نسخه nginx روی سرورهای من نصب شده است، بهطور پیشفرض اجرا میشود، و از فایل پیکربندی که شما ارائه دادهاید استفاده میکند.
ویژگیهای زبانهای دامنه (Characteristics of Domain Languages)
بیایید نگاه دقیقتری به این مثالها بیندازیم. RSpec و مسیریاب Phoenix در زبانهای میزبان خود (Ruby و Elixir) نوشته شدهاند. آنها از کدهای نسبتاً پیچیده و زیرکانهای استفاده میکنند، از جمله متا-برنامهنویسی (Metaprogramming) و ماکروها، اما در نهایت کامپایل شده و به عنوان کد معمولی اجرا میشوند.
تستهای Cucumber و پیکربندیهای Ansible در زبانهای مختص به خودشان نوشته شدهاند. یک تست Cucumber به کدی برای اجرا شدن یا به یک ساختار داده تبدیل میشود، در حالی که مشخصات Ansible همیشه به یک ساختار داده تبدیل میشوند که توسط خودِ Ansible اجرا میگردد.
در نتیجه، RSpec و کد مسیریاب در کدی که شما اجرا میکنید تعبیه (Embedded) شدهاند: آنها افزونههای واقعی برای دایره لغات کد شما هستند. Cucumber و Ansible توسط کد خوانده شده و به فرمی تبدیل میشوند که کد بتواند از آن استفاده کند. ما RSpec و مسیریاب را نمونههایی از زبانهای دامنه داخلی (Internal Domain Languages) مینامیم، در حالی که Cucumber و Ansible از زبانهای خارجی (External Languages) استفاده میکنند.
سبکسنگین کردن زبانهای داخلی و خارجی (Trade-offs)
بهطور کلی، یک زبان دامنه داخلی میتواند از ویژگیهای زبان میزبان خود بهره ببرد: زبان دامنهای که میسازید قدرتمندتر است و این قدرت به رایگان به دست میآید. برای مثال، میتوانید از مقداری کد Ruby استفاده کنید تا تعداد زیادی تست RSpec را بهطور خودکار ایجاد کنید. در این مورد میتوانیم امتیازاتی را تست کنیم که در آنها خبری از "Spares" یا "Strikes" نیست:
describe BowlingScore do
(0..4).each do |pins|
(1..20).each do |throws|
target = pins * throws
it "totals #{target} if you score #{pins} #{throws} times" do
score = BowlingScore.new
throws.times { score.add_pins(pins) }
expect(score.total).to eq(target)
end
end
end
end
این هم ۱۰۰ تستی که همین الان نوشتید. بقیه روز را مرخصی بگیرید.
نقطه ضعف زبانهای دامنه داخلی این است که شما محدود به نحو (Syntax) و معناشناسی آن زبان هستید. اگرچه برخی زبانها در این زمینه بسیار انعطافپذیرند، اما همچنان مجبورید بین زبانی که میخواهید و زبانی که میتوانید پیادهسازی کنید، مصالحه کنید. در نهایت، هر چه که ارائه میدهید باید همچنان سینتکس معتبر در زبان مقصد شما باشد. زبانهای دارای ماکرو (مانند Elixir، Clojure و Crystal) انعطافپذیری کمی بیشتری به شما میدهند، اما در نهایت سینتکس، سینتکس است.
زبانهای خارجی هیچکدام از این محدودیتها را ندارند. تا زمانی که بتوانید یک پارسر (Parser) برای زبان بنویسید، کارتان راه میافتد. گاهی اوقات میتوانید از پارسر شخص دیگری استفاده کنید (همانطور که Ansible با استفاده از YAML این کار را کرد)، اما در آن صورت دوباره به مصالحه بازمیگردید. نوشتن یک پارسر احتمالاً به معنای افزودن کتابخانههای جدید و احتمالاً ابزارهایی به برنامه شماست. و نوشتن یک پارسر خوب کار پیشپاافتادهای نیست. اما، اگر دل شیر دارید، میتوانید نگاهی به تولیدکنندگان پارسر (Parser Generators) مانند bison یا ANTLR و فریمورکهای پارسینگ مانند بسیاری از پارسرهای PEG موجود بیندازید.
پیشنهاد ما نسبتاً ساده است: تلاشی بیش از آنچه که صرفهجویی میکنید، صرف نکنید. نوشتن یک زبان دامنه هزینهای به پروژه شما اضافه میکند و باید قانع شوید که صرفهجوییهای جبرانکنندهای (احتمالاً در درازمدت) وجود دارد.
بهطور کلی، اگر میتوانید از زبانهای خارجی آماده (مانند YAML، JSON یا CSV) استفاده کنید. اگر نه، به زبانهای داخلی نگاه کنید. ما توصیه میکنیم فقط در مواردی از زبانهای خارجی استفاده کنید که زبان شما قرار است توسط کاربران برنامه شما نوشته شود.
یک زبان دامنه داخلی ارزانقیمت (On The Cheap)
در نهایت، یک راه میانبر (Cheat) برای ایجاد زبانهای دامنه داخلی وجود دارد، اگر مشکلی ندارید که سینتکس زبان میزبان کمی در آن نشت کند. سراغ متا-برنامهنویسیهای سنگین نروید. در عوض، فقط توابعی بنویسید که کار را انجام دهند.
در واقع، این تقریباً همان کاری است که RSpec انجام میدهد:
describe BowlingScore do
it "totals 12 if you score 3 four times" do
score = BowlingScore.new
4.times { score.add_pins(3) }
expect(score.total).to eq(12)
end
end
در این کد، describe، it، expect، to و eq فقط متدهای روبی هستند. کمی لولهکشی (Plumbing) در پشت صحنه در مورد نحوه انتقال اشیاء وجود دارد، اما همهچیز فقط کد است. ما این موضوع را کمی در تمرینها بررسی خواهیم کرد.
بخشهای مرتبط شامل
- مبحث ۸: جوهره طراحی خوب
- مبحث ۱۳: پروتوتایپها و یادداشتهای چسبان (Post-it)
- مبحث ۳۲: پیکربندی (Configuration)
چالشها
- آیا برخی از نیازمندیهای پروژه فعلی شما میتواند در یک زبان خاص دامنه بیان شود؟
- آیا ممکن است یک کامپایلر یا مترجم بنویسید که بتواند بیشتر کد مورد نیاز را تولید کند؟
- اگر تصمیم بگیرید زبانهای کوچک (Mini-languages) را به عنوان روشی برای برنامهنویسیِ نزدیکتر به دامنه مسئله اتخاذ کنید، میپذیرید که تلاشی برای پیادهسازی آنها مورد نیاز است. آیا میتوانید راههایی را ببینید که در آن فریمورکی که برای یک پروژه توسعه میدهید، در پروژههای دیگر قابل استفاده مجدد باشد؟
تمرینها
تمرین ۴ (پاسخ احتمالی):
ما میخواهیم یک زبان کوچک (Mini-language) برای کنترل یک سیستم گرافیکی لاکپشتی ساده پیادهسازی کنیم. این زبان شامل دستورات تکحرفی است که برخی از آنها با یک عدد دنبال میشوند. برای مثال، ورودی زیر یک مستطیل رسم میکند:
P 2 # قلم ۲ را انتخاب کن
D # قلم پایین
W 2 # غرب ۲ سانتیمتر بکش
N 1 # سپس شمال ۱
E 2 # سپس شرق ۲
S 1 # سپس برگرد جنوب
U # قلم بالا
کدی را پیادهسازی کنید که این زبان را پارس (تجزیه) کند. طراحی باید به گونهای باشد که اضافه کردن دستورات جدید ساده باشد.
تمرین ۵ (پاسخ احتمالی):
در تمرین قبلی ما یک پارسر برای زبان طراحی پیادهسازی کردیم—آن یک زبان دامنه خارجی بود. حالا آن را دوباره به عنوان یک زبان داخلی پیادهسازی کنید. کار هوشمندانهای انجام ندهید: فقط برای هر یک از دستورات یک تابع بنویسید. ممکن است مجبور شوید نام دستورات را به حروف کوچک تغییر دهید و شاید آنها را درون چیزی بپیچید (Wrap) تا کمی Context (زمینه) فراهم کنید.
تمرین ۶ (پاسخ احتمالی):
یک گرامر BNF طراحی کنید تا یک مشخصات زمانی را پارس کند. تمام مثالهای زیر باید پذیرفته شوند:
4pm, 7:38pm, 23:42, 3:16, 3:16am
تمرین ۷ (پاسخ احتمالی):
یک پارسر برای گرامر BNF تمرین قبلی با استفاده از یک تولیدکننده پارسر PEG در زبان انتخابی خود پیادهسازی کنید. خروجی باید یک عدد صحیح شامل تعداد دقایق گذشته از نیمهشب باشد.
تمرین ۸ (پاسخ احتمالی):
پارسر زمان را با استفاده از یک زبان اسکریپتی و عبارات باقاعده (Regular Expressions) پیادهسازی کنید.
مبحث ۱۵: تخمین زدن (Estimating)
کتابخانه کنگره در واشنگتن دیسی، در حال حاضر حدود ۷۵ ترابایت اطلاعات دیجیتال آنلاین دارد.
سریع بگویید! چقدر طول میکشد تا تمام این اطلاعات را روی یک شبکه ۱ گیگابیت بر ثانیه (1Gbps) ارسال کنیم؟
برای ذخیره یک میلیون نام و آدرس به چه مقدار فضای ذخیرهسازی نیاز دارید؟
فشردهسازی ۱۰۰ مگابایت متن چقدر زمان میبرد؟
تحویل پروژه شما چند ماه طول میکشد؟
در یک سطح، همه اینها سوالاتی بیمعنی هستند—همه آنها اطلاعات ناقصی دارند. و با این حال، تا زمانی که با تخمین زدن راحت باشید، به همه آنها میتوان پاسخ داد.
و در فرآیند تولید یک تخمین، شما درک بیشتری از دنیایی که برنامههایتان در آن زندگی میکنند، پیدا خواهید کرد. با یادگیری تخمین زدن، و با توسعه این مهارت تا جایی که حسی شهودی نسبت به بزرگیِ چیزها (Magnitudes) پیدا کنید، قادر خواهید بود توانایی جادویی ظاهری در تعیین امکانپذیری امور از خود نشان دهید.
وقتی کسی میگوید «ما نسخه پشتیبان را از طریق اتصال شبکه به S3 میفرستیم»، شما بهطور شهودی خواهید دانست که آیا این کار عملی است یا خیر. وقتی در حال کدنویسی هستید، خواهید دانست کدام زیرسیستمها نیاز به بهینهسازی دارند و کدامیک را میتوان به حال خود رها کرد.
نکته ۲۳: برای اجتناب از غافلگیریها، تخمین بزنید
به عنوان یک جایزه، در انتهای این بخش ما تنها پاسخ صحیح را که باید هر وقت کسی از شما تخمینی خواست بدهید، فاش خواهیم کرد.
چقدر دقیق به اندازه کافی دقیق است؟
تا حدودی، همه پاسخها تخمین هستند. فقط مسئله این است که برخی دقیقتر از بقیه هستند.
بنابراین اولین سوالی که باید از خود بپرسید وقتی کسی از شما تخمینی میخواهد، این است که پاسحتان در چه زمینهای (Context) برداشت خواهد شد. آیا آنها به دقت بالا نیاز دارند یا دنبال یک عدد حدودی (Ballpark figure) هستند؟
یکی از نکات جالب درباره تخمین زدن این است که واحدهایی که استفاده میکنید، در تفسیر نتیجه تفاوت ایجاد میکنند. اگر بگویید کاری حدود ۱۳۰ روز کاری طول میکشد، مردم انتظار دارند که کار تقریباً دقیقاً در همان زمان تمام شود. اما اگر بگویید «اوه، حدود شش ماه»، آنها میدانند که باید انتظار اتمام کار را در هر زمانی بین پنج تا هفت ماه آینده داشته باشند. هر دو عدد یک مدت زمان را نشان میدهند، اما «۱۳۰ روز» احتمالاً دلالت بر درجه دقت بالاتری دارد که شما واقعاً مد نظر ندارید.
ما توصیه میکنیم که تخمینهای زمانی را به صورت زیر مقیاسبندی کنید:
| مدت زمان | تخمین را با این واحد بیان کنید |
|---|---|
| ۱ تا ۱۵ روز | روز |
| ۳ تا ۶ هفته | هفته |
| ۸ تا ۲۰ هفته | ماه |
| ۲۰+ هفته | ماه |
قبل از ارائه تخمین خوب فکر کنید.
بنابراین، اگر بعد از انجام تمام کارهای لازم، تصمیم گرفتید که یک پروژه ۱۲۵ روز کاری (۲۵ هفته) طول میکشد، ممکن است بخواهید تخمین «حدود شش ماه» را ارائه دهید. همین مفاهیم برای تخمین هر کمیت دیگری نیز صادق است: واحدهای پاسخ خود را طوری انتخاب کنید که منعکسکننده دقتی باشد که قصد انتقال آن را دارید.
تخمینها از کجا میآیند؟
همه تخمینها بر اساس مدلهای مسئله هستند. اما قبل از اینکه خیلی عمیق وارد تکنیکهای ساخت مدل شویم، باید به یک ترفند اساسی تخمینزنی اشاره کنیم که همیشه جوابهای خوبی میدهد: از کسی بپرسید که قبلاً این کار را انجام داده است.
قبل از اینکه خیلی درگیر مدلسازی شوید، ببینید آیا کسی را پیدا میکنید که در گذشته در موقعیتی مشابه بوده باشد. ببینید مسئله آنها چگونه حل شده است. بعید است که دقیقاً همان شرایط را پیدا کنید، اما تعجب خواهید کرد که چقدر میتوانید با موفقیت از تجربیات دیگران بهره ببرید.
بفهمید چه چیزی خواسته شده است
اولین بخش هر تمرین تخمینزنی، ایجاد درکی از آن چیزی است که خواسته شده. علاوه بر مسائل مربوط به دقت که در بالا بحث شد، باید درکی از محدوده (Scope) دامنه داشته باشید. اغلب این موضوع در سوال پنهان است، اما باید عادت کنید قبل از شروع به حدس زدن، درباره محدوده فکر کنید. اغلب، محدودهای که انتخاب میکنید بخشی از پاسخی را تشکیل میدهد که میدهید: «با فرض اینکه تصادفی رخ ندهد و ماشین بنزین داشته باشد، من ۲۰ دقیقهای آنجا خواهم بود.»
یک مدل از سیستم بسازید
این بخش سرگرمکننده تخمین زدن است. بر اساس درک خود از سوال پرسیده شده، یک مدل ذهنی اولیه و ساده (Rough-and-ready) بسازید. اگر زمان پاسخگویی را تخمین میزنید، مدل شما ممکن است شامل یک سرور و نوعی ترافیک ورودی باشد.
مدلسازی میتواند هم خلاقانه و هم در درازمدت مفید باشد. اغلب، فرآیند ساخت مدل منجر به کشف الگوها و فرآیندهای زیربنایی میشود که در سطح ظاهری مشخص نبودند. حتی ممکن است بخواهید سوال اصلی را بازبینی کنید: «شما تخمینی برای انجام X خواستید. اما به نظر میرسد Y، که نوعی از X است، میتواند در حدود نصف آن زمان انجام شود و شما فقط یک ویژگی را از دست میدهید.»
ساخت مدل باعث ایجاد بیدقتیهایی در فرآیند تخمین میشود. این اجتنابناپذیر و همچنین مفید است. شما سادگی مدل را با دقت معاوضه میکنید. دو برابر کردن تلاش روی مدل ممکن است فقط افزایش اندکی در دقت به شما بدهد. تجربه به شما خواهد گفت که چه زمانی دست از اصلاح مدل بردارید.
مدل را به اجزا بشکنید
وقتی مدلی دارید، میتوانید آن را به اجزا (Components) تجزیه کنید. باید قوانین ریاضی را کشف کنید که نحوه تعامل این اجزا را توصیف میکنند. برخی اجزا ممکن است ضرایب ضربکننده (Multiplying factors) ارائه دهند، در حالی که برخی دیگر پیچیدهتر باشند. معمولاً متوجه میشوید که هر جزء دارای پارامترهایی است که بر نحوه مشارکت آن در مدل کلی تأثیر میگذارد. در این مرحله، فقط هر پارامتر را شناسایی کنید.
به هر پارامتر مقدار بدهید
حالا میتوانید پیش بروید و به هر پارامتر یک مقدار اختصاص دهید. انتظار داشته باشید که در این مرحله خطاهایی معرفی کنید. ترفند کار این است که بفهمید کدام پارامترها بیشترین تأثیر را روی نتیجه دارند و تمرکز کنید تا آنها را تقریباً درست به دست آورید. معمولاً پارامترهایی که مقادیرشان به نتیجه اضافه میشود اهمیت کمتری نسبت به آنهایی دارند که ضرب یا تقسیم میشوند.
پاسخها را محاسبه کنید
فقط در سادهترین موارد یک تخمین دارای یک پاسخ واحد است. با پیچیدهتر شدن سیستمها، باید پاسخهای خود را با شرط و شروط بیان کنید. چندین محاسبه انجام دهید، مقادیر پارامترهای حیاتی را تغییر دهید تا بفهمید کدامیک واقعاً مدل را هدایت میکنند. سپس پاسخ خود را بر اساس این پارامترها بیان کنید. «زمان پاسخگویی تقریباً سه چهارم ثانیه است اگر سیستم دارای SSD و ۳۲ گیگابایت حافظه باشد، و یک ثانیه با ۱۶ گیگابایت حافظه.»
مهارت تخمینزنی خود را ردیابی کنید
ما فکر میکنیم ایده بسیار خوبی است که تخمینهای خود را ثبت کنید تا ببینید چقدر نزدیک بودهاید. وقتی یک تخمین اشتباه از آب در میآید، فقط شانه بالا نیندازید و نروید—پیدا کنید چرا. اگر این کار را بکنید، تخمین بعدی شما بهتر خواهد بود.
تخمین زمانبندی پروژه
معمولاً از شما پرسیده میشود که انجام کاری چقدر طول میکشد.
رنگ کردن موشک (Painting the Missile)
«رنگ کردن خانه چقدر طول میکشد؟»
«خب، اگر همهچیز درست پیش برود... شاید ۱۰ ساعت. اما واقعبینانهاش نزدیک به ۱۸ ساعت است. و البته اگر هوا بد شود، ممکن است تا ۳۰ ساعت یا بیشتر طول بکشد.»
این روشی است که مردم در دنیای واقعی تخمین میزنند. نه با یک عدد واحد، بلکه با طیفی از سناریوها.
وقتی نیروی دریایی آمریکا نیاز به برنامهریزی پروژه زیردریایی Polaris داشت، این سبک تخمینزنی را با متدولوژی به نام PERT (تکنیک ارزیابی و بازنگری برنامه) اتخاذ کرد. هر وظیفه در PERT دارای یک تخمین خوشبینانه، یک تخمین محتملترین و یک تخمین بدبینانه است.
استفاده از طیفی از مقادیر راهی عالی برای جلوگیری از یکی از رایجترین دلایل خطای تخمین است: باد کردن (Padding) یک عدد چون مطمئن نیستید.
خوردن فیل (Eating the Elephant)
ما دریافتهایم که اغلب تنها راه تعیین جدول زمانی برای یک پروژه، کسب تجربه در همان پروژه است. اگر توسعه تدریجی (Incremental development) را تمرین کنید، این موضوع تناقض نخواهد بود؛ تکرار مراحل زیر با برشهای بسیار نازکی از عملکرد:
- بررسی نیازمندیها
- تحلیل ریسک
- طراحی، پیادهسازی، ادغام
- اعتبارسنجی با کاربران
در ابتدا، ممکن است فقط ایده مبهمی داشته باشید که چند تکرار (Iteration) نیاز است. برخی روشها شما را ملزم میکنند که این را در طرح اولیه تثبیت کنید؛ اما به جز پروژههای بسیار پیشپاافتاده، این یک اشتباه است.
بنابراین کدنویسی و تست عملکرد اولیه را تکمیل کنید و این را پایان اولین تکرار در نظر بگیرید. بر اساس آن تجربه، میتوانید حدس اولیه خود را در مورد تعداد تکرارها اصلاح کنید.
نکته ۲۴: زمانبندی را همراه با کد تکرار (Iterate) کنید
این نوع تخمینزنی اغلب در بازبینی تیم در پایان هر چرخه تکرار انجام میشود. این همانطوری است که شوخی قدیمی میگوید چطور باید یک فیل را خورد: لقمهلقمه.
وقتی از شما تخمین خواسته میشود چه بگویید
شما میگویید: «بهتون خبر میدم.» (I’ll get back to you)
اگر فرآیند را کند کنید و زمانی را صرف طی کردن مراحلی که در این بخش توصیف کردیم بکنید، تقریباً همیشه نتایج بهتری میگیرید. تخمینهایی که پای دستگاه قهوهساز داده میشوند (مثل خود قهوه) بازمیگردند تا یقه شما را بگیرند!
بخشهای مرتبط شامل
- مبحث ۷: ارتباط برقرار کنید!
- مبحث ۳۹: سرعت الگوریتم
چالشها
- شروع به نگه داشتن لاگ تخمینهای خود کنید. برای هر کدام، ردیابی کنید که چقدر دقیق بودید. اگر خطای شما بیشتر از ۵۰٪ بود، سعی کنید بفهمید کجای تخمینتان اشتباه بوده است.
تمرینها
تمرین ۹ (پاسخ احتمالی):
از شما پرسیده میشود: «کدامیک پهنای باند (Bandwidth) بیشتری دارد: یک اتصال شبکه ۱ گیگابیت بر ثانیه (1Gbps) یا شخصی که بین دو کامپیوتر راه میرود در حالی که یک دستگاه ذخیرهسازی پر ۱ ترابایتی در جیبش دارد؟»
چه محدودیتهایی را روی پاسخ خود اعمال میکنید تا مطمئن شوید محدوده (Scope) پاسخ شما صحیح است؟ (برای مثال، ممکن است بگویید زمان لازم برای دسترسی به دستگاه ذخیرهسازی نادیده گرفته میشود.)
تمرین ۱۰ (پاسخ احتمالی):
خب، کدامیک پهنای باند بیشتری دارد؟