ابتدا سریع کد بزنید سپس به منظور بهبود پرفورمنس، شروع به ریفکتورینگ کنید!

ابتدا سریع کد بزنید سپس به منظور بهبود پرفورمنس، شروع به ریفکتورینگ کنید!

در صنعت توسعۀ نرم‌افزار معمولاً این دولوپر (توسعه‌دهنده) نیست که تصمیم می‌گیرد چه چیزی را پیاده‌سازی کند، بلکه تصمیم‌گیرندهٔ اصلی مشتری خواهد بود (البته در برخی موارد نیز مدیر مجموعه‌ای که دولوپرها در آن مشغول به کار هستند، یکسری مسائل را مطرح کرده و انتظار دارد که ایشان سولوشنی برای حل آن‌ها ارائه کنند.) در چنین شرایطی، دولوپر در منگنه قرار می‌‌گیرد چرا که از یکسو باید به دِدلاین تحویل پروژه رسیده و از سوی دیگر، پروژه‌اش می‌بایست از پرفورمنس (عملکرد) خوبی نیز برخوردار باشد. به طور کلی، این دست موضوعات فرسایشی هستند اما دولوپرها می‌توانند با سیاستی که در ادامه معرفی می‌کنیم، تعادلی مابین سرعت و پرفورمنس برقرار سازند.

کدنویسی سریع سپس بهینه‌سازی آن (Brute Force)
این رویکرد که می‌توان آن را کدنویسی به سَبک Brute Force نامید، قابل‌استفاده در مورد همۀ مسائل در صنعت توسعهٔ نرم‌افزار است. به‌ عنوان مثال، فرض کنید که یک دولوپر فرانت‌اند هستید که قصد طراحی یک UI (مخفف User Interface به معنای رابط کاربری) برای یکی از مشتریان خود دارید. بر اساس این رویکرد، ابتدا بایستی تمام پَنل‌های مورد نیاز، ویجت‌ها و سایر گزینه‌های مد نظر را بر روی UI بسازید و پس از آن می‌توانید این UI را با تغییر مجدد مکان کامپوننت‌ها و به منظور ایجاد یک #تجربۀ کاربری بهتر، برای ایشان بهینه کنید.

به علاوه اینکه بهینه‌سازی را می‌توان با هدف آنالیز کاربران از طریق تست و همچنین R&D در مورد رفتار ایشان انجام داد؛ اما نکتۀ حائز اهمیت این است که یک چیزی بسازیم که با آن تَسک مد نظر انجام شود و پس از آن می‌توانیم به بهینه‌سازی کد یا محصول خود بپردازیم.

در پاسخ به این سؤال که «آیا می‌دانید منظور از کد روزمرۀ یک دولوپر چیست؟» بایستی گفت کدی است که نوشتن آن برای دولوپر خسته‌کننده می‌باشد اما به منظور توسعۀ نرم‌افزار، نوشتن آن ضروری است و بایستی انجام شود.

منظور از بهینه‌سازی چیست؟
در واقع، بهینه‌سازی (Optimization) عبارت است از کاهش زمان محاسبه یا اجرای سورس‌کد در هر نقطۀ ممکن از آن که در نتیجۀ این کار، سرعت اپلیکیشن نیز بالاتر خواهد رفت. در همین راستا، در ادامه در قالب مثالی واقعی، روش‌هایی را بررسی می‌کنیم که با اِعمال تغییر روی برخی قسمت‌های سورس‌کد، آن را بهینه‌تر خواهند کرد.

برای مثال، فرض کنید دولوپری از طرف مدیر خود مسئول این کار شده تا از طریق یکسری سورس مختلف، نام دانش‌آموزان را جستجو کند و Student ID (کد دانش‌آموزی) ایشان را پیدا کرده و نام آن‌ها را در صفحهٔ نمایش چاپ کند. برای نمونه، در ادامه سَمپِلی از چنین کدی را آورده‌ایم که با زبان #جاوا نوشته شده است:

public static void main(String[] args) {
    //initialised when program is started
    int targetStudentID = Integer.parseInt(args[0]);
    ArrayList < Student > studentList = new ArrayList < Student > ();
    //static method that fills the list with Student objects
    populateList(studentList);
    for (int i = 0; i < studentList.size(); i++) {
        if (studentList.get(i).id == targetStudentID) {
            System.out.println(studentList.get(i).getName());
        }
    }
}

حال در ادامه به تفسیر کد فوق می‌پردازیم:

