حالت تاریک
فصل ۸ — آزمایشی بودن
آزمایش (Experimentation) بهصورت «رویهای که برای پشتیبانی، رد یا اعتبارسنجی فرضیه انجام میشود. آزمایشها با نشان دادن اینکه چه نتیجهای وقتی عامل خاصی دستکاری میشود رخ میدهد، بینش علّی-معلولی ارائه میدهند» تعریف میشود.[^1]
رویکرد آزمایشی به حل مسائل عمیقاً مهم است. استدلال میکنم علم و شیوه آزمایشی در قلب آن، بیش از هر چیز دیگر، تمدن مدرن پرفناوری ما را از جوامع کشاورزی پیشین متمایز میکند. انسانها بهعنوان گونهای متمایز صدها هزار سال وجود داشتهاند، با این حال سرعت پیشرفتی که در ۳۰۰ یا ۴۰۰ سال اخیر از زمان نیوتن یا گالیله — دورههایی که بیشتر مردم آغاز علم مدرن را میدانند — داشتهایم، همه چیز قبل را با اختلاف بسیار پشت سر گذاشته است. برآوردها میگویند کل دانش بشری در تمدن ما هر ۱۳ ماه دو برابر میشود.[^2]
[^1]: منبع: Wikipedia، https://en.wikipedia.org/wiki/Experiment [^2]: Buckminster Fuller منحنی دوبرابر شدن دانش را ایجاد کرد: https://bit.ly/2WiyUbE.
تا حد زیادی بهخاطر کاربرد بهترین تکنیک حل مسئله بشریت است.
با این حال، بیشتر توسعه نرمافزار واقعاً اینطور کار نمیکند. بیشتر توسعه نرمافزار آگاهانه بهعنوان تمرینی در صنعتگری انجام میشود که کسی حدس میزند کاربران چه چیزی را دوست دارند. درباره طراحی و/یا فناوری که میتواند به اهداف محصول برسد حدس میزند. توسعهدهندگان سپس حدس میزنند آیا کدی که مینویسند همان کاری را میکند که منظورشان است و آیا باگی در آن هست. بسیاری از سازمانها حدس میزنند آیا نرمافزارشان مفید است یا پول بیشتری از هزینه ساختش درآورده.
میتوانیم بهتر عمل کنیم. میتوانیم حدس را جایی که مناسب است بهکار ببریم، اما سپس آزمایشهایی برای آزمایش حدسها طراحی کنیم.
این کند، گران و پیچیده به نظر میرسد، اما نیست. این واقعاً فقط تغییر رویکرد و ذهنیت است. درباره «سختتر کار کردن» نیست؛ درباره «هوشمندانهتر کار کردن» است. تیمهایی که دیدهام اینطور کار کردهاند و این ایدهها را به دل گرفتهاند، کند یا بیش از حد دانشگاهی نیستند. با این حال، در نحوه رویکرد به حل مسئله منضبطترند و در نتیجه راهحلهای بهتر و ارزانتری سریعتر پیدا میکنند و نرمافزاری با کیفیت بیشتر که کاربرانشان را بیشتر راضی میکند تولید میکنند.
«آزمایشی بودن» یعنی چه؟
یکی از ایدههای کلیدی در ریشه تفکر علمی، دور شدن از تصمیمات بر اساس اقتدار است. Richard Feynman، همانطور که همیشه، نقلقول عالی در این موضوع دارد:
علم باور به نادانی متخصصان است.
او همچنین گفت:
هیچ احترامی به اقتدار نداشته باشید؛ فراموش کنید چه کسی گفت و بهجای آن ببینید از کجا شروع کرد، به کجا رسید و از خودتان بپرسید: «منطقی است؟»
با وجود زبان کمی جنسیتزده زمانش، احساس درست است.
باید از تصمیمگیری بر اساس آنچه مهمترین، کاریزماتیکترین یا مشهورترین افراد میگویند — حتی اگر Richard Feynman باشد — دور شویم و بهجای آن بر اساس شواهد تصمیم و انتخاب بگیریم.
این تغییر بزرگی برای صنعت ماست و معمولاً اینطور کار نمیکند. متأسفانه، این برای جامعه بهطور کلی هم درست است، نه فقط توسعه نرمافزار، پس اگر میخواهیم بهعنوان مهندس موفق شویم، باید از جامعه بهطور کلی بهتر عمل کنیم.
چه چیزی شما را به انتخاب زبان برنامهنویسی، فریمورک یا ویرایشگری که کدتان را در آن مینویسید واداشت؟ آیا مکالمههایی دارید که درباره مزایای نسبی Java در مقابل Python بحث میکنید؟ آیا فکر میکنید هر کسی که VI را بهعنوان ویرایشگر استفاده میکند باهوش است یا احمق؟ آیا باور دارید برنامهنویسی تابعی تنها راه درست است، یا object orientation بهترین چیزی است که اختراع شده؟ بله، من هم!
پیشنهاد نمیکنم برای هر چنین تصمیمی آزمایش کنترلشده و جامع بسازیم، اما باید جنگهای مذهبی درباره این چیزها را متوقف کنیم.
اگر میخواهیم استدلال کنیم Clojure از C# بهتر است، چرا یک آزمایش کوچک نکنیم و پایداری و توان عملیاتی نتیجه را اندازه نگیریم؟ حداقل آنگاه میتوانیم بر اساس شواهد، حتی اگر کامل نباشد، تصمیم بگیریم، نه بر اساس اینکه چه کسی در بحث متقاعدکنندهتر بود. اگر با نتایج مخالفید، آزمایش بهتری انجام دهید و استدلالتان را نشان دهید.
آزمایشی بودن به معنای پایهگذاری هر تصمیم بر فیزیک سخت نیست. همه علوم بر آزمایش استوارند، اما درجه کنترل متفاوت است. در مهندسی، آزمایش در قلب آن باقی میماند، اما شکلی عملی و کاربردی از آزمایش است.
چهار ویژگی «آزمایشی بودن» را بهعنوان رویکرد تعریف میکنند:
- بازخورد (Feedback): باید بازخورد را جدی بگیریم و بفهمیم چگونه نتایجی جمعآوری کنیم که سیگنال واضحی به ما بدهند و آنها را کارآمد به نقطهای که فکر میکنیم برگردانیم. باید حلقه را ببندیم.
- فرضیه (Hypothesis): باید ایدهای در ذهن داشته باشیم که قصد ارزیابی آن را داریم. بیهدف و تصادفی داده جمع نمیکنیم. این کافی نیست.
- اندازهگیری (Measurement): باید ایده روشنی از چگونگی ارزیابی پیشبینیهایی که در فرضیهمان آزمایش میکنیم داشته باشیم. «موفقیت» یا «شکست» در این بستر یعنی چه؟
- کنترل متغیرها (Control the variables): باید تا حد عملی ممکن متغیرها را حذف کنیم تا سیگنالی که آزمایش به ما میفرستد را درک کنیم.
بازخورد
از دیدگاه مهندسی، مهم است اثر تسریع کارایی و کیفیت بازخورد را بشناسیم.
نیاز به سرعت
یک بار در شرکتی کار کردم که نرمافزار پیچیده معاملات مالی تولید میکرد. توسعهدهندگان بسیار خوب بودند و شرکت موفق، اما میدانستند میتوانند بهتر عمل کنند و کار من کمک به بهبود شیوههای توسعه نرمافزارشان بود.
وقتی پیوستم، رویکرد نسبتاً مؤثری به آزمایش خودکار اتخاذ کرده بودند. تستهای زیادی داشتند. یک build شبانه اجرا میکردند و بخش عمده پیشنهادشان یک build بزرگ C++ بود که نه و نیم ساعت برای تکمیل، از جمله اجرای همه تستها، طول میکشید. پس build را هر شب اجرا میکردند.
یکی از توسعهدهندگان گفت در سه سالی که اینطور کار کردهاند، فقط سه بار بود که همه تستها پاس شدند.
پس هر صبح ماژولهایی که همه تستهایشان پاس شده بود را منتشر میکردند و ماژولهایی که شکست تست داده بودند را نگه میداشتند.
این خوب بود، تا وقتی یکی از ماژولهای پاسشده به تغییرات یکی از ماژولهای شکستخورده وابسته نباشد — و گاهی بود.
چیزهای زیادی میخواستم تغییر دهم، اما بهعنوان گام اول روی بهبود کارایی بازخورد کار کردیم، بدون هیچ تغییر دیگری.
پس از آزمایش و کار سخت زیاد، توانستیم یک build سریع commit در ۱۲ دقیقه و بقیه تستها در ۴۰ دقیقه اجرا کنیم. همان کار build نه و نیم ساعته را انجام میداد، فقط سریعتر! هیچ تغییر دیگری در سازمان، فرایند یا ابزار، فراتر از تسریع build و رساندن کارآمدتر نتایج به توسعهدهندگان، نبود.
در اولین دوره دو هفتهای پس از انتشار این تغییر، دو build بود که همه تستها پاس شدند. در دو هفته بعد و تا زمانی که آنجا کار میکردم، حداقل یک build در روز بود که همه تستها پاس شدند و همه کد قابل انتشار بود.
بدون هیچ تغییری جز بهبود سرعت بازخورد، به تیمها ابزاری دادیم که برای رفع ناپایداری زیربنایی نیاز داشتند.
داستان «نیاز به سرعت» در کادر بالا نمایش خوبی از مؤثربودن اعمال تکنیکهای آزمایشگرایی بر کارمان و همچنین بهینهسازی برای بازخورد خوب است.
در این مورد، برای بهبود کارایی و کیفیت بازخورد به توسعهدهندگان آزمایش کردیم. در طول این کار معیارهای بهتری از عملکرد build ایجاد کردیم، متغیرها را با version control و infrastructure as code بهبودیافته کنترل کردیم و چند راهحل فنی و سیستم build مختلف را A/B test کردیم.
فقط از طریق رویکرد نسبتاً منضبط به اعمال این نوع تفکر آزمایشی بر این مسئله — مسئلهای که تلاشهای مختلف قبلاً برای بهبود آن شکست خورده بود — توانستیم پیشرفت کنیم. چند ایدهمان نتیجه نداد. آزمایشهایمان نشان داد سرمایهگذاری زیاد روی ابزارها یا تکنیکهای خاص فایده ندارد چون سرعتهایی که نیاز داشتیم را نمیدهند.
فرضیه
وقتی درباره علم و مهندسی صحبت میکنیم، اغلب از «حذف حدس» صحبت میشود. من هم گناهکار استفاده از آن عبارت در گذشتهام. اما اشتباه است. از یک جهت مهم، علم بر حدس بنا شده؛ فقط این است که رویکرد علمی به حل مسئله حدس را نهادینه میکند و فرضیه مینامد. همانطور که Richard Feynman در سخنرانی فوقالعادهاش درباره روش علمی بهخوبی گفت:[^3]
ما به دنبال قانون جدید با این فرایند میگردیم، اول حدس میزنیم!
[^3]: فیزیکدان برنده جایزه نوبل Richard Feynman درباره روش علمی: https://bit.ly/2RiEivq
حدسها یا فرضیهها نقطه شروعاند. تفاوت علم و مهندسی، در مقایسه با رویکردهای کمتر مؤثر دیگر، این است که دیگران آنجا متوقف میشوند.
برای علمی بودن، وقتی حدسی بهصورت فرضیه داریم، شروع به پیشبینی میکنیم و سپس میتوانیم راههایی برای بررسی آن پیشبینیها پیدا کنیم.
Feynman در آن ارائه عالی ادامه میدهد:
اگر حدس شما با آزمایش مخالف باشد، آنگاه (حدس شما) اشتباه است!
این قلب موضوع است! آنجاست که باید برسیم تا بتوانیم ادعا کنیم آنچه انجام میدهیم مهندسی است نه حدس.
باید بتوانیم فرضیههایمان را آزمایش کنیم. آزمایشهایمان میتوانند اشکال مختلفی داشته باشند. میتوانیم واقعیت (تولید) را مشاهده کنیم، یا آزمایش کنترلشدهتر، شاید بهصورت تست خودکار از نوعی، انجام دهیم.
میتوانیم روی دریافت بازخورد خوب از تولید برای آگاه کردن یادگیریمان تمرکز کنیم، یا ایدههایمان را در شرایط کنترلشدهتر امتحان کنیم.
سازماندهی تفکر و کارمان برای پیشروی بهعنوان مجموعهای از آزمایشها برای اعتبارسنجی فرضیههایمان بهبود مهمی در کیفیت کارمان است.
اندازهگیری
چه دادهای را از واقعیت (تولید) جمعآوری کنیم یا آزمایش کنترلشدهتری انجام دهیم، باید اندازهگیری را جدی بگیریم. باید فکر کنیم دادهای که جمع میکنیم چه معنایی دارد و نسبت به آن انتقادی باشیم.
خیلی آسان است با تلاش برای «جور کردن حقایق با داده» خودمان را فریب دهیم. میتوانیم تا حدی از چنین اشتباهاتی با فکر کردن دقیق، بهعنوان بخشی از طراحی آزمایشمان، درباره اندازهگیریهایی که منطقی به نظر میرسند محافظت کنیم. باید بر اساس فرضیهمان پیشبینی کنیم و سپس بفهمیم چگونه نتایج پیشبینیمان را اندازه بگیریم.
مثالهای زیادی از اندازهگیری چیزهای اشتباه به ذهنم میرسد. در یکی از مشتریانم، تصمیم گرفتند میتوانند کیفیت کد را با افزایش سطح test coverage بهبود دهند. پس پروژهای برای نهادینه کردن اندازهگیری شروع کردند، داده جمع کردند و سیاستی برای تشویق بهبود test coverage اتخاذ کردند. هدف «۸۰ درصد test coverage» گذاشتند. سپس از این اندازهگیری برای تشویق تیمهای توسعه استفاده کردند، پاداشها به رسیدن به اهداف test coverage گره خورد.
حدس بزنید چه شد؟ به هدفشان رسیدند!
مدتی بعد، تستهایی که داشتند را تحلیل کردند و یافتند بیش از ۲۵ درصد تستها اصلاً assertion نداشتند. پس به افراد تیمهای توسعه، از طریق پاداشها، برای نوشتن تستهایی که هیچ چیز را آزمایش نمیکردند پول داده بودند.
در این مورد، معیار بسیار بهتری پایداری بود. آنچه این سازمان واقعاً میخواست تست بیشتر نبود بلکه کد باکیفیتتر بود، پس اندازهگیری مستقیمتر آن بهتر کار کرد.
این دشواری در اندازهگیری چیزهای اشتباه فقط به «معیارها» و هوشمندی انسانها در بازی کردن با سیستم محدود نمیشود.
بیش از ده سال از حرفهام را در سیستمهای مالی کمتأخیر کار کردم. وقتی شروع کردیم، بسیار روی اندازهگیری latency و throughput متمرکز بودیم، پس سخت کار کردیم اندازهگیریها را ثبت کنیم و اهدافی مثل «سیستم باید بتواند ۱۰۰٬۰۰۰ پیام در ثانیه با latency بیش از ۲ میلیثانیه پردازش کند» گذاشتیم. اولین تلاشها بر اساس میانگین بود که بعداً فهمیدیم بیمعناست. باید مشخصتر میبودیم؛ زمانهایی در چرخه معاملاتی بعدی بود که بار اوج ما بسیار از معادل ۱۰۰٬۰۰۰ msg/sec فراتر میرفت و به اعدادی معادل میلیونها msg/sec میرسید. latency میانگین مهم نبود اگر outlierها فراتر از محدودیتهای خاصی بودند. در دنیای واقعی معاملات با فرکانس بالا، ۲ میلیثانیه میانگین نبود — آن محدودیت بود!
در این مثال دوم، آزمایشی شروع کردیم، اما تا حدی بهخاطر دقت اندازهگیریمان — هرچند برخی چیزهای اشتباه را اندازه میگرفتیم — سریع شروع به یادگیری و بهبود کیفیت و دقت اندازهگیریها و هدفگیری بهتر آزمایشهایمان کردیم. همه چیز درباره یادگیری است!
همه به این درجه دقت در اندازهگیری اهمیت نمیدهند، اما اصول هر نوع نرمافزاری که میسازید باقی میماند. آزمایشی بودن از ما میخواهد به اندازهگیری سیستممان، هرچه در بستر ما معنا دارد، توجه بیشتری کنیم.
کنترل متغیرها
برای جمعآوری بازخورد و اندازهگیریهای مفید، باید متغیرها را تا حد عملی ممکن کنترل کنیم. وقتی Jez Humble و من کتاب Continuous Delivery را نوشتیم، زیرعنوانش Reliable Software Releases Through Build, Test, and Deployment Automation بود. فکر نمیکنم آن زمان اینطور فکر میکردم، اما آنچه واقعاً میگوید این است: «متغیرها را کنترل کنید تا انتشارهایتان قابلاعتماد باشند.»
version control به ما امکان میدهد درباره تغییراتی که به تولید منتشر میکنیم دقیقتر باشیم. آزمایش خودکار به ما امکان میدهد درباره رفتار، سرعت، استحکام و کیفیت کلی نرمافزاری که تولید میکنیم دقیقتر باشیم. اتوماسیون استقرار و ایدههایی مثل infrastructure as code به ما امکان میدهند درباره محیطهایی که نرمافزارمان در آنها عمل میکند دقیقتر باشیم.
همه این تکنیکها به ما امکان میدهند بسیار مطمئنتر باشیم وقتی نرمافزارمان را به تولید میگذاریم، همان کاری را انجام میدهد که منظورمان است.
نگاه من به continuous delivery بهعنوان رویکرد عمومی به توسعه نرمافزار این است که به ما امکان میدهد با اطمینان بسیار بیشتر پیش برویم. تا حد زیادی متغیرهای حول کیفیت کارمان را حذف میکند تا بتوانیم روی اینکه آیا ایدههای محصولمان خوباند تمرکز کنیم. تصویر بسیار واضحتری از «آیا چیزهای درست را میسازیم» میگیریم چون «آیا چیزها را درست میسازیم» را کنترل کردهایم.
با کنترل بسیاری از متغیرهای فنی در توسعه نرمافزار، continuous delivery به ما امکان میدهد با اطمینان بسیار بیشتری نسبت به قبل پیشرفت کنیم. این به تیمهای توسعه نرمافزار امکان میدهد واقعاً از تکنیکهای بهینهسازی برای یادگیری که در قلب این کتاب است بهره ببرند.
مثلاً، deployment pipeline مربوط به continuous delivery پلتفرم آزمایشی ایدهآلی برای یادگیری درباره تغییراتی است که میخواهیم به سیستمهای تولیدیمان بدهیم.
کار بهگونهای که نرمافزارمان همیشه در وضعیت قابل انتشار باشد، ایده در قلب CD، ایدهای است که بازخوردی که میتوانیم درباره کیفیت کارمان بگیریم را حداکثر میکند و بهشدت ما را به کار در گامهای کوچکتر وادار میکند. این به نوبه خود یعنی تقریباً مجبوریم تکراری و افزایشی کار کنیم.
داشتن نرمافزاری که همیشه در وضعیت قابل انتشار است — خوب، احمقانه است از آن بهره نبریم! یعنی سازمانها میتوانند مکررتر منتشر کنند و بازخورد بسیار بیشتری، خیلی زودتر، درباره کیفیت ایدههایشان بگیرند و محصولات بهتری بسازند.
آزمایش خودکار بهعنوان آزمایش
آزمایشها میتوانند اشکال مختلفی داشته باشند، اما در نرمافزار نسبت به هر رشته دیگر مزیت عظیمی داریم: این پلتفرم آزمایشی فوقالعاده — کامپیوتر!
میتوانیم اگر بخواهیم هر ثانیه میلیونها آزمایش اجرا کنیم. این آزمایشها هم میتوانند اشکال مختلفی داشته باشند؛ میتوانیم مرحله کامپایل را نوعی آزمایش بدانیم: «پیشبینی میکنم کدم بدون هیچ هشداری کامپایل شود» یا «پیشبینی میکنم هیچ کد UI به کتابخانه پایگاه داده دسترسی نداشته باشد.» انعطافپذیرترین شکل آزمایش در بستر نرمافزار، با این حال، تست خودکار است.
هر تست خودکاری برای اعتبارسنجی نرمافزارمان میتواند اگر سخت تلاش کنید آزمایش تلقی شود. با این حال، اگر تستهای خودکار را پس از نوشتن کد بنویسید، ارزش آزمایش کاهش مییابد. آزمایش باید بر اساس نوعی فرضیه باشد و تصمیمگیری درباره اینکه آیا کد کار میکند یا نه فرضیه بسیار ضعیفی است.
منظورم سازماندهی توسعه حول مجموعهای از آزمایشهای تکراری است که پیشبینیهای کوچکی درباره رفتار مورد انتظار کدمان میکنند و به ما اجازه میدهند تدریجاً عملکرد نرمافزارمان را افزایش دهیم.
واضحترین شکل چنین آزمایشی، توسعه نرمافزار هدایتشده با تست، یا test-driven development (TDD) است.
TDD استراتژی مؤثری است که در آن از تستها بهعنوان مشخصات اجرایی برای رفتار سیستممان استفاده میکنیم. تعریف دقیق تغییر در رفتاری که قصد دستیابی به آن را داریم، فرضیه ماست: «با توجه به این بستر خاص، وقتی این اتفاق میافتد، انتظار این نتیجه را داریم.» این پیشبینی را بهصورت تست کوچک و ساده میسازیم و سپس تأیید میکنیم پیشبینیهای مورد تستمان وقتی کد را کامل میکنیم و آزمایش را انجام میدهیم برآورده شد.
میتوانیم این رویکرد TDD را در سطوح مختلف دانهبندی عملی کنیم. میتوانیم با ایجاد مشخصات کاربر-محور، با تکنیکهای acceptance test–driven development (ATDD)، که گاهی behavior-driven development (BDD) هم نامیده میشود، شروع کنیم. از این مشخصات اجرایی سطح بالا برای هدایت unit testing فنیتر و ریزدانهتر استفاده میکنیم.
نرمافزار توسعهیافته با این تکنیکها بهطور قابلاندازهگیری و قابلتوجهی باگ کمتری نسبت به نرمافزار توسعهیافته بهشکل متعارفتر دارد.[^4]
[^4]: چند مطالعه، دانشگاهی و غیررسمی، درباره تأثیر TDD بر کاهش نقص وجود دارد. بیشتر مطالعات موافقاند کاهش نقص در بازه ۴۰ درصد تا بیش از ۲۵۰ درصد است. منبع: https://bit.ly/2LFixzS، https://bit.ly/2LDh3q3، https://bit.ly/3MurTgF
این بهبود خوشایندی در کیفیت است، اما ارزش را واقعاً تا وقتی تأثیر چنین کاهشهایی بر بهرهوری را هم در نظر بگیریم نمیبینیم. احتمالاً، در نتیجه این کاهش نقص، تیمهای توسعه زمان بسیار کمتری صرف فعالیتهای دیگر مثل کشف، triage و تحلیل باگ میکنند.
نتیجه این است که تیمهای با عملکرد بالا که از تکنیکهایی مثل TDD، continuous integration و continuous delivery استفاده میکنند ۴۴ درصد زمان بیشتری صرف کار مفید میکنند.[^5] این تیمها بسیار تولیدیتر از هنجارند در حالی که همزمان نتیجه باکیفیتتری تولید میکنند. میتوانید کیک را داشته باشید و بخورید هم!
[^5]: منبع: گزارشهای «State of DevOps» (سالهای مختلف) و Accelerate: The science of Lean and DevOps اثر Forsgren، Humble و Kim. ببینید https://amzn.to/369r73m.
شیوههای extreme programming در بستر continuous delivery، بهویژه continuous integration و TDD، پلتفرم آزمایشی فوقالعاده و مقیاسپذیری فراهم میکنند که در آن میتوانیم ایدههایمان را در طراحی و پیادهسازی ارزیابی و پالایش کنیم. این تکنیکها تأثیر مهم و قابلتوجهی بر کیفیت کارمان و نرخ تولید نرمافزار خوب دارند. اینها همان نتایجیاند که در رشتههای دیگر به مهندسی نسبت میدهیم.
قرار دادن نتایج آزمایشی تست در بستر
ببخشید اگر لحظهای فلسفی باشم، اما فکر میکنم تا الان به آن عادت کردهاید.
بیایید فکر کنیم مجموعهای از تستها، مثل آنهایی که توصیف کردم، واقعاً چه معنایی دارد.
ادعایی برای عقلانیت علمی بهعنوان اصل راهنمای رویکردی که سعی در توصیف آن دارم مطرح میکنم. اشتباه رایج توسعهدهندگان نرمافزار، و شاید مردم بهطور کلی، این است که بهمحض اشاره به «علم» تقریباً همیشه «فیزیک» فکر میکنیم.
من علاقهمند آماتور فیزیک هستم. فیزیک را دوست دارم و مدلهای ذهنیای که برای درک چیزهای اطرافم به من میدهد. گاهی شوخی میکنم فیزیک تنها علم واقعی است، اما منظورم نیست.
علم بسیار گستردهتر از فیزیک است، اما خارج از حوزه abstractهای سادهشدهای که در قلب فیزیک استفاده میکنیم، علوم دیگر اغلب کثیفتر و کمتر دقیقاند. این ارزش استدلال به سبک علمی را کاهش نمیدهد. زیستشناسی، شیمی، روانشناسی و جامعهشناسی هم علوماند. با دقت فیزیک پیشبینی نمیکنند چون نمیتوانند متغیرها را در آزمایش بهاندازه فیزیک سختگیرانه کنترل کنند، اما همچنان بینش عمیقتر و نتایج بهتری از جایگزینها ارائه میدهند. لحظهای انتظار ندارم بهاندازه فیزیک دقیق یا کامل باشیم.
با این حال، در نرمافزار نسبت به تقریباً هر شکل دیگر مهندسی و چند علم، جایی که آزمایشها اغلب به دلایل اخلاقی یا عملی دشوار است، مزایای عمیقی داریم. میتوانیم «جهان»ی که نرمافزارمان در آن زندگی میکند را کاملاً بسازیم و کنترل کنیم. اگر بخواهیم میتوانیم کنترل ظریف و دقیق اعمال کنیم. میتوانیم میلیونها آزمایش با هزینه کم ایجاد کنیم و قدرت آمار را به نفعمان بهکار ببریم. بهطور سادهشده، این همان چیزی است که یادگیری ماشین مدرن واقعاً هست.
کامپیوترها به ما فرصت میدهند کنترل نرمافزارمان را به دست بگیریم و آزمایشهایی در مقیاسی که در هیچ بستر دیگری غیرقابل تصور است انجام دهیم.
در نهایت، یک توانایی دیگر، کاملاً عمیق، وجود دارد که نرمافزار به ما میدهد.
پس فیزیکدان نخواهیم شد، اما لحظهای تصور کنیم هستیم. اگر شما و من ایده جدیدی در فیزیک مطرح کنیم، چگونه بدانیم ایده خوبی است؟ باید بهاندازه کافی مطالعه کرده باشیم که بفهمیم با حقایقی که فیزیک فعلاً درک میکند جور میآید. پیشنهاد «اینشتین اشتباه کرد» فایده ندارد اگر ندانیم اینشتین چه گفت. فیزیک موضوع وسیعی است، پس هرچقدر هم مطالعه کرده باشیم، باید ایده را بهاندازه کافی واضح توصیف کنیم که دیگران هم بتوانند آزمایش کنند. اگر هر آزمایشی از ایده شکست خورد، پس از بررسی اینکه اشتباه در آزمایش نبود، میتوانیم ایده را رد کنیم.
در نرمافزار، میتوانیم همه این کارها را با تستهایمان انجام دهیم و بهجای ماهها یا سالها برای به ثمر رسیدن ایدههایمان، در دقیقهها نتیجه بگیریم. این ابرقدرت ماست!
اگر فکر کنیم نرمافزارمان در جهان کوچکی که میسازیم وجود دارد، هرچقدر هم نرمافزار بزرگ یا پیچیده باشد، میتوانیم آن جهان را دقیق کنترل کنیم و نقش نرمافزارمان در آن را ارزیابی کنیم. اگر برای «کنترل متغیرها» تا حدی که بتوانیم آن جهان را بهطور قابلاعتماد و تکرارپذیر بازسازی کنیم — مثلاً infrastructure as code بهعنوان بخشی از deployment pipeline مربوط به continuous delivery — کار کنیم، نقطه شروع خوبی برای آزمایشهایمان داریم.
مجموعه کامل همه تستهایی که نوشتهایم، از جمله مجموعه آزمایشهایی که درکمان از رفتار سیستم در آن جهان کنترلشده را تأیید میکنند، بدنه دانش ما از سیستم است.
میتوانیم تعریف «جهان» و «بدنه دانش» را به هر کسی بدهیم و آنها میتوانند تأیید کنند که بهصورت کل، درونسازگارند — همه تستها پاس میشوند.
اگر بخواهیم «دانش جدید» در سیستم ایجاد کنیم، میتوانیم آزمایش جدیدی، تستی، بسازیم که دانش جدیدی که انتظار مشاهده آن را داریم تعریف کند و سپس آن دانش را بهصورت کد کاری که نیازهای آزمایش را برآورده میکند اضافه کنیم. اگر ایدههای جدید با ایدههای قبلی — یعنی «بدنه دانش» در جهان کوچک و کنترلشدهمان — سازگار نباشند، آزمایشها شکست میخورند و میفهمیم این ایده اشتباه است، یا حداقل با بیانیه ثبتشده دانش در سیستم ناسازگار است.
میدانم این نمایش کمی ایدهآلیستی از سیستم نرمافزاری و تستهای مرتبطش است، اما روی چند سیستم کار کردهام که بسیار به این ایدهآل نزدیک شدند. با این حال، حتی اگر فقط ۸۰ درصد راه را رفتید، فکر کنید این چه معنایی دارد. در عرض دقیقهها، نشانهای از اعتبار و سازگاری ایدههایتان در سراسر سیستم دارید.
همانطور که قبلاً گفتم، این ابرقدرت ماست. این کاری است که میتوانیم با نرمافزار انجام دهیم اگر آن را بهعنوان فرایند مهندسی، نه فقط مبتنی بر صنعتگری، در نظر بگیریم.
دامنه یک آزمایش
آزمایشها در همه اندازهها میآیند. میتوانیم چیزی کوچک، تقریباً پیشپاافتاده انجام دهیم یا چیزی بزرگ و پیچیده. گاهی به هر دو نیاز داریم، اما ایده «کار در مجموعهای از آزمایشها» برای برخی دلهرهآور به نظر میرسد.
بگذارید یک نوع رایج آزمایشی که منظم در توسعه نرمافزار خودم انجام میدهم توصیف کنم تا ذهنتان آرام شود.
وقتی TDD را عملی میکنم، تغییر مورد نظر در کدم را با یک تست شروع میکنم. هدف ایجاد تست شکستخورده است. میخواهیم تست را اجرا کنیم و شکست آن را ببینیم تا بررسی کنیم تست واقعاً چیزی را آزمایش میکند.
پس با نوشتن تست شروع میکنم. وقتی تست را همانطور که میخواهم دارم، پیام خطای دقیقی که انتظار دارم تست با آن شکست بخورد پیشبینی میکنم: «انتظار دارم این تست با پیامی شبیه 'expected x but was 0' شکست بخورد» یا چیزی مشابه. این یک آزمایش است؛ این کاربرد کوچک روش علمی است.
- مشکل را فکر کردم و توصیف کردم: «رفتاری که از سیستم میخواهم را تصمیم گرفتم و بهصورت مورد تست ثبت کردم.»
- فرضیه ساختم: «انتظار دارم تستم شکست بخورد!»
- پیشبینی کردم: «وقتی شکست بخورد، با این پیام خطا شکست میخورد…»
- آزمایش را انجام دادم: «تست را اجرا کردم.»
این تغییر کوچکی از نحوه کار قبلیام است، اما تأثیر قابلتوجه و مثبتی بر کیفیت کارم داشته است.
اعمال رویکرد منضبطتر و آزمایشیتر به کارمان لزوماً پیچیده یا سنگین نیست. اگر میخواهیم مهندسان نرمافزار شویم، باید چنین انضباطهایی را بپذیریم و مداوم به کارمان اعمال کنیم.
خلاصه
ویژگی کلیدی کار به روش آزمایشیتر، درجه کنترلی است که بر متغیرهای درگیر اعمال میکنیم. بخشی از تعریف «آزمایشگرایی» در ابتدای این فصل «نشان دادن اینکه چه اتفاقی وقتی عامل خاصی دستکاری میشود رخ میدهد» است. برای کار آزمایشیتر، میخواهیم رویکردمان به کار کمی کنترلشدهتر باشد. میخواهیم نتایج «آزمایش»هایمان قابلاعتماد باشد. در بستر فنی سیستمهایی که میسازیم، کار آزمایشی و کنترل متغیرهایی که میتوانیم، از طریق آزمایش خودکار مؤثر و تکنیکهای continuous delivery مثل infrastructure as code، آزمایشهایمان را قابلاعتمادتر و تکرارپذیرتر میکند. عمیقتر، آنها همچنین نرمافزارمان را قطعیتر و در نتیجه باکیفیتتر، قابلپیشبینیتر و قابلاعتمادتر در استفاده میکنند.
هر رویکرد مهندسی نرمافزاری که ارزش نام بردن دارد باید برای همان میزان کار، نرمافزار بهتری نتیجه دهد. سازماندهی کارمان بهعنوان دنبالهای از آزمایشهای کوچک، معمولاً ساده، این کار را انجام میدهد.