یکی از مشکلات رایج تیمهای نرمافزاری این است که اگر ازشان بپرسی یک تغییر از لحظهای که تصمیم میگیریم انجامش بدهیم تا وقتی کاربر آن را در محصول میبیند دقیقا چه مسیری را طی میکند، جواب چندان روشنی نخواهید شنید. نه اینکه فرآیندی نداشته باشند؛ اتفاقا معمولا فرآیند زیاد دارند. کار ثبت میشود، اولویت میگیرد، برنامهریزی میشود، توسعه داده میشود، بازبینی میشود، تست میشود، تأیید میگیرد و بالاخره منتشر میشود. روی کاغذ همهچیز مرتب است. اما وقتی یک تغییر واقعی را از ابتدا تا انتها دنبال میکنی (به قولی Flow کار)، معمولا با چیز دیگری روبهرو میشوی: چند مرحله انتظار، چند بار دستبهدست شدن کار، تصمیمهایی که جایی ثبت نشدهاند، کارهای دستی که همه فراموش کردهاند چون هنوز دستی هستند و مهمتر از همه، جاهایی که هیچکس دقیقا نمیداند از کجا باید بفهمد این تغییر واقعا موفق بوده یا نه.
این برای من از خیلی از بحثهایی که درباره روشهای توسعه نرمافزار میشود جالبتر است. ما در این صنعت عاشق اسم گذاشتن روی روشها هستیم. برای تقریبا هر مسئلهای یک روش داریم. متدولوژيهایی مثل TDD، BDD، DDD، SDD و تا ابد Dهای دیگر. بعد هم در لایه مدیریت Scrum و Sprint و Story و Point و Velocity و انواع فرآیندها را داریم. بعضی از این ایدهها واقعا هم مفیدند و نمیشود ارزششان را انکار کرد، اما مشکل از جایی شروع میشود که خود روش تبدیل به هدف میشود. یعنی به جای اینکه بپرسیم «این کار چه مشکلی از ما حل میکند؟»، تمرکزمان روی این است که «آیا داریم فولان روش را درست اجرا میکنیم؟»
مثلاً درباره TDD، بحث اصلی برای من این نیست که تست را قبل از کد بنویسیم یا بعد از آن. ارزش اصلی تست این است که فاصله بین فرض ما درباره درست بودن نرمافزار و شواهدی که نشان میدهد واقعا درست است را کوتاه میکند. اگر امروز چیزی را تغییر بدهی و پنج دقیقه بعد بفهمی رفتارش اشتباه بوده، هزینه آن اشتباه معمولا کم است. اگر سه هفته بعد و بعد از اینکه چند تغییر دیگر روی همان قسمت انجام شده بفهمی، داستان کاملا فرق میکند. بنابراین چیزی که واقعا اهمیت دارد خود تست نیست؛ کوتاه بودن حلقه بازخورد است. تست یکی از ابزارهای این کار است، نه خود هدف.
وقتی این نگاه را کمی بزرگتر کنیم، میبینیم که همین مسئله در کل چرخه تحویل نرمافزار وجود دارد. فرض کن یک تغییر را نوشتهای، تستهایش هم موفق شدهاند و کد هم بازبینی شده است. آیا کار تمام شده؟ اگر هنوز نمیتوانی با اطمینان آن را منتشر کنی، شاید نه. اگر بعد از انتشار نمیتوانی بفهمی چه اثری روی سیستم گذاشته، باز هم نه. اگر در صورت بروز مشکل نمیدانی چطور سریع اثرش را محدود یا تغییر را برگردانی، باز هم نه. یعنی «تمام شدن توسعه» لزوما به معنی «تمام شدن تغییر» نیست. تغییر زمانی واقعا تمام میشود که وارد سیستم واقعی و پروداکشن شده باشی و بتوانی از رفتار آن بازخورد بگیری.
اینجاست که به نظرم نگاه دواپس اهمیت پیدا میکند. DevOps را اگر از تمام ابزارها و شعارهای اطرافش جدا کنیم، یک ایده نسبتا ساده دارد: فاصله بین تغییر و فهمیدن نتیجه تغییر باید تا جای ممکن کوتاه باشد. کد را تغییر میدهی، آن را میسازی، تست میکنی، منتشر میکنی و بعد باید بتوانی بفهمی در دنیای واقعی چه اتفاقی افتاده است. این «بعد» بخش کوچکی از کار نیست؛ ادامه همان کار است.
مشکل اینجاست که خیلی وقتها محیط آزمایشی را جایگزین واقعیت میکنیم. هرچقدر هم محیط آزمایش خوب باشد، باز هم محیط واقعی نیست. در محیط واقعی دادههای واقعی داریم، کاربران واقعی داریم، حجم واقعی درخواستها را داریم، رفتارهایی داریم که کسی برایشان آزمایش ننوشته و وابستگیهایی داریم که ممکن است دقیقا در همان لحظهای که سیستم ما تغییر کرده، رفتار متفاوتی نشان دهند. بنابراین هدف مهندسی خوب این نیست که قبل از انتشار به جایی برسیم که مطمئن باشیم هیچ خطایی وجود ندارد؛ چنین نقطهای عملا وجود ندارد. هدف این است که وقتی چیزی را نمیدانیم، آن را با کمترین هزینه و کمترین دامنه اثر کشف کنیم.
برای همین انتشار مرحلهای برای من فقط یک تکنیک مربوط به دیپلوی نیست؛ یک تصمیم مهندسی درباره مدیریت عدم قطعیت است. اگر یک تغییر را یکباره روی کل سیستم منتشر کنیم، در واقع داریم ریسک را یکجا قبول میکنیم. اگر همان تغییر را ابتدا روی بخش کوچکی از سیستم اجرا کنیم، داریم امکان اشتباه را قبول میکنیم ولی اندازه اشتباه را محدود میکنیم. بعد اگر رفتار سیستم طبیعی بود، دامنه را بزرگتر میکنیم. این تفاوت مهمی است: ما دیگر سعی نمیکنیم ریسک را حذف کنیم؛ سعی میکنیم آن را قابل کنترل کنیم.
البته این روش فقط وقتی ارزش دارد که بتوانیم بفهمیم چه اتفاقی افتاده. چیزی که با Observability تعریف میشود که به نظرم این مفهوم معمولا با داشتن چند نمودار و گراف و هشدار اشتباه گرفته میشود. ممکن است دهها داشبورد داشته باشی و باز هم وقتی کاربر نمیتواند خرید کند، نمیدانی مشکل دقیقا کجاست. CPU و حافظه طبیعیاند، سرویسها بالا هستند، تعداد خطاها هم زیاد نیست، ولی کاربر درنهایت نمیتواند کار اصلی محصول را انجام دهد. اینجا سیستم از دید زیرساخت سالم است و از دید کسبوکار خراب.
بعد از چنین اتفاقی سؤال جالبتر از «چرا خراب شد؟» این است که «چرا زودتر نفهمیدیم خراب شده؟» چون پاسخ این سؤال معمولا چیزی را درباره خود سیستم آشکار میکند. شاید معیار اشتباهی را اندازه گرفتهایم. شاید یک رفتار مهم کاربر اصلا قابل مشاهده نیست. شاید هشدار لازم را نداشتهایم. شاید طراحی سیستم طوری است که تشخیص خطا را سخت میکند. در واقع Incident فقط یک مشکل در نرمافزار نیست؛ گاهی یک مشکل در توانایی ما برای دیدن نرمافزار و عدم درک درست از مبحث Observability است.
اینجا یک حلقه جالب شکل میگیرد. یک Incident اتفاق میافتد، علتش را پیدا میکنیم و اصلاحش میکنیم؛ اما اگر کارمان فقط همین باشد، چیز زیادی یاد نگرفتهایم. ممکن است لازم باشد علاوه بر اصلاح کد، یک تست جدید اضافه کنیم تا همان خطا دوباره برنگردد، یک معیار جدید اضافه کنیم تا دفعه بعد زودتر متوجه شویم، روش انتشار را تغییر دهیم تا اثر چنین خطایی محدودتر باشد یا حتی طراحی سیستم را عوض کنیم. به این ترتیب، Incident تبدیل میشود به ورودی چرخه توسعه بعدی و یک نقطه قوت.
به نظرم این یکی از تفاوتهای مهم بین تیمی است که فقط Bugها را برطرف میکند و تیمی که واقعا از سیستمش یاد میگیرد. در تیم اول، Incident یک اتفاق ناخوشایند است که باید هرچه سریعتر بسته شود. در تیم دوم، Incident فرصتی است برای اینکه یک ضعف دائمی در سیستم پیدا شود و یک لایه دفاعی جدید به آن اضافه شود. ممکن است آن لایه یک تست باشد، یک هشدار باشد، یک محدودیت در کد باشد، یک تغییر معماری باشد یا حتی یک تغییر در نحوه انتشار. مهم این است که دانش بهدستآمده از حادثه دوباره وارد سیستم شود.
اگر از این زاویه نگاه کنیم، شاید حتی تعریفمان از کیفیت نرمافزار هم کمی تغییر کند. نرمافزار باکیفیت فقط نرمافزاری نیست که باگ کمتری دارد. نرمافزاری است که وقتی چیزی در آن اشتباه پیش میرود، بتوانیم سریع بفهمیم چه اتفاقی افتاده، اثرش را محدود کنیم و از آن اتفاق چیزی به سیستم اضافه کنیم تا دفعه بعد وضعیت کمی بهتر باشد. این تعریف شاید خیلی جذابتر از این باشد که صرفاً بگوییم «تست کاورج ما ۸۰ درصد است!».
حالا این موضوع با آمدن Agentهای هوش مصنوعی جالبتر شده است. چون اگر یک Agent بتواند کد را خیلی سریعتر از یک انسان تولید کند، یک سؤال جدید به وجود میآید: خب بعدش چی؟ اگر تولید کد ده برابر سریعتر شود ولی بازبینی، تست، دیپلوی و گرفتن بازخورد همان سرعت قبلی را داشته باشند، فقط سرعت تولید چیزی را زیاد کردهایم که هنوز نمیتوانیم به همان سرعت بررسی و تحویلش بگیریم. در واقع ممکن است گلوگاه از نوشتن کد به جاهای دیگری منتقل شده.
برای همین فکر نمیکنم مهمترین ارزش Agentها صرفا این باشد که «سریعتر کد مینویسند». ارزش خیلی بزرگتر میتواند این باشد که بتوانند بخشی از همین حلقه بازخورد را هم کوتاه کنند. مثلا تغییر را ایجاد کنند، تستهای لازم را اجرا کنند، آن را در یک محیط محدود(سندباکس) قرار دهند، رفتار سیستم را بررسی کنند و بر اساس نتیجه تصمیم بگیرند که تغییر آماده تحویل است یا نه. آنجا Agent دیگر صرفا یک برنامهنویس سریعتر نیست؛ تبدیل میشود به بخشی از سیستم مهندسی ما.
البته اینجا یک دام جدی هم وجود دارد. اگرFlow ما خراب باشد، Agent میتواند همان فلوی خراب را سریعتر اجرا کند. اگر نیازمندیها مبهم باشد، Agent میتواند سریعتر یک راهحل اشتباه بسازد. اگر بازبینی گلوگاه باشد، کد بیشتری پشت صف بازبینی جمع میشود. اگر انتشار پرریسک باشد، Agent میتواند تعداد تغییرات پرریسک بیشتری تولید کند. ممکن است یک تغییر فقط چند ساعت کار واقعی داشته باشد ولی سه روز در انتظار بازبینی، تأیید یا استقرار بماند. در این حالت اگر زمان نوشتن کد را نصف کنیم، اتفاق چندان مهمی نیفتاده است. یا ممکن است دیپلوی فقط پنج دقیقه طول بکشد، اما بعد از آن دو روز طول بکشد تا بفهمیم آن تغییر مشکلی ایجاد کرده. اینجا هم سریعتر کردن دیپلوی احتمالا مسئله اصلی نیست. چیزی که باید کوتاه شود فاصله بین «تغییر» و «فهمیدن نتیجه تغییر» است.
به همین دلیل است که هرچه بیشتر به این موضوع فکر میکنم، کمتر برایم مهم است که اسم روش مورد استفاده چیست. حالا TDD میتواند مفید باشد، Scrum میتواند در یک تیم جواب بدهد، مدلسازی دامنه در یک سیستم پیچیده واقعا ارزشمند باشد و روشهای مختلف دیگری هم هرکدام جای خودشان را داشته باشند. ولی هیچکدام نباید جای سؤال اصلی را بگیرند که آیا تیم ما یک چرخه کوتاه، قابل مشاهده و قابل اعتماد برای تغییر دادن سیستم و یاد گرفتن از نتیجه آن دارد؟ اگر داشته باشد، خیلی از تکنیکها به شکل طبیعی سر جای خودشان قرار میگیرند. تست جایی استفاده میشود که بازخورد سریع لازم است. انتشار مرحلهای جایی استفاده میشود که عدم قطعیت بالاست. Observability جایی جدی میشود که رفتار سیستم را نمیتوان از قبل کامل پیشبینی کرد. Incidentها تبدیل به ورودی توسعه میشوند و Agentها هم میتوانند در جاهایی وارد شوند که واقعا گلوگاه را کم میکنند.
در نهایت مهندسی نرمافزار درباره ساختن یک سیستم خوب است که بتواند از تغییرات خودش یاد بگیرد. تغییر بدهی، با ریسک محدود امتحانش کنی، ببینی چه اتفاقی افتاده، اگر اشتباه بود سریع بفهمی، اصلاحش کنی و کاری کنی که دفعه بعد سیستم کمی بهتر از قبل باشد. اگر چنین چرخهای داشته باشیم، شاید دیگر خیلی مهم نباشد اسمش چیست.