- targetStudentID: آرگومان‌های موجود در برنامه، مقادیری من‌جمله شمارۀ Student ID را گرفته و مقدار آن‌ها را به نوع دادۀ int (عدد صحیح) تبدیل می‌کند که این دیتاتایپ نیز مشخص‌کنندۀ کد دانش‌آموزی فرد است (برای مثال، می‌توان عدد 111222333 را به‌ عنوان یک نمونۀ کد دانش‌آموزی عنوان کرد.)

- studentList: این مورد نمونه‌ای از پیاده‌سازی یک لیست در زبان جاوا است که با استفاده از آن می‌توان انواع آبجکت‌های Student را ذخیره کرد.

- (populateList (studentList: این متد لیست دانش‌آموزان را می‌گیرد و در واقع نوعی ارسال کوئری به دیتابیس برای استخراج لیست دانش‌آموزان است.

- (++for (int i = 0; i < studentList.size(); i: حلقۀ تکراری که تمامی آبجکت‌های Student را در یک لیست قرار می‌دهد.

- (if (studentList.get(i).id == targetStudentID: این شرط شناسهٔ دانش‌آموز درخواستی را با تمام آی‌دی‌های موجود در لیست مقایسه می‌کند.

- (()System.out.println(studentList.get(i).getName: نام دانش‌آموز مد نظر به همراه شناسهٔ وی را در خروجی چاپ می‌کند.

چگونه این برنامه را آپتیمایز (بهینه) کنیم؟
با در نظر گرفتن توضیحات فوق، در ادامه برخی مواردی را بررسی می‌کنیم که می‌توان در هر خط کد و به منظور بهبود آن‌ها اِعمال کرد که پرفورمنس سورس‌کد و در نتیجۀ آن اپلیکیشن مد نظر به میزان قابل‌توجهی افزایش می‌یابد.

در مورد خط سوم، با پیاده‌سازی بهتر ()parseInt می‌توان کد را بهبود بخشید و همچنین پیاده‌سازی مجدد این متد از پیش تعریف‌شدۀ زبان جاوا می‌تواند در بهبود سورس‌کد کمک‌کننده باشد (البته با توجه به اینکه این خط در حال حاضر به شیوه‌ای درست عمل می‌کند، دست به ریفکتورینگ آن نمی‌زنیم.)

در مورد خط چهارم، studentList یک لیست از نوع ArrayList (آرایه) است که در زبان جاوا یک دیتااستراکچر از نوع List است. برای بهینه‌سازی در این قسمت از سورس‌کد می‌توان به جای دیتااستراکچری از نوع List، دیتااستراکچر نوع Hash Table را به کار برد که این تغییر مسئلۀ بهینه‌سازی کد را به بهترین شکل ممکن حل می‌کند چرا که به جای جستجو در لیستی از آبجکت‌های Student که منجر به پیچیدگی زمانی (O(N برای الگوریتم‌مان خواهد شد، می‌توان با به‌کارگیری دیتااستراکچر نوع Hash Table و برای دسترسی به Student مد نظر، پیچیدگی زمانی سورس‌کد را به (1) O کاهش داد (N تعداد دانش‌آموزان را نشان می‌دهد.)

خط ششم یک کوئری به دیتابیس به منظور بازیابی اطلاعات Student ارسال می‌کند و از آنجایی که این بخش از کد مرتبط با کانکشن با دیتابیس است، این خط از سورس‌کد ممکن است با تمام مسائل مربوط به دسترسی به داده‌های موجود در دیتابیس روبه‌رو باشد که از جملۀ این مسائل می‌توان به زمان ورود یا خروج دیتا، پرفورمنس (عملکرد) شبکه و مسائل مربوط به کانکارنسی (هم‌زمانی) در ارسال برخی ریکوئست‌ها به دیتابیس و دریافت ریسپانس از آن اشاره کرد (همچنین سروری که سرویس هاستینگ اپلیکیشن را ارائه می‌دهد، قابلیت توسعه داشته و برای مثال می‌توان آن را به یک سرور 64بیتی با 48 گیگابایت حافظه ارتقاء داد که توان هَندل کردن بیش از 80،000،000 آیتم داده را داشته باشد.)

حال سؤالی که پیش می‌آید این است که آیا سروری به این اندازه برای میزبانی چنین اپلیکیشن کافی است؟ برای پاسخ به چنین سؤالی بایستی دولوپرها همواره در ساختن اپلیکیشن‌ها طول‌عمر آن اپلیکیشن و مدت‌ زمانی که مورد استفاده قرار خواهد گرفت را در نظر داشته باشند. حال فرض کنیم که به جای دیتابیس، از یک سرور اصطلاحاً Memcached برای ذخیرۀ آبجکت‌های Student استفاده شود که چنین سروری مطمئناً پرفورمنس بازیابی دیتای مربوط به Student را نسبت به حالتی که این دیتا مستقیماً از دیتابیس خوانده می‌شوند، افزایش می‌دهد (در سرور Memcached دولوپرها می‌توانند اطلاعات Student را تنها از طریق Student ID درخواست کنند تا در نهایت نام Student در خروجی چاپ شود؛ در واقع، این سرور پرفورمنس اپلیکیشن را با عدم نیاز به دسترسی به کل دایرکتوری Student بهبود می‌دهد.)

در ادامه، خطوط ۷، ۸ و ۹ را یکجا بررسی می‌کنیم چرا که هر سه دستور مبتنی بر حلقه‌های تکرار هستند و در این حلقه‌ها به منظور دسترسی به آبجکت‌ها، بایستی کل لیست به طور کامل جستجو شود تا در نهایت ID متناسب با Student ID درخواستی یافته شده که این راهبرد بسیار کُند انجام می‌شود (و یا حتی در بدترین حالت ممکن است Student مد نظر در آخر لیست قرار گرفته باشد) که از همین روی بهترین کاری که در این مورد موجب بهبود پرفورمنس می‌شود، قرار دادن یک دستور break در حلقه است تا زمانی که Student مد نظر پیدا شد، جستجو متوقف شده و در نهایت برنامه پایان یابد. بنابراین، شکل بهبودیافتۀ سورس‌کد فوق به این صورت خواهد بود:

public static void main(String[] args) {
    //initialised when program is started
    int targetStudentID = Integer.parseInt(args[0]);
    HashMap < Integer, Student > studentMap = new HashMap < Integer, Student > ();
    //static method that fills the map with Student objects
    populateMap(studentMap);
    //looks up the map in O(1) time to get the Student object
    System.out.println(studentMap.get(targetStudentID).getName());
}

در این نمونه کد آپتیمایزشده (بهبود‌یافته)، استفاده از دیتااستراکچر Hash Table به جای دیتااستراکچر List در برنامهٔ نوشته‌شده زمان اجرای برنامه را به طور قابل‌توجهی کاهش می‌دهد. همچنین استفاده از شناسهٔ Student به‌ عنوان کلیدی برای جستجوی آبجکت‌های مربوط به آن، موجب کاهش زمان جستجو می‌شود؛ علاوه بر اینکه سورس‌کد پیاده‌سازی شده در مقیاس‌های بزرگ‌تر نیز به خوبی کار خواهد کرد (به عبارت دیگر، سورس‌کد قابلیت توسعه خواهد داشت. به‌ عنوان مثال، شرایطی را در نظر بگیرید که حدود پنج سال از توسعۀ اپلیکیشن گذشته و اکنون نیز مدرسۀ مذکور تعداد دانش‌آموزان بیشتری را دارد که در این شرایط با به‌کارگیری نمونه کد اول، جستجو در لیست و پیدا کردن دانش‌آموز مد نظر زمان بیشتری طول خواهد کشید و از همین روی نیز سولوشنی مفید است که قابلیت توسعه در مقیاس را دارا باشد؛ یعنی نمونه کد دوم.)

نتیجه‌گیری
استفاده از این فرآیند بهینه‌سازی، به دولوپرها کمک خواهد کرد تا اپلیکیشن‌های بهتر و سریع‌تری را توسعه دهند که بنا بر نیاز و با افزایش احتمالی دیتا، به راحتی قابل‌توسعه باشند. با این حال، بعضی از چیزها را در برنامه‌نویسی نمی‌توان بهینه کرد؛ بنابراین دولوپرها بایستی در ابتدا بر اساس رویکرد Brute Force شروع به توسعۀ نرم‌افزار خود کنند تا سریع‌تر به اپلیکیشن مد نظر دست بیابند و پس از گرفتن خروجی، می‌توانند کد را آنالیز کرده و هر خط از سورس‌کد را بهینه کنند. در واقع، تکنیک‌هایی از این دست به دولوپرها کمک می‌کنند تا به یک خالق نرم‌افزار بهتر تبدیل شوند و مشتریان خود را با کیفیت محصولات‌شان تحت‌تأثیر قرار داده و از آن مهم‌تر، کاربران را راضی‌تر نگاه دارند.

منبع