چندی پیش شخص از من خواست تا به کدی که نوشته بود نگاهی بیندازم. این کد را در انتهای این پست آوردهام. این کد یک مسالهی دشوار را حل میکند. مطمئن هستم که بیشتر از ۹۰ درصد از برنامهنویسها و دانشجویان رشتهی کامپیوتر قادر به حل کردن این مساله نیستند (اگر فکر میکنید که جز این ۹۰ درصد نیستید امتحان کنید). من قصد ندارم که در این مورد که این کد چقدر خوب یا بد این مساله را حل میکند نظری بدهم. فقط میخواهم چند سطر در مورد این موضوع که چطور میتوان این کد را بهتر کرد توضیح بدهم.
نوشتن برنامه برای حل کردن مسایل دنیای واقعی با استفاده از کامپیوتر کار بسیار پیچیدهای است و برنامهنویسها باید هر کاری میتوانند انجام دهند تا این پیچیدگی کم شود. یکی از این پیچیدگیها این است که در هر برنامه تعداد زیادی متغیر تعریف میشود. حتی اگر برنامه کوچک باشد هیچکس به خاطر نمیآورد که متغیر x که در فلان سطر از فلان فایل استفاده شده است کجا تعریف شده است، چرا تعریف شده است، چه مقداری در آن ذخیره میشود، و …
برای اینکه پیچیدگی برنامهها کمتر شود باید کاری کرد که افرادی که کد را میخوانند (چه به قصد فهمیدن، چه به قصد رفع اشکال، چه به قصد بهبود دادن) به سادگی بتوانند بفهند که هر متغیر به چه منظور تعریف شده و چه مقداری در أن ذخیره میشود.
در یکی از سطرهای این برنامه این دستور را میتوانید پیدا کنید:
k = 3;
برای اینکه بفهمید این دستور چه کاری انجام میدهد باید بدانید که متغیر k برای چه منظوری تعریف شده است. برای اینکه بفهمید k برای چه منظوری تعریف شده است باید سطری که متغیر k در آن تعریف شده را پیدا کنید و اگر خوششانس باشید در کنار آن توضیح داده شده است که k برای چه تعریف شده است. اگر خوششانس نباشید باید فردی که این کد را نوشته پیدا کنید و از او بپرسید که چرا k را تعریف کرده است. باز هم اگر آدم خوششانسی باشید خودتان کد را نوشتهاید و با کمی فکر کردن یادتان میآید که چرا k را تعریف کردهاید. در غیر این صورت باید از پشت دستگاه بلند شوید و دنبال فردی که کد را نوشته بگردید . اگر در این حالت هم خوششانس باشید فرد در دسترس است و میتوانید از او سوال بپرسید. نمیخواهم بیشتر از این بحث را با توضیح دادن سناریوهای مختلف طولانی کنم. ولی در یک جمله در تمام این سناریوها لازم است که شما چند لحظه تمرکز خود را از روی مسالهی اصلی بردارید و به یک موضوع فرعی (متغیر k) فکر کنید. لازم نسیت یادآوری کنم که مسالهی اصلی به اندازهی کافی پیچیده است و به تمرکز زیادی نیاز دارد (هیچ کس دوست ندارد هنگاه راه رفتن یک تخم مرغ را به وسیلهی یک قاشق در دهانش حمل کند).
این دو دستور را با هم مقایسه کنید:
k = 3;
و
subStringLength = 3;
یا حتی اگر به جز زبان مادری خود زبان دیگری نمیدانید
touleZirReshte = 3;
وقتی یک نفر دستور اول را میخواند هیچ برداشتی به او دست نمیدهد. ولی وقتی دستور دوم (یا سوم) را میخواند خیلی راحت حدس میزند که این دستور طول زیررشته را برابر با ۳ قرار میدهد. دقت کنید که در همین پاراگراف هم برای اینکه ببینید دستور اول و دوم و سوم چه بودند باید تمرکز خود را از روی پاراگراف برمیداشتید و چند بار به پاراگرافهای بالاتر مراجعه میکردید.
متاسفانه در این کدِ این برنامه این نوع متغیرها کم نیستند. i , j , m , temp, sub. دلیل این امر شاید به فرهنگ نادرستی برمیگردد که توسط کتابهای آموزش برنامهنویسی ترویج داده میشود. بسیاری از کتابهای آموزش برنامهنویسی نامهای اینچنینی برای متغیرها انتخاب میکنند. بخشی از أن هم به فرهنگی بر میگردد که در بین ریاضیدانها استفاده میشود. ریاضیدانها معمولا از نمادهای تک حرفی برای بیان روابط و معادلهها استفاده میکنند. اما کد یک برنامهی کامپیوتری با اثبات یک قضیه ریاضی خیلی تفاوت دارد و نباید از فرهنگ رایج ریاضیدانها برای نوشتن برنامههای کامپیوتری استفاده کرد (مگر أنکه به سادهتر شدن مساله کمک کند).
همین موضوع در مورد نام تابعهایی که در برنامه تعریف شدهاند نیز صدق میکند. تقریبا تمام تابعهای تعریف شده در این برنامه نامهای عجیبی دارند که به هیچ وجه کاری که انجام میدهند را القا نمیکنند. برای مثال دستور زیر را درنظر بگیرید:
query(input, k);
به نظر شما این دستور چه کاری انجام میدهد؟ آیا k بار برای پیدا کردن input جست و جو انجام میدهد؟ آیا در کاراکتر kام input جست و جو انجام میدهد؟ آیا …
برای پیدا کردن جواب باید حتما به جایی که متغیرهای k و input و تابع query تعریف شده است مراجعه کنید و توضیحات مربوط به أنها را بخوانید (اگر خوششانس باشید). حالا دستور زیر را در نظر بگیرید.
splitIntoSubstrings(inputString, substringLength);
به سادگی میتوان فهمید که این دستور رشتهی inputString را به زیر رشتههایی با طول substringLength تقسیم میکند.
همین موضوع در مورد سایر تابعها به خصوص تابع compute صدق میکند. برای مثال دستور زیر را در نظر بگیرید.
compute(str.substring(m).concat(str.substring(0, len - (strLen - m))));
در مورد اینکه این دستور بیشتر به یک فاجعه شباهت دارد تا یک دستور که توسط یک برنامهنویس خوب نوشته میشود خیلی حرف دارم و احتمالا یک پست مجزا در مورد آن خواهم نوشت. ولی در حال حاضر فقط این موضوع را یادآوری میکنم که یکی از بدترین اسمهایی که برای یک تابع میتوان انتخاب کرد همین compute است. برخی دیگر از این اسامی عبارتند از process, analyze, calculate, do, perform, init, initilize, start. این نامها به این دلیل نامناسب هستند که هیچ اطلاعاتی در مورد اینکه این تابعها چه کاری انجام میدهند به فردی که کد را میخواند منتقل نمیکنند. همه میدانند که هر تابعی بلاخره یک چیزی را حساب (compute) میکند، مهم این است که بدانند این تابع چه چیزی را حساب میکند.
کد اصلی
/* this class implements the encoding requested in problem #1706
*/
/**
* @author Esmaeil Ashrafi <s.ashrafi@gmail.com>
*/
import java.util.ArrayList;
public class StierlitzCipher {
private static String input; // the string suppose to be ciphered
private static int k; // the length of sub strings
// temporary variable for query substrings
private static StringBuffer temp = new StringBuffer();
// an array to store substrings of each query string
private static ArrayList<String> sub = new ArrayList<String>();
/**
* @param args
* the command line arguments
*/
public static void main(final String[] args) {
/*
* if ran in command line console,first argument willl be the string to
* cipher and second considered as the key
*/
if (args.length == 2) {
input = new String(args[1]);
k = Integer.parseInt(args[0]);
} // otherwise use of default values of problem
else {
input = new String("abaccc");
k = 3;
// prompt the user to using default values
System.out.println("doing encrypt by default values:"
+ "\nString : \"abaccc\"\nKey : 3");
}
query(input, k);
System.out.println(); // just for trimming the output !
}// end of main
/**
* gets the entire string and splits it to substrings in length of "len" and
* sends each substring to method compute for calculating the number of
* different non-empty substrings could obtained from them.there will be
* substrings(and consequent call to compute method)call as much as the length
* of parameter str
*
* @param str
* - the string to be cipher
* @param len
* - the length of substrings(the key)
*/
private static void query(final String str, final int len) {
int m;
final int strLen = str.length();
for (m = 0; m <= strLen - len; m++) {
compute(str.substring(m, m + len));
}
for (m = m; m < strLen; m++) {
compute(str.substring(m).concat(str.substring(0, len - (strLen - m))));
}
}// end of query
/**
* queries queryString to calculate number of its different and non-empty
* substrings and prints the count to the standard output
*
* @param queryStr
* - the string to be queried
*/
private static void compute(final String queryStr) {
final int qLen = queryStr.length();
for (int i = 0; i < qLen; i++) {
for (int j = i + 1; j <= qLen; j++) {
if (!sub.contains(temp.replace(0, temp.length(),
queryStr.substring(i, j)).toString())) {
sub.add(temp.toString());
}
}
}
System.out.print(" " + sub.size());
sub.clear();
}// end of compute
}// end of class
آوریل 8, 2010 در 10:09 ق.ظ. |
من هم بیشتر از اون چیزی که فکرش رو بکنید با نظرات شما موافق هستم !
هر کسی کمی در انجمنهای اینترنتی قصد کمک کردن یا کمک گرفتن داشته باشه به اهمیت این موضوع واقفه
هر کسی قبلن برنامه ای نوشته باشه که از اسم های خیلی کوتاه و یا شناسه هایی که هیچ اطلاعاتی درباره ی محتوای خودشون نمیدند استفاده کرده و بدتر از همه هیچ مستندسازی (یا چند کلمه توضیح جلوی یه دستور، متغیر، یا تابع ) انجام نداده باشه و یک مدت بعد از تکمیل کارش (فقط چند ماه بعد، نه چند سال بعد!) دوباره بخاد بره سراغش، به اهمیت مطالبی که در این مقاله اومده واقف خاهد بود. مثل خود من که یه پروژه دانشجویی به زبانC++ نوشته بودم یه موقعی و ( تازه اونقدر بد بود که حتی همون موقع پیاده سازی برای خودم هم باید از یه فرهنگ لغت استفاده میکردم !) و حالا که اصلن هیچ نظری درباره ش ندارم. فقط یادم میاد که از تکنیکهای جالبی استفاده کرده بودم و افسوس که الان نمیتونم درک کنم (مخصوصن که خیلی وقته سراغ C++ نرفتم )
و تازه یادمه اون موقع بعضی دوستان بودند که به مرموز (cryptic ) بودن کدهاشون افتخار میکردند و احتمالن با واژه ی نگهداری بیگانه بودند !
و گذشت و هیچوقت اصول مهندسی نرم افزار اونچنان که باید و شاید در دانشگاههای ایران جدی گرفته نشد. ما هم یکیش !
استفاده از نامهای مناسب خود نوعی مستند سازی توکار – هیچ توضیحی – محسوب میشه بدون!
اما درباره ی این مثال :
قبل از خوندن مقاله تون ، از لحاظ رعایت قواعد استاندارد ، به خودم نمره ی 70 تا 80 میدادم. حالا دور و بر 50 ! دلیلش هم 90 درصد مربوط میشه به اشاراتی که به نامگذاری داشتید (که کمی حرف دارم درباره ش)
اما فکر میکنم تلاشم برای قرار دادن خطوط توضیحات خوب بوده . خب البته قدری هم باید به خلاصه بودنش توجه میکردم ! بگذریم از اینکه در ساعات انتهایی روز- خاب آلوده – به سراغ این مسئله رفتم…
در عین اینکه با روشن سازی کامل شناسه ها کاملن موافق نظر شمام ، مخصوصن زمانیکه درخاست راهنمایی از کسانی داریم که داوطلبانه میل به راهنمایی دارند ، باید تا جاییکه ممکنه شرایط رو براشون تسهیل کرد و حداقل ، نامگذاری مناسب ، به درک سریعتر مخاطب (و البته خود نگارنده ) از کدها کمک فوق العاده ای میکنه
اما میتونم یه دوجین از کدهای منبع کتابخونه ی استاندارد جاوا رو بهتون نشون بدم که از خلاصه سازی (و نه مرموزسازی !) برای نامگذاری شناسه ها استفاده شده : مثلن args, srcBegin,buf یا حتی f برای نشون دادن یک float که اینا همه مال کلاس String بودند (به غیر از buf برای buffer فک کنم)
مثلن من در اون متد query – که انصافن نام مناسبی نداشت یا حداقل میشد نام مناسبتری بنابر پیشنهادات شما براش بکار برد – از نام len بجای length و از strLen بجای stringLength استفاده کردم حتی در کلاس String از کتابخونه ی استاندارد هم شما چنین نامهایی – مثل str – رو به عنوان نام پارامترهای رسمی متد میبینید
البته تعیین مرز خلاصه نویسی جای تامل داره . مثلن من هنوز فک میکنم حلقه ی for ی که قراره شمارنده ش فقط طول یک رشته رو پیمایش کنه چه ایرادی داره که از i یا j استفاده کنیم !؟
ولی اینطور که من متوجه شدم هرچه شناسه ای محلی تر باشه خلاصه سازی بیشتر مجازه !
در پایان باید اذعان کنم که شما عاشق برنامه نویسی هستید وگرنه وقتتون رو روی مطالعه ی کدی که کاملن واضح نیست نمیگذاشتید.حالا اگه میخاید بردارم با قاعده بنویسمش تحویلتون بدم بهم اطلاع بدید
سپاسگزارم
آوریل 8, 2010 در 10:44 ق.ظ. |
نقل قل :
»
برای مثال دستور زیر را درنظر بگیرید:
query(input, k);
به نظر شما این دستور چه کاری انجام میدهد؟ آیا k بار برای پیدا کردن input جست و جو انجام میدهد؟ آیا در کاراکتر kام input جست و جو انجام میدهد؟ آیا …
»
البته بنده بالای هر متد (تابع) اطلاعاتی راجع به پارامترها و وظیفه ی تابع قرار دادم !
آوریل 8, 2010 در 2:39 ب.ظ. |
ولی این نکته را به یاد داشته باشید که فردی که کد را میخواند برای فهمیدن کار این تابع باید تمرکز خود را از روی دستوری که فعلا در حال خواندن آن است بردارد و به چند سطر بالاتر (یا پایینتر) مراجعه کند. اگر انسان خوششانسی باشد توضیحات این تابع در همین بخش از کد که روی مانیتور خود در حال دیدن آن است پیداست. در غیر این صورت باید به طور کلی فریم فکری خود را عوض کند و شروع به گردش و جستوجو در کد برنامه کند. با این ترتیب کلا زنجیرهی افکار او در مورد دستوری که درحال خواندن آن بود از هم پاشیده میشود.
بازهم یادتان باشد که نوشتن یک برنامه کار دشواری است (خصوصا برنامهای که شما نوشتهاید) و این کار باعث میشود که نوشتن برنامه دشوارتر شود.
آوریل 8, 2010 در 10:47 ق.ظ. |
راستی یه موضوع خارج از بحث :
چرا به پارامترها ، final اضافه شده ؟
آوریل 8, 2010 در 2:34 ب.ظ. |
من کد شما رو داخل محیط eclipse کپی کردم. تنظیمات محیط eclipse روی کامپیوتر من طوری هست که هروقت یک فایل را save میکنم، به صورت خودکار یک سری پاکسازی روی کد انجام میدهد.
از جمله هرجا که ممکن بود کلمهی final را اضافه میکند.