در اوایل برنامهنویسی ، سیستم های خود را باroutine ها و subroutine ها ساختیم. سپس، در عصر Fortran و PL / 1 سیستمهایمان را با برنامهها، زیر برنامهها و توابع ساختیم. امروزه تنها توابع هستند که از آن دوران اولیه باقی ماندهاند. توابع اولین خط سازماندهی در هر برنامه هستند. خوب نوشتن آنها موضوع این فصل است. کد ارائهشده در لیست 1-3 را در نظر بگیرید. پیدا کردن یک تابع طولانی در FitNesse1 دشوار است ، اما پس از کمی جستجو به این مورد رسیدم. این تابع نه تنها طولانی است ، بلکه کد تکراری، تعداد زیادی رشته عجیب و غریب و انواع مختلف داده ها و API های عجیب و مبهم را نیز داراست. ببینید که در مدت سه دقیقه آینده چقدر از آن را میتوانید درک کنید.
Listing 3-1 HtmlUtil.java (FitNesse 20070619)
public static String testableHtml(PageData pageData,boolean includeSuiteSetup) throws Exception {
WikiPage wikiPage=pageData.getWikiPage();
StringBuffer buffer=new StringBuffer();
if(pageData.hasAttribute("Test")){
if(includeSuiteSetup){
WikiPage suiteSetup=PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_SETUP_NAME,wikiPage);
if(suiteSetup!=null){
WikiPagePath pagePath=suiteSetup.getPageCrawler().getFullPath(suiteSetup);
String pagePathName=PathParser.render(pagePath);
buffer.append("!include -setup .")
.append(pagePathName)
.append("\n");
}
}
WikiPage setup=PageCrawlerImpl.getInheritedPage("SetUp",wikiPage);
if(setup!=null){
WikiPagePath setupPath=wikiPage.getPageCrawler().getFullPath(setup);
String setupPathName=PathParser.render(setupPath);
buffer.append("!include -setup .")
.append(setupPathName)
.append("\n");
}
}
buffer.append(pageData.getContent());
if(pageData.hasAttribute("Test")){
WikiPage teardown=PageCrawlerImpl.getInheritedPage("TearDown",wikiPage);
if(teardown!=null){
WikiPagePath tearDownPath=wikiPage.getPageCrawler().getFullPath(teardown);
String tearDownPathName=PathParser.render(tearDownPath);
buffer.append("\n")
.append("!include -teardown .")
.append(tearDownPathName)
.append("\n");
}
if(includeSuiteSetup){
WikiPage suiteTeardown=PageCrawlerImpl.getInheritedPage(SuiteResponder.SUITE_TEARDOWN_NAME,wikiPage);
if(suiteTeardown!=null){
WikiPagePath pagePath=suiteTeardown.getPageCrawler().getFullPath(suiteTeardown);
String pagePathName=PathParser.render(pagePath);
buffer.append("!include -teardown .")
.append(pagePathName)
.append("\n");
}
}
}
pageData.setContent(buffer.toString());
return pageData.getHtml();
}
Listing 3-2
HtmlUtil.java (refactored)
public static String renderPageWithSetupsAndTeardowns( PageData, boolean isSuite) throws Exception
{
boolean isTestPage = pageData.hasAttribute("Test");
if (isTestPage)
{
WikiPage testPage = pageData.getWikiPage();
StringBuffer newPageContent = new StringBuffer();
includeSetupPages(testPage, newPageContent, isSuite);
newPageContent.append(pageData.getContent());
includeTeardownPages(testPage, newPageContent, isSuite);
pageData.setContent(newPageContent.toString());
}
return pageData.getHtml();
}
اولین قانون برای توابع این است که آنها باید کوچک باشند. قانون دوم این است که آنها باید از آن هم کوچکتر باشند. این ادعایی نیست که بتوانم توجیه کنم. نمیتوانم به تحقیقاتی اشاره کنم که نشان میدهد توابع بسیار کوچک بهتر هستند. چیزی که میتوانم به شما بگویم این است که نزدیک به چهار دهه توابع مختلفی در ابعاد مختلف نوشتم. چندین مورد ناپسند 3000 خطی نوشتم. تعداد زیادی تابع در حدود 100 تا 300 خط نوشتم. و توابعی به طول 20 تا 30 خط نوشتهام. آنچه این تجربه از طریق آزمایش طولانی و خطا به من آموخته است اینست که توابع باید بسیار کوچک باشند.
در دهه هشتاد میگفتیم که یک تابع نباید بزرگتر از یک صفحه نمایش باشد. البته این را در زمانی گفتیم که صفحه های VT100, 24 خط در 80 ستون بودند و ویرایشگران ما تنها از 4 خط برای مقاصد مدیریتی استفاده میکردند. امروزه با یک فونت کوتاه شده و یک مانیتور بزرگ خوب ، میتوانید 150 کاراکتر را روی یک خط و 100 خط یا بیشتر را در یک صفحه قرار دهید. خطوط نباید 150 کاراکتر داشته باشد. توابع نباید 100 خط باشند. توابع باید به سختی به طول 20 خط برسند.
یک تابع چقدر باید کوتاه باشد؟ در سال 1999 من برای دیدن Kent Beck به خانه اش در اورگان رفتم. نشستیم و با هم برنامهنویسی کردیم. در یک لحظه او یک برنامه Java/Swing کوچک زیبا را به من نشان داد که آن را Sparkle نامید. این اثر یک جلوه بصری بسیار شبیه به چوب جادویی الهه افسانه ای در فیلم سیندرلا ایجاد کرد. با حرکت دادن ماوس ، جرقه ها با یک قلاب رضایت بخش از مکان نما بیرون میریزند و از طریق یک میدان گرانشی شبیهسازی شده، به پایین پنجره می افتادند. وقتی Kent کد را به من نشان داد، من از این که همه توابع چقدر کوچک هستند ، تحت تأثیر قرار گرفتم. من به توابع برنامههای Swing که فضای عمودی زیادی اشغال میکنند، عادت داشتم. هر تابع در این برنامه فقط دو، سه یا چهار خط طول داشت. هر کدام واضح و آشکار بود. هر کدام داستانی را بیان می کرد. و هر کدام شما را به ترتیبی قانعکننده به سمت بعدی سوق میدادند. این مقدار همانی مقداری است که توابع شما باید به آن اندازه کوچک باشند!
توابع شما چقدر کوتاه باشد؟ آنها معمولاً باید از لیست 2-3 کوتاهتر باشند! در واقع، لیست 2-3 باید به اندازه لیست 3-3 کوتاه شود.(من از Kent سؤال کردم که آیا او هنوز یک نسخه دارد ، اما او نتوانست یکی از آنها را پیدا کند. من تمام رایانه های قدیمی خود را نیز جستجو کردم ، اما فایدهای نداشت. تمام آنچه اکنون باقی مانده است ، خاطره من از آن برنامه است.)
Listing 3-3 HtmlUtil.java (re-refactored)
public static String renderPageWithSetupsAndTeardowns(PageData pageData, boolean isSuite) throws Exception
{
if (isTestPage(pageData))
includeSetupAndTeardownPages(pageData, isSuite);
return pageData.getHtml();
}
این لیست نشان میدهد که بلوک های موجود در جملات شرطی، حلقه ها، و نظایر آن، باید به اندازه یک خط باشند. احتمالاً آن خط باید یک فراخوانی تابع باشد. این کار نهتنها تابع محصور را کوچک نگه میدارد بلکه یک ارزش مستندسازی را نیز به آن اضافه میکند زیرا تابع فراخوانیشده در آن بلوک می تواند نام توصیفی خوبی داشته باشد. همچنین این لیست نشان میدهد توابع نباید برای نگهداشتن ساختارهای تو در تو به اندازه کافی بزرگ باشند. بنابراین ، سطح تورفتگی یک تابع نباید بیشتر از یک یا دو باشد. این امر البته باعث می شود خواندن و درک توابع آسان تر شود.
باید خیلی واضح باشد که لیست 1-3 بیش از یک کار انجام میدهد. این از جمله ایجاد بافر، واکشی صفحات، جستجوی صفحات وراثت، تفسیر مسیرها، افزودن رشته های arcane و ایجاد HTML از جمله موارد دیگر است. لیست 1-3 بسیار مشغول انجام کارهای مختلف است. از طرف دیگر ، لیست 3-3 یک کار ساده را انجام میدهد. این شامل setups و teardowns به صفحات تست است.
توصیه های زیر به مدت 30 سال یا بیشتر به یک شکل یا شکل دیگر ظاهر شده است.
توابع باید یک کار را انجام دهند. باید خوب انجامش دهند. فقط باید این کار را انجام دهند.
مشکلی که در این گفته وجود دارد این است که دانستن اینکه "یک کار" چیست دشوار است. آیا لیست 3-3 یک کار را انجام میدهد؟ به راحتی می توان موزدی را درنظر گرفت که سه کار را انجام میدهد:
- تعیین اینکه آیا این صفحه یک صفحه آزمایشی است یا خیر.
- در این صورت ، تنظیمات و درهم دریدن شامل شود.
- صفحه در HTML پردازش شود.
پس کدام است؟ آیا تابع یک کار را انجام میدهد یا سه کار؟ توجه کنید که سه مرحله از عملکرد، تنها یک سطح انتزاع زیر نام بیان شده تابع است. میتوانیم تابع را به عنوان یک بند کوتاه TO شرح دهیم:
TO RenderPageWithSetupsAndTeardowns، چک میکنیم که صفحه آیا صفحه تست است و اگر بود، setups و teardowns را اضافه میکنیم. در هر صورت، صفحه را به HDML رندر میکنیم.
اگر یک تابع فقط آن مراحل را انجام دهد که یک سطح زیر نام بیان شده تابع باشد، آنگاه تابع یک کار را انجام میدهد. از این گذشته، دلیل اینکه ما توابع را می نویسیم، تجزیه مفهوم بزرگتر (به عبارت دیگر، نام تابع) در مجموعه مراحل در سطح بعدی انتزاع است.
باید خیلی واضح باشد که لیست 1-3 شامل مراحل در بسیاری از سطوح مختلف انتزاع است. بنابراین به وضوح بیش از یک کار را انجام میدهد. حتی لیست 2-3 دارای دو سطح انتزاع است ، که با توانایی ما در شکاندن آن اثبات می شود. اما شکاندن لیست 3-3 بسیار سخت خواهد بود.میتوانیم قسمت if را بهصورت متدی بنام includeSetupsAndTeardownsIfTestPage استخراج کنیم، اما باز کد را بدون تغییر سطح انتزاع بیان میکند.
بنابراین ، راه دیگری برای دانستن اینکه یک تابع بیشتر از "یک کار" انجام میدهد ، این است که اگر شما میتوانید تابع دیگری را از آن با نامی استخراج کنید که صرفاً بازگویی در اجرای آن نیست [G34].
به لیست 4-7 در صفحه ۷۱ نگاه کنید. درنظر داشته باشید که تابع generatePrimes به بخشهایی مثل declarations، initializations و sieve تقسیم میشود. این یک نشانه بارز انجام بیش از یک کار است. توابعی که یک کار را انجام میدهند به طور منطقی نمیتوانند به بخشها تقسیم شوند.
برای اینکه مطمئن شویم توابع ما "یک کار" را انجام میدهند ، باید اطمینان حاصل کنیم که statements موجود در تابع، در یک سطح انتزاع قرار دارند. به راحتی می توان دید که چگونه لیست 1-3 این قانون را نقض میکند. مفاهیمی در آنجا وجود دارند که در سطح بسیار بالایی از انتزاع قرار دارند، مثل getHtml()؛ مابقی در سطح متوسطی از انتزاع هستند، مثل: String pagePathName = PathParser.render(pagePath)؛ و مابقی هنوز بصورت قابل ملاحظهای سطح پایین هستند، مثل: .append("\n") .
ترکیب سطوح مختلف انتزاع در یک تابع همیشه گیجکننده است. خوانندگان ممکن است نتوانند بگویند آیا یک عبارت خاص یک مفهوم اساسی است یا یک جز. بدتر ، مانند پنجره های شکسته ، هنگامی که جزئیات با مفاهیم اساسی مخلوط میشوند ، جزئیات بیشتر و بیشتری تمایل به پیوستن به درون تابع دارند.
میخواهیم کد مانند روایتی از بالا به پایین خوانده شود. میخواهیم هر تابع با سطح بعدی انتزاع دنبال شود تا بتوانیم برنامه را بخوانیم، در حالی که لیست توابع را می خوانیم، یک سطح انتزاع کم می شود. من این را قاعده گام به گام مینامم.
برای طور دیگر گفتن این حرف، میخواهیم بتوانیم برنامه را بخوانیم گویی مجموعهای از پاراگرافهای TO است, هرکدام از آنها سطح فعلی انتزاع را توصیف کرده و به پاراگرافهای TO پسین در سطح پایین بعدی اشاره میکند.
TO include setups و teardowns، include setups، سپس include محتوای صفحه تست، و سپس include teardowns.
TO include setups، include suite setup اگر یک suite وجود دارد، سپس include setup معمولی
TO include suite setup، دنبال والد سلسله مراتبی برای صفحه “SuiteSetUp” میگردیم و یک include statement با مسیر صفحه اضافه می کنیم.
TO دنبال والد بگرد...
به نظر میرسد که برای برنامهنویسان خیلی سخت است که یاد بگیرند از این قاعده پیروی کنند و توابعی را بنویسند که در یک سطح انتزاعی باقی بمانند. اما یادگیری این ترفند نیز بسیار مهم است. این، کلید کوتاه نگه داشتن توابع و اطمینان از انجام "یک کار" آنها است. نوشتن كد بصورتیکه مانند مجموعه بالا به پایین پاراگرافهای TO خوانده شود، یك روش مؤثر برای ثابت نگه داشتن سطح انتزاع است.
در پایان این فصل به لیست 3-7 نگاهی بیندازید. کل تابع testableHtml را نشان میدهد که مطابق با اصول شرح داده شده در اینجا بازنویسی شدهاست. توجه کنید که چگونه هر تابع، تابع بعدی را معرفی میکند ، و هر تابع در یک سطح ثابت از انتزاع باقی می ماند.
خیلی سخت است که switch statement کوچکی ساخت. حتی یک switch statement فقط با دو مورد بزرگتر از آن است که دوست داشته باشم یک بلوک یا یک عملکرد باشد. حتی ساختن یک switch statement که یک کار انجام دهد، سخت است. براساس ماهیت آنها، switch statement همیشه N مورد را انجام میدهند. متأسفانه ، ما همیشه نمیتوانیم از switch statements پرهیز کنیم ، اما میتوانیم اطمینان حاصل کنیم که هر switch statement در یک کلاس سطح پایین قرار دارد و هرگز تکرار نمیشود. این کار را با چند ریختی (polymorphism) انجام میدهیم.
لیست 3-4 را در نظر بگیرید. تنها یکی از عملیاتی را نشان میدهد که ممکن است به نوع کارمند وابسته باشد.
Listing 3-4 Payroll.java
public Money calculatePay(Employee e) throws InvalidEmployeeType
{
switch (e.type)
{
case COMMISSIONED:
return calculateCommissionedPay(e);
case HOURLY:
return calculateHourlyPay(e);
case SALARIED:
return calculateSalariedPay(e);
default:
throw new InvalidEmployeeType(e.type);
}
}
Listing 3-5
Employee and Factory
public abstract class Employee {
public abstract boolean isPayday();
public abstract Money calculatePay();
public abstract void deliverPay(Money pay);
}
public interface EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType;
}
public class EmployeeFactoryImpl implements EmployeeFactory {
public Employee makeEmployee(EmployeeRecord r) throws InvalidEmployeeType {
switch (r.type) {
case COMMISSIONED:
return new CommissionedEmployee(r) ;
case HOURLY:
return new HourlyEmployee(r);
case SALARIED:
return new SalariedEmploye(r);
default:
throw new InvalidEmployeeType(r.type);
}
}
}
روابطی که بقیه ی سیستم نمی تواند ببینتشان [G23]. البته هر شرایطی یکتاست و مواقعی وجود دارد که من یک یا چند بخش از آن قانون را نقض میکنم.
در لیست 3-7 نام تابع مثالمان را از testableHtml به SetupTeardownIncluder.render تغییر دادم. این یک نام به مراتب بهتر است زیرا تابع را بهتر توصیف میکند. همچنین به هریک از توابع خصوصی نام توصیفی بهتری مانند isTestable یا SetupAndTeardownPages دادم. ارزش گذاری نامهای خوب کار سختی است. اصل Ward را بیاد بیاور: "وقتی هر روال تقریباً همان چیزی است که انتظار داشتید، آن موقع دارید تمیز کد میزنید." نیمی از نبرد برای دستیابی به آن اصل، انتخاب نامهای خوب برای توابع کوچکی است که یک کار را انجام میدهند. هرچه تابعی کوچکتر و متمرکزتر باشد، انتخاب نام توصیفی آسانتر است.
از ایجاد نام طولانی نترسید. یک نام توصیفی طولانی بهتر از یک اسم مبهم کوتاه است. یک نام توصیفی طولانی بهتر از یک توضیحات توصیفی طولانی است. از یک قرارداد نامگذاری استفاده کنید که اجازه میدهد چندین کلمه به راحتی در نامهای تابع خوانده شود ، و سپس از آن کلمات چندگانه استفاده کنید تا یک نامی برای تابع انتحاب کنید که می گوید چه کاری انجام میدهد.
از صرف وقت خود برای انتخاب نام نترسید. در واقع ، شما باید چندین نام مختلف را امتحان کنید و کد را با هر یک از نامها در محل بخوانید. IDE های مدرن مانند Eclipse یا IntelliJ تغییر نامها را بسیار ساده میکند. از یکی از آن IDE ها استفاده کنید و با نامهای مختلف تست کنید. تا زمانی انجام دهید که یکی از آنها، به همان توصیفی است که کد را نوشتید.
انتخاب نامهای توصیفی ، طراحی ماژول را در ذهن شما روشن میکند و به شما در بهبود آن کمک میکند. به هیچ وجه غیر معمولی نیست که جستجوی نام خوب منجر به بازنویسی مطلوب کد شود.
در نامگذاری یکپارچه عمل کنید. از همان عبارات ، اسمها و افعال در نامهای تابع استفاده کنید که برای ماژول های خود انتخاب می کنید. بطور مثال، نامهای includeSetupAndTeardownPages، includeSetupPages، includeSuiteSetupPage و includeSetupPage. اصطلاحات مشابه در این نامها به توالی اجازه میدهد داستانی را بازگو کنند. در واقع ، اگر من توالی بالا را به شما نشان دادم ، از خودتان می پرسید: "برای includeTeardownPages، includeSuiteTeardownPage و includeTeardownPage چه اتفاقی افتاد؟ " این چطوراست " . . تقریباً آنچه انتظار داشتید. "
تعداد ایدهآل آرگومان برای یک تابع صفر است (niladic). بعدی یک (monadic) و درنهایت دو (dyadic). در صورت امکان باید از سه آرگومان (triadic) اجتناب شود. بیش از سه مورد (polyadic) نیاز به توجیه بسیار ویژه دارند - و به هر حال نباید از آنها استفاده کرد.
آرگومان ها خیلی قوی هستند. آنها از قدرت مفهومی زیادی مصرف می کنند. به همین دلیل در مثال تقریباً شر همه آنها را کم کردم. بطور مثال StringBuffer را درنظر بگیرید. باید آن را به عنوان یک آرگومان استفاده میکردیم به جای اینکه آن را به عنوان یک متغیر تعریف کنیم ، اما پس خوانندگان ما باید هر بار که می دیدند ، آن را تفسیر کنند. هروقت داستانی که ماژول میگوید را میخوانید، includeSetupPage() ساده تر درک میشود تا includeSetupPageInto(newPageContent). آرگومان در سطح متفاوتی از انتزاع نسبت به نام تابع است و شما را مجبور میکند جزئیاتی را بشناسید (به عبارت دیگر StringBuffer) که در آن مرحله به خصوص اهمیتی ندارد.
آرگومان حتی از نقطه نظر تست هم سخت است. مشکل در نوشتن تمام موارد تست را تصور کنید تا اطمینان حاصل شود که تمام ترکیبات مختلف آرگومان ها به درستی کار می کنند. اگر آرگومانی وجود نداشته باشد ، این مسائل دیگر مهم نیستند. اگر یک آرگومان وجود داشته باشد ، خیلی سخت نیست. با دو آرگومان ، این مسئله کمی چالش برانگیزتر می شود. با بیش از دو آرگومان ، تست هر ترکیبی از مقادیر مناسب می تواند دلهره آور باشد.
درک آرگومانهای خروجی سخت تر از آرگومان های ورودی است. هنگامی که ما یک تابع را فرا می خوانیم ، به ایده عادت داریم که اطلاعات از طریق آرگومان ها وارد تابع می شود و از طریق مقدار بازگشتی از آن خارج می شود . ما معمولاً انتظار نداریم که اطلاعات از طریق آرگومان ها خارج شوند. بنابراین آرگومان های خروجی غالباً باعث کار دو برابر میشوند.
بعد بدون آرگومان، یک آرگومان ورودی بهترین است. SetupTeardownIncluder.render(pageData) تقریبا قابل درک است. واضح است که ما قصد داریم داده ها را در شیء PageData رندر کنیم.
دو دلیل بسیار رایج برای دادن یک آرگومان به تابع وجود دارد. ممکن است در مورد آن آرگومان در boolean fileExists(“MyFile”) سوال بپرسید. یا ممکن است شما بر اساس آن آرگومان کار کنید ، آن را به چیز دیگری تبدیل کرده و برگردانید. به عنوان مثال ، InputStream fileOpen(“MyFile”) یک رشته نام فایل را به مقدار بازگشتی InputStream تبدیل میکند. این دو استفاده همان چیزی است که خوانندگان با دیدن یک تابع ، انتظار دارند. شما باید نامهایی را انتخاب کنید که تمایز ایجاد میکند و همیشه از دو شکل در یک زمینه استفاده کنید. (به تفکیک فرمان پرسشی زیر مراجعه کنید.)
یک شکل کمی کمتر رایج، اما هنوز هم بسیار مفید برای یک تابع تک آرگومانی ، یک رویداد است. در این شکل ، یک آرگومان ورودی وجود دارد اما هیچ آرگومان خروجی وجود ندارد. منظور از برنامه کلی ، تعبیر فراخوانی به عنوان یک رویداد و استفاده از آرگومان برای تغییر وضعیت سیستم است ، برای مثال void passwordAttemptFailedNtimes(int attempts). با دقت از این فرم استفاده کنید. باید برای خواننده بسیار روشن باشد که این یک رویداد است. نامها و متن ها را با دقت انتخاب کنید.
سعی کنید از توابع monadic که این فرم ها را رعایت نمی کنند ، خودداری کنید، برای مثال void includeSetupPageInto(StringBuffer pageText). استفاده از آرگومان خروجی به جای مقدار بازگشتی برای یک تبدیل گیج کننده است. اگر یک تابع أرگومان ورودی خود را تغییر دهد ، تبدیل باید به عنوان مقدار بازگشتی ظاهر شود. درواقع، StringBuffer transform(StringBuffer in) بهتر است از void transform(StringBuffer out)، حتی اگر پیادهسازی در حالت اول آرگومان ورودی را بازگرداند. حداقل هنوز شکل تبدیل را دنبال میکند.
آرگومان های پرچم زشت هستند. دادن یک بولین به تابع یک عمل واقعاً وحشتناک است. بلافاصله امضای تابع را پیچیده میکند و با صدای بلند می گوید که این تابع بیشتر از یک کار را انجام میدهد. اگر پرچم درست باشد یک کار، و اگر پرچم نادرست باشد یک کار دیگر را انجام میدهد!
در لیست 3-7 ما چاره ای نداشتیم زیرا صدازنندگان آن پرچم داده بودند و من می خواستم دامنه ریفکتور را به تابع و زیر آن محدود کنم. همچنان صدازدن متد render(true) برای خواننده ضعیف گیج کننده است. بردن ماوس روی صدازننده و دیدن render(boolean isSuite) میتواند کمی کمک کند اما نه زیاد. ناچاریم تابع را به تابع renderForSuite() و renderForSingleTest() تقسیم کنیم.
درک تابع دو آرگومانی، سختتر از تابع تک آرگومانی است. بطور مثال، درک writeField(name) آسان تر است از writeField(output-Stream, name). گرچه معنای هر دو روشن است ، اما با اولین دیدن سرسری ، به راحتی معنی آن را می رساند. مورد دوم، به مکث کوتاه نیاز دارد تا بفهمیم پارامتر اول را نادیده بگیریم. و این البته در نهایت منجر به مشکلاتی می شود زیرا ما هرگز نباید بخشی از کد را نادیده بگیریم. قسمت هایی که نادیده می گیریم جایی است که اشکالات در آن پنهان میشوند.
البته زمانهایی وجود دارد که دو آرگومان مناسب است. بطور مثال، Point p = new Point(0,0)؛ کاملا منطقیست. نقاط دکارتی به طور طبیعی دو آرگومان را در بر می گیرد. در واقع ، از دیدن new Point(0) بسیار شگفت زده خواهیم شد. با این حال ، در این مورد ، دو آرگومان به اجزای یک مقدار استفاده می شود! در حالی که outputStream و name هیچ انسجام طبیعی ندارند.
حتی توابع دو آرگومانی بدیهی مانند assertEquals(expected, actual) مشکل زا هستند. چند بار از این در جایی که انتظار می رود باشد استفاده کردید؟ این دو آرگومان هیچ نظم طبیعی ندارند. ترتیب واقعی و مورد انتظار یک قرارداد است که برای یادگیری نیاز به تمرین دارد.
دو آرگومانی ها شر نیستند و مطمئناً باید آنها را بنویسید. با این حال ، باید بدانید که هزینه دارند و باید از مکانیسم هایی که در دسترس شماست، آنان را تبدیل به تک آرگومانی کنید. برای مثال، ممکن است متد writeField را عضوی از outputStream کنید تا بتوانید بگویید outputStream.writeField(name). یا ممکن است outputStream را عضوی از متغیرهای کلاس جاری کنید تا نیازی به دادنش به تابع نباشد. یا ممکن است کلاس جدیدی، بطور مثال FieldWriter، ایجاد کنید تا outputStream را در کانستراکتور اش بگیرد و یک مثد write داشته باشد.
توابعی که سه آرگومان را در بر می گیرد درکشان به مراتب سخت تر از دو آرگومانی هاست. مسائلی مانند ترتیب ، مکث و نادیده گرفتن، بیش از دو برابر شده است. پیشنهاد میکنم قبل از ایجاد یک تابع سه آرگومانه ، خیلی با دقت فکر کنید.
بطور مثال، شکل رایج overload متد assertEquals که سه آرگومان میگیرد را درنظر گیرید: assertEquals(message, expected, actual). چندبار message را خواندید و فکر کردید که expected است؟ بارها روی این سه آرگومانه خاص مکث کردم. در واقع ، هر بار که آن را می بینم ، دوباره کاری انجام می دهم و بعد یاد می گیرم که message را نادیده بگیرم.
از طرف دیگر، تابع سه آرگومانی ای وجود دارد که دردسرساز نیست: assertEquals(1.0, amount, .001). اگرچه این امر هنوز نیاز به یک برداشت دوباره دارد ، اما ارزش آن را دارد. همیشه خوب است یادآوری کنیم که برابری مقادیر اعشاری، یک چیز نسبی است.
وقتی به نظر میرسد تابع بیش از دو یا سه آرگومان نیاز دارد ، احتمالاً بعضی از این آرگومان ها باید در یک کلاس از نوع خود پیچیده شوند. به عنوان مثال ، تفاوت بین دو تعریف زیر را در نظر بگیرید:
Circle makeCircle(double x, double y, double radius); Circle makeCircle(Point center, double radius);
ممکن است کاهش تعداد آرگومان ها با ایجاد آبجکت های خارج از آنها تقلب به نظر برسد ، اما اینگونه نیست. وقتی گروههای متغیر با هم داده میشوند ، مثلا x و y در مثال بالا، احتمالاً بخشی از یک مفهوم هستند که سزاوار اسمی برای خود هستند.
بعضی اوقات میخواهیم تعداد متغیری از آرگومان را به یک تابع منتقل کنیم. بطور مثال، متد String.format را در نظر بگیرید:
String.format("%s worked %.2f hours.", name, hours);
اگر با همه آرگومان های متغیر به طور یکسان رفتار شوند ، همانطور که در مثال بالا وجود دارد، پس معادل یک آرگومان واحد از نوع list هستند. بنابراین، String.format درواقع یک متد دو آرگومانی است. درواقع، تعریف متد String.format که در زیر آمده، بوضوح دو آرگومانی بودنش را نشان میدهد.
public String format(String format, Object... args)
بنابراین این قوانین به همه، یکسان اعمال می شود. توابعی که آرگومانهای متغیر میگیرند، می توانند تک آرگومانی، دو آرگومانی و یا حتی سه آرگومانی باشند. اما این اشتباه است که آرگومان های بیشتری را به آن دهیم.
void monad(Integer... args);
void dyad(String name, Integer... args);
void triad(String name, int count, Integer... args);
انتخاب نامهای خوب برای یک تابع می تواند شروع کننده یک راه طولانی برای توضیح هدف تابع و ترتیب و هدف آرگومان ها باشد. در مورد توابع تک آرگومانی، تابع و آرگومان باید یک جفت فعل/اسم بسیار زیبا را باشند. بطور مثال، write(name) بسیار خوب است. حال “name” هرچه که باشد، قرار است نوشته(“written”) شود. حتی یک نام بهتر، میتواند writeField(name) باشد که میگوید “name” یک “field” است. این آخری، مثالی است از فرم کلمه کلیدی برای نام یک تابع. با استفاده از این فرم، نام آرگومان ها را در نام تابع رمزگذاری می کنیم. برای مثال، assertEquals بهتر است به شکل assertExpectedEqualsActual(expected, actual) نوشته شود. این، به شدت مشکل یادآوری ترتیب آرگومان ها را کاهش میدهد.
عوارض جانبی دروغ است. تابع شما قول انجام یک کار را میدهد ، اما کارهای پنهان دیگری را نیز انجام میدهد. بعضی اوقات تغییرات غیرمنتظره ای در متغیرهای کلاس خاص خود ایجاد میکند. گاهی اوقات پارامترها را به تابع یا globals system منتقل میکند. در هر دو صورت ، آنها گراه کننده و آسیب زا هستند که غالباً به ایجاد پیوندهای موقتی عجیب و غریب و ایجاد وابستگی های ترتیبی منجر میشوند.
به عنوان مثال ، عملکرد به ظاهر بی ضرر لیست 3-6 را در نظر بگیرید. این تابع از یک الگوریتم استاندارد برای نظیرسازی یک نام کاربری به رمز عبور انجام میدهد. اگر نظیرسازی صورت گرفت، مقدار true و در غیر اینصورت هر اتفاق دیگری بیوفتد، false برمیگرداند. اما یک عارضه جانبی دارد. میتوانید آن را تشخیص دهید؟
Listing 3-6
UserValidator.java
public class UserValidator
{
private Cryptographer cryptographer;
public boolean checkPassword(String userName, String password)
{
User user = UserGateway.findByName(userName);
if (user != User.NULL)
{
String codedPhrase = user.getPhraseEncodedByPassword();
String phrase = cryptographer.decrypt(codedPhrase, password);
if ("Valid Password".equals(phrase))
{
Session.initialize();
return true;
}
}
return false;
}
}
عارضا جانبی در صدازدن Session.initialize() است. تابع checkPassword طبق اسمش، رمز عبور را چک میکند. نامش نشان نمیدهد که session را ایجاد میکند. پس هرگاه صدازننده که اعقاد دارد کار تابع طبق اسمش خواهد بود، ریسک پاک شدن دیتا session موجود را ایجاد میکند.
این عارضه جانبی، یک جفت موقتی ایجاد میکند. تابع checkPassword فقط در زمان خاصی میتواند صدازده شود (به عبارت دیگر، زمانیکه ایجاد session ایمن است). اگر خارج از ترتیب صدا زده شود، ممکن است دیتا session از دست رود. جفت موقتی گیج کننده است، بخصوص زمانی که در عوارض جانبی پنهان شده باشند. اگر مجبورید که یک جفت موقتی داشته باشید، باید این موضوع را در نام تابع مشخص کنید. در مثال ما تابع باید به checkPasswordAndInitializeSession تغییر نام یابد که مشخص است قانون "انجام دادن یک کار" را نقض میکند.
آرگومان ها به طور طبیعی به عنوان ورودی های یک تابع تعبیر میشوند. اگر بیش از چند سال مشغول برنامهنویسی هستید ، مطمئنم که روی یک آرگومانی کار کردید که در واقع یک خروجی بود نه ورودی. مثلا:
appendFooter(s);
آیا این تابع s را به عنوان پانویس به چیزی اضافه میکند؟ یا پانویسی را به s اضافه میکند؟ آیا s یک ورودیست یا خروجی؟ زمان زیادی طول نمیکشد که به تابع نگاه کنیم و ببینیم:
public void appendFooter(StringBuffer report)
اما مشخص شدن مسئله به بهای چک کردن پیاده سازی تابع است! هرچیزی که شمارا مجبور به چک کردن امضا تابع کند، دوباره کاریست. یک حواس پرتیست که باید از آن اجتناب شود.
در روزگار قبل از برنامهنویسی شی گرا، گاهی وجود آرگومان خروجی لازم بود. هرچند نیاز به آرگومان خروجی در زبان های شی گرا ناپدید شد زیرا این هدف به عنوان آرگومان خروجی عمل میکند. به عبارت دیگر، بهتر است appendFooter به شکل زیر فراخوانی شود:
report.appendFooter();
به طور کلی باید از آرگومان های خروجی جلوگیری کرد. اگر تابع شما باید وضعیت چیزی را تغییر دهد ، وضعیت آبجکت آن را تغییر دهید.
توابع یا باید کاری انجام دهند یا به چیزی پاسخ دهند ، اما نه هر دو اینها. یا تابع شما باید وضعیت یک شی را تغییر دهد ، یا باید برخی از اطلاعات مربوط به آن شی را برگرداند. انجام هر دو، اغلب منجر به سردرگمی می شود. برای مثال، تابع زیر را در نظر بگیرید:
public boolean set(String attribute, String value);
این تابع مقدار یک ویژگی مشخص شده را تعیین میکند و در صورت موفقیت true و اگر چنین صفتی وجود نداشت، مقدار false برمیگرداند. این منجر به statements عجیب مثل این می شود:
if (set("username", "unclebob"))...
این را از نقطه نظر خواننده تصور کنید. چه معنی میدهد؟ آیا میپرسد که خصیصه “username” قبلا “unclebob” مقداردهی شده؟ یا میپرسد که خصیصه "username" با موفقیت با “unclebob” مقداردهی شده است؟ استنباط از این فراخوانی دشوار است زیرا مشخص نیست که کلمه "set" فعل است یا صفت.
نویسنده قصد دارد که "set" فعل باشد، اما در متن if statement بتظر میرسد صفت باشد. بنابراین statement به این صورت خوانده می شود که: "اگر خصیصه usename قبلا unclebob مقداردهی شده" و "مقداردهی خصیصه username به unclebob انجام شد" اتفاق نیوفتاد. ما میتوانیم با تغییر نام تابع set به setAndCheckIfExists سعی کنیم این مسئله را برطرف کنیم ، اما این به خوانایی if statement کمک نمیکند. راه حل واقعی جدا کردن دستور از کوئری است تا ابهام رخ ندهد.
if (attributeExists("username")) {
setAttribute("username", "unclebob");
...
}
بازگشت کدهای خطا از توابع فرمان یک نقض ظریف در تفکیک کوئری فرمان است. این دستورات را برای استفاده به عنوان عبارات در if statementها ، ترویج میکند.
if (deletePage(page) == E_OK)
این امر از سردرگمی فعل/صفت رنج نمی برد بلکه منجر به ساختارهای عمیقاً تو در تو می شود. هنگامی که یک کد خطا را برگردانید ، این مشکل را ایجاد می کنید که صدازننده باید بلافاصله با خطا دست و پنجه نرم کند.
if (deletePage(page) == E_OK) {
if (registry.deleteReference(page.name) == E_OK) {
if (configKeys.deleteKey(page.name.makeKey()) == E_OK) {
logger.log("page deleted");
}
else {
logger.log("configKey not deleted");
}
}
else {
logger.log("deleteReference from registry failed");
}
}
else {
logger.log("delete failed");
return E_ERROR;
}
از طرف دیگر ، اگر به جای کدهای خطای برگشتی از exceptionها استفاده می کنید ، میتوانید پردازش کد خطا را از مسیر کد جدا کرده و ساده سازی شود.
try
{
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
catch (Exception e)
{
logger.log(e.getMessage());
}
بلوک های try/catch در نوع خود زشت هستند. ساختار کد را گیج کننده می کنند و پردازش خطا را با پردازش عادی مخلوط می کنند. بنابراین بهتر است بدنه بلوک های try/catch را استخراج کنید و توابع مربوطه را بنویسید.
public void delete(Page page) {
try {
deletePageAndAllReferences(page);
}
catch (Exception e) {
logError(e);
}
}
private void deletePageAndAllReferences(Page page) throws Exception {
deletePage(page);
registry.deleteReference(page.name);
configKeys.deleteKey(page.name.makeKey());
}
private void logError(Exception e) {
logger.log(e.getMessage());
}
در بالا، تابع delete فقط مربوط به پردازش خطا است. فهم آن آسان است و بعد می توان براحتی نادیده گرفتش. تابع DeletePageAndAllReferences همه چیز مربوط به فرآیندهای حذف کامل یک صفحه است. مدیریت خطا را می توان نادیده گرفت. این یک جداسازی خوب را فراهم میکند که درک و تغییر کد را ساده تر میکند.
توابع باید یک کار را انجام دهند. مدیریت خطا نیز یک کار است. بنابراین ، تابعی که مدیریت خطاها را انجام میدهد ، نباید کاری دیگر انجام دهد. این دلالت دارد (مانند مثال بالا) که اگر کلمه کلیدی try در یک تابع وجود داشته باشد ، باید اولین کلمه در تابع باشد و بعد از بلوک های catch/finally نیز چیزی نباید وجود داشته باشد.
بازگرداندن کدهای خطا معمولاً دلالت بر این دارد که تعدادی کلاس یا enum وجود دارد که در آن همه کدهای خطا تعریف میشوند.
public enum Error
{
OK,
INVALID,
NO_SUCH,
LOCKED,
OUT_OF_RESOURCES,
WAITING_FOR_EVENT;
}
کلاس هایی مانند این یک آهنربای وابستگی هستند. بسیاری از کلاس های دیگر باید آنها را import و استفاده کنند. بنابراین ، هنگامی که Error enum تغییر میکند ، باید تمام کلاسهای دیگر مجدداً deploy و کامپایل شوند. این، یک فشار منفی به کلاس Error وارد میکند. برنامهنویسان نمی خواهند خطاهای جدیدی اضافه کنند زیرا پس از آن مجبورند همه چیز را مجدداً build و deploy کنند. بنابراین آنها به جای اضافه کردن کدهای جدید ، از کدهای خطای قدیمی استفاده مجدد می کنند.
هنگامی که شما به جای کدهای خطا از exception استفاده می کنید ، exceptionهای جدید مشتقات کلاس exception هستند. می توان آنها را بدون مجبور به کامپایل یا deploy دوباره اضافه كرد.
دوباره به لیست 1-3 با دقت نگاه کنید و متوجه خواهید شد که یک الگوریتم وجود دارد که چهار بار تکرار می شود ، یک بار برای هر یک از موارد SetUp ، SuiteSetUp ، TearDown و SuiteTearDown. مشاهده این تکثیر کار آسانی نیست زیرا این چهار نمونه با کد دیگر درهم آمیخته اند و به طور یکنواخت کپی نشپه اند. با این وجود ، تکثیر یک مشکل است زیرا کد را منبسط میکند و در صورت نیاز به تغییر الگوریتم نیاز به اصلاح چهار برابری دارد. همچنین امکان خطا چهار برابری برای حذف نکردن آن صورت می گیرد.
این تکثیر با استفاده از include متد در لیست 3-7 اصلاح شد. دوباره آن کد را بخوانید و متوجه شوید که چگونه با کاهش آن تکرار ، قابلیت خواندن کل ماژول افزایش می یابد.
تکثیر ممکن است ریشه همه شر در نرم افزار باشد. بسیاری از اصول و شیوه ها به منظور کنترل یا از بین بردن آن ایجاد شده است. به عنوان مثال ، در نظر بگیرید که همه فرم های عادی بانک اطلاعاتی Codd برای از بین بردن تکثیر در داده ها استفاده میشوند. همچنین در نظر بگیرید که چگونه برنامهنویسی شی گرا با متمرکز کردن کد در کلاس های پایه از تکرار جلوگیری میکند. برنامهنویسی ساخت یافته، برنامهنویسی جهت گرا و برنامهنویسی مؤلفه ای ، کاملاً استراتژی هایی برای از بین بردن تکثیر است. به نظر میرسد از زمان اختراع زیرتوالی ها ، نوآوری ها در توسعه نرم افزار تلاشی مداوم برای از بین بردن تکثیر از کد منبع ما بوده است.
برخی از برنامهنویسان از قوانین برنامهنویسی ساخت یافته Edsger Dijkstra پیروی می کنند. Dijkstra گفت كه هر تابع و هر بلوك درون یك تابع باید دارای یك ورودی و یك خروجی باشد. پیروی از این قوانین بدان معنی است که فقط باید یک عبارت برگشتی در یک تابع داشته باشید ، بدون break یا continue در یک loop، وهرگز، بدون goto. در حالی که ما با اهداف و اصول برنامهنویسی ساختاری دلسوز هستیم ، وقتی توابع بسیار کوچک باشند ، این قوانین سود کمتری دارند. فقط در توابع بزرگتر اینگونه قوانین مزایای قابل توجهی را ارائه میدهند.
بنابراین اگر توابع خود را کوچک نگه دارید، چندین return ،break یا continue هیچ ضرری ندارد و حتی گاهی حتی می تواند بیانگرتر از قانون تک ورودی، تک خروجی باشد. از طرف دیگر، goto فقط در عملکردهای بزرگ معنی دارد، بنابراین باید از آن اجتناب کرد.
نوشتن نرم افزار مانند هر نوع نوشتن دیگر است. وقتی مقاله ای می نویسید ، ابتدا افکار خود را می نویسید ، سپس با آن ور می روید تا خوب خوانا شود. پیش نویس اول ممکن است دست و پا چلفتی و سازماندهی نشده باشد ، بنابراین شما آنرا کلمه بندی می کنید ، آنرا دوباره سازی کرده و مجدداً آن را اصلاح می کنید تا اینکه خواننده مطالب را آنطور بخواند که می خواهید.
وقتی توابع را می نویسم، طولانی و پیچیده میشوند. حلقه های تو در تو و درهم زیادی دارند. دارای لیست طولانی آرگومان هستند. نامها دلبخواه هستند و کد تکراری نیز وجود دارد. اما من همچنین یک مجموعه تست واحد دارم که هرکدام از آن خطوط دست و پا چلفتی کد را پوشش میدهد.
بنابراین ، آن کد را مشت و مال داده و اصلاح میکنم ، توابع را تقسیم میکنم ، نامها را تغییر می دهم و تکرار را حذف میکنم. متدها را کوچک میکنم و دوباره مرتبشان میکنم. بعضی اوقات کلاس ها را که تست ها را پشت سر گذاشتند، حذف میکنم.
در پایان ، توابعی باقی می مانند که از قوانینی که در این فصل تنظیم کرده ام پیروی می کنند. در شروع، آنان را به این شکل نمی نویسم. فکر نمیکنم کسی بتواند.
هر سیستم از یک زبان خاص دامنه ساخته شده که توسط برنامهنویسان برای توصیف آن سیستم طراحی شده است. توابع فعلهای آن زبان و کلاسها اسمها هستند. این چندان مفهوم قدیمی مسخره نیست که اسم ها و افعال موجود در یک سند مورد نیاز اولین حدس کلاس ها و عملکردهای یک سیستم هستند. در عوض، این یک حقیقت بسیار قدیمی است. هنر برنامهنویسی هنر طراحی زبان همیشه بوده.
برنامهنویسان ارشد ، سیستم ها را به عنوان داستان هایی که باید گفته شود فکر می کنند نه برنامههایی که باید نوشته شوند. آنها از امکانات زبان برنامهنویسی انتخاب شده خود برای ساختن زبانی بسیار غنی تر و رساتر استفاده می کنند که می توان برای گفتن آن داستان استفاده کرد. بخشی از آن زبان خاص دامنه سلسله مراتبی از توابع است که کلیه اقدامات انجام شده در آن سیستم را توصیف میکند. در یک عمل هنری بازگشتی، این اقدامات نوشته شده اند تا از همان زبان دامنه-خاص برای گفتن قسمت کوچک خود از داستان استفاده شود.
این فصل درمورد مکانیزم نوشتن خوب توابع بود. اگراین قوانین را رعایت کنید ، توابع شما کوتاه ، به خوبی نامیده شده و به خوبی سازماندهی میشوند. اما هرگز فراموش نکنید که هدف واقعی شما این است که داستان سیستم را بگویید و توابعی که شما می نویسید باید کاملاً با هم در یک زبان واضح و دقیق قرار بگیرد تا به شما در بیان آن کمک کند.
Listing 3-7 SetupTeardownIncluder.java
package fitnesse.html;
import fitnesse.responders.run.SuiteResponder;
import fitnesse.wiki.*;
public class SetupTeardownIncluder {
private PageData pageData;
private boolean isSuite;
private WikiPage testPage;
private StringBuffer newPageContent;
private PageCrawler pageCrawler;
public static String render(PageData pageData) throws Exception {
return render(pageData, false);
}
public static String render(PageData pageData, boolean isSuite) throws Exception
{
return new SetupTeardownIncluder(pageData).render(isSuite);
}
private SetupTeardownIncluder(PageData pageData)
{
this.pageData = pageData;
testPage = pageData.getWikiPage();
pageCrawler = testPage.getPageCrawler();
newPageContent = new StringBuffer();
}
private String render(boolean isSuite) throws Exception
{
this.isSuite = isSuite;
if (isTestPage())
includeSetupAndTeardownPages();
return pageData.getHtml();
}
private boolean isTestPage() throws Exception
{
return pageData.hasAttribute("Test");
}
private void includeSetupAndTeardownPages() throws Exception
{
includeSetupPages();
includePageContent();
includeTeardownPages();
updatePageContent();
}
private void includeSetupPages() throws Exception
{
if (isSuite)
includeSuiteSetupPage();
includeSetupPage();
}
private void includeSuiteSetupPage() throws Exception
{
include(SuiteResponder.SUITE_SETUP_NAME, "-setup");
}
private void includeSetupPage() throws Exception
{
include("SetUp", "-setup");
}
private void includePageContent() throws Exception
{
newPageContent.append(pageData.getContent());
}
private void includeTeardownPages() throws Exception
{
includeTeardownPage();
if (isSuite)
includeSuiteTeardownPage();
}
private void includeTeardownPage() throws Exception
{
include("TearDown", "-teardown");
}
private void includeSuiteTeardownPage() throws Exception
{
include(SuiteResponder.SUITE_TEARDOWN_NAME, "-teardown");
}
private void updatePageContent() throws Exception
{
pageData.setContent(newPageContent.toString());
}
private void include(String pageName, String arg) throws Exception
{
WikiPage inheritedPage = findInheritedPage(pageName);
if (inheritedPage != null) {
String pagePathName = getPathNameForPage(inheritedPage);
buildIncludeDirective(pagePathName, arg);
}
}
private WikiPage findInheritedPage(String pageName) throws Exception
{
return PageCrawlerImpl.getInheritedPage(pageName, testPage);
}
private String getPathNameForPage(WikiPage page) throws Exception
{
WikiPagePath pagePath = pageCrawler.getFullPath(page);
eturn PathParser.render(pagePath);
}
private void buildIncludeDirective(String pagePathName, String arg)
{
newPageContent
.append("\n!include ")
.append(arg)
.append(" .")
.append(pagePathName)
.append("\n");
}
}