آیا تاکنون در مورد سازماندهی، نظم و ترتیب کدهایی که داخل یک کلاس قرار دارند فکر کردهاید؟ بسیاری از دولوپرها در حین کدنویسی -بهخصوص تازهکارها- خیلی روی نظم سورسکد تمرکز نمیکنند و این در حالی است که اگر با قانون Stepdown آشنایی داشته باشند، خواهند دید که با رعایت این قانون به نحوی کاملاً حرفهای میتوان کدنویسی کرد که در صورت نیاز به ریفکتور کردن کدها در آینده (خواه توسط خود دولوپر خواه توسط سایر دولوپرها)، با مشکل چندانی روبهرو نخواهند شد.
برخی دولوپرها، فانکشنهای موجود در یک کلاس را به صورت تصادفی و بدون نظم خاصی مینویسند؛ به قول معروف، باری به هر جهت عمل میکنند! برای اینکه ما از یک برنامهنویس معمولی به یک برنامهنویس حرفهای مبدل شویم، باید عادات بَد قدیمی خود را اصلاح کنیم (گرچه این کار ساده به نظر میآید، ولی بسیار مهم است.)
آشنایی با قانون Stepdown در پروسهٔ کدنویسی
این قانون میگوید که کدهای داخل کلاس، باید از منظر بالا به پایین خوانایی داشته باشند به طوری که هر فانکشن نسبت به فانکشن قبلیاش، یک مرحله نزول در اصطلاحاً Abstraction (انتزاع) داشته باشد. به عبارت دیگر، ترتیب فانکشنها نباید به صورت رَندوم باشد؛ در واقع، یک فانکشن فراخوانیکننده (Caller) باید همیشه بالای فانکشن فراخوان شونده (Callee) نوشته شده باشد. برای روشنتر شدن این موضوع، یک نمونه کد بَد را در ادامه میبینیم:
private void serve() {
wife.give(fryingPan.getContents(20, PERCENT));
self.give(fryingPan.getContents(80, PERCENT)); // huehuehue
}
private void addEggs() {
fridge
.getEggs()
.forEach(egg -> fryingPan.add(egg.open());
}
private void cook() {
fryingPan.mixContents();
fryingPan.add(salt.getABit());
fryingPan.mixContents();
}
public void makeBreakfast() {
addEggs();
cook();
serve();
}
همانطور که مشاهده میکنید، آرایش تصادفی کدها درک آنها را بسیار سختتر میکند. همچنین ممکن است اصلاً دنبال درک جزئیات کدها نباشید و تنها میخواهید بدانید مثلاً Breakfast (صبحانه) چگونه سرو میشود ولی بههمریختگی کدها و آرایش نامناسبشان، باعث میشود مجبور شوید بسیاری از جزئیاتی که نمیخواستید را هم بخوانید. در کدی که در ادامه مشاهده میشود، ترکیب مراحل مختلف Abstraction که اصلاً خوب نیست صورت گرفته است:
public void makeBreakfast() {
addEggs();
cook();
wife.give(fryingPan.getContents(20, PERCENT));
self.give(fryingPan.getContents(80, PERCENT)); // huehuehue
}
private void addEggs() {
fridge
.getEggs()
.forEach(egg -> fryingPan.add(egg.open());
}
private void cook() {
fryingPan.mixContents();
fryingPan.add(salt.getABit());
fryingPan.mixContents();
}
به عبارت دیگر، باید متدها را به صورتی پیادهسازی کرد که مرحلهٔ یکسانی از انتزاع را دربربگیرند؛ در مثال بالا، با اینکه متد ()makeBreakfast خیلی طولانی نیست، باز هم خواندن آن نسبتاً سخت است و برای رفع این مشکل، باید فرایندهای مختلف را از داخل آن متد خارج کرده و در متدهای دیگری -که معمولاً از نوع private هستند- قرار داد. به طور کلی، برای درست کردن صبحانه، شما باید:
- تخممرغها را داخل ماهیتابه بشکنید
- آنها را بپزید
ـ 20٪ آن را به همسرتان بدهید و 80٪ را برای خود بردارید!
با در نظر گرفتن این الگوریتم، برای مشاهدهٔ یک نمونه کد خوب، میتوان سورسکد زیر را مد نظر قرار داد:
public void makeBreakfast() {
addEggs();
cook();
serve();
}
private void addEggs() {
fridge
.getEggs()
.forEach(egg -> fryingPan.add(egg.open());
}
private void cook() {
fryingPan.mixContents();
fryingPan.add(salt.getABit());
fryingPan.mixContents();
}
private void serve() {
wife.give(fryingPan.getContents(20, PERCENT));
self.give(fryingPan.getContents(80, PERCENT)); // huehuehue
}
اگر از منظر بالا به پایین نگاه کنیم، انتزاع در فانکشنهای پشت سر هم دارای ردهٔ یکسانی هستند، ولی با هر فراخوانی در فانکشنها، یک رده از انتزاع کاهش مییابد.
حال یکسری سؤال پیش میآید مثل اینکه اگر فانکشن ردهٔ پایینتر توسط دو یا تعداد بیشتری از فانکشنهای رده بالاتر مورد استفاده قرار گیرد چهکار باید کرد؟ در پاسخ به این سؤال بایستی گفت که فانکشن ردهٔ پایینتر را بعد از آخرین محل کاربرد آن قرار دهید:
public void makeBreakfast() {
addEggs();
cook();
serve();
}
// addEggs(), cook()
public void makeDinner() {
// stuff
serve();
}
private void serve() {
wife.give(fryingPan.getContents(20, PERCENT));
self.give(fryingPan.getContents(80, PERCENT)); // huehuehue
}
سؤال دیگر اینکه اگر فانکشنهای یکسانی داخل یک کلاس وجود داشت چه باید کرد؟ این قضیه در واقع مسالهٔ رایجی است؛ برنامهنویسها معمولاً ذهن سازمان یافتهای داشته، قرینه و چیزهای اینچنینی را دوست دارند و اگر چیزی مثل حالت زیر را ببینند، اصلاً خوشحال نخواهند شد:
public void makeBreakfast() {
addEggs();
cook();
serveBreakfast();
}
// addEggs(), cook()
public void makeDinner() {
// stuff
serveDinner();
}
private void serveBreakfast() {
wife.give(fryingPan.getContents(20, PERCENT));
self.give(fryingPan.getContents(80, PERCENT)); // huehuehue
}
private void serveDinner() {
wife.give(pot.getContents(40, PERCENT));
self.give(pot.getContents(60, PERCENT)); // still!
}
معمولاً دولوپرها کدها را با متدهای شبیه به متد مورد نظر خود دنبال نمیکنند؛ پس سعی کنید ساختار بالا به پایین را همچون نمونه کد زیر حفظ کنید:
public void makeBreakfast() {
addEggs();
cook();
serveBreakfast();
}
// addEggs(), cook()
private void serveBreakfast() {
wife.give(fryingPan.getContents(20, PERCENT));
self.give(fryingPan.getContents(80, PERCENT)); // huehuehue
}
public void makeDinner() {
// stuff
serveDinner();
}
private void serveDinner() {
wife.give(pot.getContents(40, PERCENT));
self.give(pot.getContents(60, PERCENT)); // still!
}
در نهایت هم به این سؤال میرسیم که اگر بخواهیم یک Test بنویسیم، چهکار باید کنیم؟ در پاسخ به این سؤال بایستی گفت که باز هم با شرایطی مواجه شدهایم که بسیار رایج است. در اغلب موارد، متدهایی از جنس Factory وجود دارند تا دیتای مورد نظر برای تست را برایمان آماده کنند:
@Test
public void breakfastShareMakesHappiableWifeHappy() {
Wife wife = happiableWife();
Husband husband = new TypicalMan(wife);
husband.makeBreakfast();
assertTrue(wife.isHappy());
}
@Test
public void dinnerShareMakesAngryWifeLessAngry() {
Wife wife = angryWife();
Husband husband = new TypicalMan(wife);
Long initialAnger = wife.getAnger();
husband.makeDinner();
assertTrue(initialAnger > wife.getAnger());
}
private Wife happiableWife() {
Wife wife = new Wife();
wife.set... // complicated stuff
wife.set...
wife.set...
...
return wife;
}
private Wife angryWife() {
Wife wife = new Wife();
wife.tellStupidJoke(); // far easier!
return wife;
}
گرچه مواردی اینچنینی کمی چالشبرانگیز هستند، اما باز هم سعی کنید به قانون Stepdown (یا بالا به پایین) پایبند بمانید؛ در اکثر مواقع، شاید سعی کنید کدهای جدیدتر را به آخر کدهایتان اضافه کنید چرا که بیشتر وقتها دربارهٔ کدنویسی صحیح، تنبلی میکنیم.
اگر تنظیمات مربوط به تست نیاز به کار زیادی داشته باشد و درک تست را سختتر میکند، این بدان معنا است که باید طراحیمان را بهبود ببخشیم و به جای درج کدهای جدید در آخر کد و پشت سر هم گذاشتن آنها، از چند دیزاین پترن مناسب استفاده کنیم (برای آشنایی بیشتر با مفهوم دیزاین پترن دورهٔ آشنایی با الگوهای طراحی مراجعه نمایید.) به طور مثال داریم:
@Test
public void breakfastShareMakesHappiableWifeHappy() {
Wife wife = happiableWife();
Husband husband = new TypicalMan(wife);
husband.makeBreakfast();
assertTrue(wife.isHappy());
}
private Wife happiableWife() {
Wife wife = new Wife();
wife.set... // complicated stuff
wife.set...
wife.set...
...
return wife;
}
@Test
public void dinnerShareMakesAngryWifeLessAngry() {
Wife wife = angryWife();
Husband husband = new TypicalMan(wife);
Long initialAnger = wife.getAnger();
husband.makeDinner();
assertTrue(initialAnger > wife.getAnger());
}
private Wife angryWife() {
Wife wife = new Wife();
wife.tellStupidJoke(); // far easier!
return wife;
}
سخن پایانی
قانون Stepdown بیان میکند که باید کدهایمان را سازماندهی کنیم تا بتوان آنها را از بالا به پایین به راحتی دنبال کرد و اینکه با هر فراخوانی فانکشنها، یک سطح از انتزاع کاهش مییابد. این قضیه حتی اگر از یک فانکشن چندین و چند بار استفاده شود یا فانکشنهای مشابهی در همان کلاس وجود داشته باشد یا حتی در حال نوشتن تست باشیم نیز برقرار است.
در آخر اینکه چه از این قانون استفاده کنید و چه نکنید، در رویکرد کدنویسی مورد استفادهٔ خود ثابتقدم باشید؛ بدین ترتیب، اگر دولوپر دیگری شروع به کار با کدهای شما کرد، اگر روش سازماندهی فانکشنهای شما را درک کند، میتواند آن را به دیگر بخشهای پروژه نیز تعمیم دهد و در درک بقیه کدها راحتتر پیش برود.
حال نوبت به نظرات شما میرسد. آیا تاکنون قانون Stepdown را خواه به صورت خودآگاه و خواه به صورت ناخودآگاه در کدنویسی مورد استفاده قرار دادهاید؟ نظرات، دیدگاهها و تجربیات خود را در مورد این سبک کدنویسی با سایر کاربران سکان آکادمی به اشتراک بگذارید.