Skip to content

فصل ۹ — مدولاریته

مدولاریته به‌عنوان «میزانی که اجزای یک سیستم می‌توانند از هم جدا و دوباره ترکیب شوند، اغلب با مزیت انعطاف و تنوع در استفاده» تعریف می‌شود.[^1]

من مدت‌هاست که کد می‌نویسم و از همان آغاز یادگیری‌ام، حتی وقتی بازی‌های ویدیویی ساده را با زبان اسمبلی می‌نوشتم، مدولاریته به‌عنوان چیزی مهم در طراحی کدمان ستایش می‌شد.

با این حال، بخش زیادی از کدی که دیده‌ام — در واقع بیشتر کدی که دیده‌ام و شاید حتی بخشی از کدی که خودم نوشته‌ام — از مدولار بودن دور بوده است. در یک نقطه این برای من تغییر کرد. کد من اکنون همیشه مدولار است؛ بخشی جدانشدنی از سبک من شده است.

مدولاریته برای مدیریت پیچیدگی سیستم‌هایی که می‌سازیم حیاتی است. سیستم‌های نرم‌افزاری مدرن عظیم، پیچیده و اغلب واقعاً پیچیده‌اند. بیشتر سیستم‌های مدرن از ظرفیت هر انسانی برای نگه‌داشتن همهٔ جزئیات در ذهن فراتر می‌روند.

برای کنار آمدن با این پیچیدگی، باید سیستم‌هایی که می‌سازیم را به قطعات کوچک‌تر و قابل‌فهم‌تر تقسیم کنیم — قطعاتی که بتوانیم روی آن‌ها تمرکز کنیم بدون آنکه زیاد نگران آنچه در جای دیگر سیستم می‌گذرد باشیم.

این همیشه درست است و باز هم این ایده‌ای فراکتالی است که در مقیاس‌های مختلف عمل می‌کند.

به‌عنوان یک صنعت، پیشرفت کرده‌ایم. وقتی کارم را شروع کردم، کامپیوترها و نرم‌افزارشان ساده‌تر بودند، اما برای انجام کارها باید سخت‌تر تلاش می‌کردیم. سیستم‌عامل‌ها فراتر از دسترسی به فایل‌ها و نمایش متن روی صفحه کار چندانی نمی‌کردند. هر چیز دیگری که لازم داشتیم باید برای هر برنامه از صفر نوشته می‌شد. می‌خواهید چیزی چاپ کنید؟ باید تعاملات سطح پایین با چاپگر مشخص خودتان را بفهمید و بنویسید.

قطعاً با بهبود انتزاع و مدولاریته سیستم‌عامل‌ها و سایر نرم‌افزارها جلو رفته‌ایم.

با این حال، بسیاری از سیستم‌ها خودشان مدولار به نظر نمی‌رسند. دلیلش این است که طراحی سیستم‌های مدولار کار سختی است. اگر توسعهٔ نرم‌افزار دربارهٔ یادگیری باشد، با یادگیری، درک ما تکامل می‌یابد و تغییر می‌کند. پس ممکن است — بلکه محتمل است — که دیدگاه ما دربارهٔ اینکه کدام ماژول‌ها منطقی‌اند و کدام‌ها نه، هم با گذشت زمان تغییر کند.

برای من، این مهارت واقعی توسعهٔ نرم‌افزار است. این همان ویژگی است که بیش از هر چیز دیگر کد نوشته‌شده توسط خبرگان و استادان حرفه‌شان را از کد تازه‌کاران متمایز می‌کند. در حالی که به مهارت نیاز دارد تا در طراحی‌هایمان مدولاریتهٔ خوبی به دست آوریم، آنچه در بخش زیادی از کدی که می‌بینم درک می‌کنم این است که مردم فقط «مدولاریته را بد انجام نمی‌دهند»، بلکه «اصلاً تلاش نمی‌کنند». بسیاری از کدها طوری نوشته می‌شوند انگار یک دستور پخت است؛ یعنی دنباله‌ای خطی از گام‌ها که در متدها و توابعی جمع شده‌اند و صدها یا حتی هزاران خط کد را در بر می‌گیرند.

یک لحظه تصور کنید اگر ویژگی‌ای در کدبیس شما فعال کنیم که هر کدی را که متدی طولانی‌تر از ۳۰ خط — یا ۵۰ یا ۱۰۰ — دارد رد کند؟ کد شما از چنین آزمایشی عبور می‌کند؟ می‌دانم که بیشتر کدی که در دنیای واقعی دیده‌ام عبور نمی‌کند.

امروزه وقتی پروژهٔ نرم‌افزاری را شروع می‌کنم، در خط لولهٔ استقرار تحویل مستمر، در «مرحلهٔ commit»، بررسی‌ای قرار می‌دهم که دقیقاً همین نوع آزمایش را انجام می‌دهد و هر commitای را که متدی طولانی‌تر از ۲۰ یا ۳۰ خط دارد رد می‌کند. همچنین امضای متدهایی با بیش از پنج یا شش پارامتر را رد می‌کنم. این مقادیر دلخواه‌اند، بر اساس تجربه و ترجیحات من با تیم‌هایی که با آن‌ها کار کرده‌ام. منظورم توصیه به این مقادیر خاص نیست؛ بلکه این است که «نرده‌های راهنما»یی مثل این برای صادق نگه‌داشتن ما در طراحی مهم‌اند. هر فشار زمانی که باشد، نوشتن کد بد هرگز صرفه‌جویی در زمان نیست!

نشانه‌های مدولاریته

چطور بفهمید سیستم شما مدولار است؟ سطح ساده‌ای وجود دارد به این معنا که ماژول مجموعه‌ای از دستورالعمل‌ها و داده است که می‌تواند در برنامه گنجانده شود. این نمایش «فیزیکی» بیت‌ها و بایت‌هایی را که ماژول را می‌سازند به تصویر می‌کشد.

اما از نظر عملی‌تر، آنچه دنبالش هستیم چیزی است که کد را به بخش‌های کوچک تقسیم می‌کند. هر بخش می‌تواند چندین بار، شاید در زمینه‌های مختلف، دوباره استفاده شود.

کد در یک ماژول آنقدر کوتاه است که به‌راحتی به‌عنوان یک موجودیت مستقل، خارج از زمینهٔ سایر بخش‌های سیستم، قابل فهم باشد — حتی اگر برای انجام کار مفید به سایر بخش‌های سیستم نیاز داشته باشد.

کنترلی روی دامنهٔ متغیرها و توابع وجود دارد که دسترسی به آن‌ها را محدود می‌کند، به‌طوری‌که در نوعی مفهوم «بیرون» و «داخل» ماژول وجود داشته باشد. رابطی از نوعی وجود دارد که دسترسی را کنترل می‌کند، ارتباط با کد دیگر را مدیریت می‌کند و با ماژول‌های دیگر سر و کار دارد.

کم‌ارزش گذاشتن به اهمیت طراحی خوب

چند دلیل وجود دارد که چرا بسیاری از توسعه‌دهندگان نرم‌افزار به ایده‌هایی مثل این توجه نمی‌کنند. به‌عنوان یک صنعت، اهمیت طراحی نرم‌افزار را کم‌ارزش گذاشته‌ایم. روی زبان‌ها و فریم‌ورک‌ها وسواس داریم. دربارهٔ IDE در برابر ویرایشگر متنی یا برنامه‌نویسی شی‌گرا در برابر تابعی بحث می‌کنیم. با این حال هیچ‌کدام از این‌ها به اندازهٔ ایده‌هایی مثل مدولاریته یا تفکیک دغدغه‌ها برای کیفیت خروجی ما مهم و بنیادی نیستند.

اگر کدی با مدولاریته و تفکیک دغدغه‌های خوب داشته باشید — هر پارادایم برنامه‌نویسی، زبان یا ابزاری که باشد — بهتر، راحت‌تر برای کار، راحت‌تر برای تست و راحت‌تر برای تغییر خواهد بود با توجه به آنچه دربارهٔ مسئله‌ای که می‌خواهید حل کنید یاد می‌گیرید. همچنین از کدی که این ویژگی‌ها را ندارد انعطاف‌پذیرتر در استفاده خواهد بود.

برداشت من این است که یا اصلاً این مهارت‌ها را آموزش نمی‌دهیم یا چیزی ذاتی در برنامه‌نویسی (یا برنامه‌نویسان) وجود دارد که ما را وادار می‌کند اهمیت آن‌ها را نادیده بگیریم.

واضح است که طراحی برای مدولاریته نوعی مهارت متفاوت از دانستن نحو یک زبان برنامه‌نویسی است. مهارتی است که اگر امید داریم به درجه‌ای از تسلط برسیم باید روی آن کار کنیم و می‌توانیم یک عمر صرف کنیم و احتمالاً هرگز آن را کامل نکنیم.

اما برای من، توسعهٔ نرم‌افزار واقعاً دربارهٔ همین است. چطور می‌توانیم کد و سیستم‌هایی بسازیم که در طول زمان رشد و تکامل یابند اما به‌اندازهٔ کافی بخش‌بندی شده باشند تا اگر اشتباه کردیم آسیب را محدود کنند؟ چطور سیستم‌هایی بسازیم که به‌اندازهٔ کافی انتزاعی باشند تا بتوانیم مرزهای بین ماژول‌ها را فرصتی برای بهبود سیستم‌ها بدانیم نه بدهی‌هایی که مانع تغییرشان می‌شوند؟

این نکتهٔ مهمی در تز این کتاب است.

یک بار کلاسی دربارهٔ توسعهٔ مبتنی بر آزمون (TDD) تدریس کردم. می‌خواستم نشان دهم TDD چطور می‌تواند پیچیدگی طراحی‌هایمان را کاهش دهد که یکی از شرکت‌کنندگان (آن‌ها را برنامه‌نویس نمی‌نامم) پرسید چرا مهم است که کد کم‌پیچیده‌تر باشد. اعتراف می‌کنم شوکه شدم. اگر این شخص تفاوت تأثیر و ارزش بین کد مبهم و پیچیده و کد روشن و ساده را نمی‌دید، دید متفاوتی از شغل ما دارد. بهترین تلاشم را برای پاسخ کردن کردم و دربارهٔ اهمیت نگهداشت‌پذیری و مزایای کارایی صحبت کردم، اما متقاعد نیستم استدلال‌هایم تأثیر زیادی گذاشت.

در اصل، پیچیدگی هزینهٔ مالکیت نرم‌افزار را افزایش می‌دهد. این هم تأثیر اقتصادی مستقیم دارد و هم تأثیر ذهنی‌تر: روی کد پیچیده کار کردن به اندازهٔ کد ساده لذت‌بخش نیست!

اما مسئلهٔ واقعی این است که کد پیچیده، تا حدی تعریفاً، تغییر دادنش سخت‌تر است. یعنی وقتی اولین بار می‌نویسیدش فقط یک شانس دارید درست از آب درآید. همچنین اگر کدم پیچیده باشد، احتمالاً آن را آن‌طور که فکر می‌کنم نمی‌فهمم؛ جاهای بیشتری برای پنهان شدن اشتباهات وجود دارد.

اگر برای محدود کردن پیچیدگی کدی که می‌نویسیم تلاش کنیم، می‌توانیم اشتباه کنیم و شانس بهتری برای اصلاحش داشته باشیم. پس یا می‌توانیم روی نبوغ خودمان شرط ببندیم و فرض کنیم همه‌چیز را از اول کاملاً درست می‌کنیم، یا محتاط‌تر پیش برویم. با فرض این شروع می‌کنیم که چیزهایی بوده که به آن‌ها فکر نکردیم، سوءتفاهم‌ها و تغییرات در دنیا وجود دارد که احتمالاً روزی باید کدمان را دوباره ببینیم. پیچیدگی هزینه دارد!

مهم است که به ایده‌های جدید باز باشیم. مهم است که مداوم فرضیاتمان را زیر سؤال ببریم. با این حال، این به معنای برابری ارزش همهٔ ایده‌ها نیست. ایده‌های احمقانه‌ای هست که باید رد شوند؛ ایده‌های عالی‌ای هست که باید ارزشمند شمرده شوند.

دانستن نحو یک زبان برای «برنامه‌نویس» بودن — چه برسد به برنامه‌نویس خوب — کافی نیست. «اصطلاحی بودن در زبان X» کم‌ارزش‌تر و کم‌اهمیت‌تر از کیفیت بالا در طراحی است. دانستن جزئیات عجیب «API Y» شما را توسعه‌دهندهٔ بهتری نمی‌کند؛ همیشه می‌توانید پاسخ آن نوع سؤال را جستجو کنید!

مهارت‌های واقعی — چیزهایی که واقعاً برنامه‌نویسان عالی را از ضعیف متمایز می‌کنند — وابسته به زبان یا فریم‌ورک خاص نیستند. جای دیگری نهفته‌اند.

هر زبان برنامه‌نویسی فقط یک ابزار است. افتخار کار با چند برنامه‌نویس در سطح جهانی را داشته‌ام. این افراد در زبانی که هرگز استفاده نکرده‌اند کد خوب می‌نویسند. در HTML و CSS یا اسکریپت‌های پوستهٔ Unix یا YAML کد تمیز می‌نویسند. یکی از دوستانم حتی Perl خوانا می‌نویسد!

ایده‌هایی عمیق‌تر و عمیق‌تر از زبانی که برای بیانشان استفاده می‌شود وجود دارند. مدولاریته یکی از این ایده‌هاست؛ اگر کد شما مدولار نیست، تقریباً قطعاً به اندازهٔ کدی که مدولار است خوب نیست!

اهمیت قابلیت آزمون

من از adopters اولیهٔ TDD بودم و اولین گام‌های مرددانه‌ام را در پاسخ به کتاب Kent Beck Extreme Programming Explained که در ۱۹۹۹ منتشر شد برداشتم. تیم من با ایده‌های جذاب Kent آزمایش کرد و همان سال اشتباهشان را هم کرد. با این حال، از این رویکرد سود زیادی بردیم.

TDD یکی از مهم‌ترین گام‌هایی است که عمل توسعهٔ نرم‌افزار در طول دوران حرفه‌ام برداشته است. گیج‌کننده این است که دلیل ارزش بالای آن برای من ربط چندانی به «آزمون» به معنای معمول ندارد. در واقع اکنون فکر می‌کنم Kent Beck از نظر بازاریابی اشتباه کرد که «test» را در نام این عمل گنجاند — و نه، نمی‌دانم به‌جایش چه باید می‌نامید!

فصل ۵ توضیح داد چطور می‌توانیم از آزمون‌هایمان بازخورد سریع و دقیق دربارهٔ کیفیت طراحی‌هایمان بگیریم و چطور قابل‌آزمون کردن کد کیفیتش را بالا می‌برد. این ایده‌ای فوق‌العاده مهم است.

چیزهای کمی — فراتر از سلیقهٔ خوب یک برنامه‌نویس باتجربه و ماهر — می‌توانند به ما بازخورد زودهنگام دربارهٔ کیفیت طراحی بدهند. ممکن است هفته‌ها، ماه‌ها یا سال‌ها بعد هنگام تلاش برای تغییر طراحی بفهمیم خوب یا بد است، اما جز آن، معیار عینی کیفیت وجود ندارد — مگر اینکه بخواهیم طراحی را از آزمون‌ها هدایت کنیم.

طراحی برای قابلیت آزمون، مدولاریته را بهبود می‌دهد

اگر نوشتن آزمون‌هایمان سخت باشد، یعنی طراحی‌مان ضعیف است. فوراً سیگنال می‌گیریم. هنگام تلاش برای پالایش طراحی برای افزایش بعدی رفتار، بازخورد کیفیت طراحی می‌گیریم. اگر از انضباط Red, Green, Refactor در TDD پیروی کنیم، این درس‌ها خودکار به ما می‌رسند. وقتی نوشتن آزمون سخت است، طراحی‌مان بدتر از آنچه باید باشد است. اگر نوشتن آزمون آسان باشد، کدی که آزمون می‌کنیم ناگزیر ویژگی‌هایی را که به‌عنوان نشانه‌های کیفیت بالا در کد ارزش می‌نهیم نشان می‌دهد.

این به این معنا نیست که رویکرد test-driven به‌طور خودکار طراحی عالی می‌سازد. چوب جادویی نیست. هنوز به مهارت و تجربهٔ طراح وابسته است. توسعه‌دهندهٔ عالی همچنان نتیجهٔ بهتری می‌سازد. آنچه هدایت طراحی از آزمون‌ها انجام می‌دهد این است که ما را تشویق می‌کند کد و سیستم‌های قابل‌آزمون بسازیم و بنابراین، با توجه به محدودیت‌های تجربه و استعدادمان، نتیجه را بهبود می‌دهد.

فکر نمی‌کنم تکنیک دیگری داشته باشیم که تا این حد چنین کند. این تقویت‌کنندهٔ استعداد ابزار مهمی است اگر بخواهیم از صنعت‌گری به مهندسی برسیم.

اگر آرزوی مهندس بودن داریم، توصیه به «بهتر انجام دهید» کافی نیست. به ابزارهایی نیاز داریم که ما را هدایت کنند و به نتیجهٔ بهتر کمک کنند. تلاش برای قابلیت آزمون در سیستم‌هایمان یکی از این ابزارهاست.

بیایید به موضوع برگردیم و این را به‌طور خاص در زمینهٔ مدولاریته در نظر بگیریم. طراحی برای دستیابی به قابلیت آزمون چطور مدولاریتهٔ بیشتر را تشویق می‌کند؟

اگر بخواهم اثربخشی بال هو (airfoil) یک بال هواپیما را آزمون کنم، می‌توانم هواپیما را بسازم و پرواز کنم. این ایدهٔ وحشتناکی است که حتی برادران رایت، سازندگان اولین هواپیمای کنترل‌شدهٔ موتوردار، فهمیدند جواب نمی‌دهد.

اگر این رویکرد ساده‌لوحانه را بگیرید، باید همهٔ کار را قبل از یادگیری چیزی انجام دهید. با این روش یادگیری، چطور اثربخشی این airfoil را در برابر دیگری می‌سنجید؟ هواپیمای دیگری بسازید؟

حتی آن‌گاه چطور نتایج را مقایسه می‌کنید؟ شاید باد هنگام پرواز نمونهٔ اول تندتر بوده. شاید خلبان در پرواز اول صبحانهٔ بیشتری خورده. شاید فشار هوا یا دما متفاوت بوده و بال‌ها مقدار متفاوتی برآ ایجاد کرده‌اند. شاید دستهٔ سوخت بین دو نمونه متفاوت بوده و موتور توان متفاوتی تولید کرده. چطور همهٔ این متغیرها را مدیریت می‌کنید؟

اگر رویکرد waterfall کل‌سیستمی برای حل این مسئله بگیرید، پیچیدگی سیستم اکنون کل محیطی را که airfoil در آن عمل می‌کند در بر می‌گیرد.

راه علمی سنجش airfoil این است که این متغیرها را کنترل و در آزمایش‌ها استاندارد کنیم. چطور پیچیدگی را کاهش دهیم تا سیگنال‌هایی که از آزمایش برمی‌گردد روشن باشند؟ می‌توانیم دو هواپیما را در محیط کنترل‌شده‌تری — شاید چیزی شبیه تونل باد بزرگ — قرار دهیم. این کنترل دقیق‌تر جریان هوا روی بال‌ها و باد را ممکن می‌کند. شاید در محیط با دما و فشار کنترل‌شده انجام دهیم. فقط با این نوع کنترل می‌توانیم به نتایج تکرارپذیرتر برسیم.

اگر بخواهیم این مسیر را برویم، واقعاً به موتور یا کنترل‌های پرواز یا بقیهٔ هواپیما نیاز نداریم. چرا فقط دو مدل از بال‌ها با airfoilهایی که می‌خواهیم آزمون کنیم نسازیم و آن‌ها را در تونل باد با دما و فشار کنترل‌شده امتحان کنیم؟

این قطعاً آزمایش دقیق‌تری از پرواز کردن است، اما هنوز باید کل بال را دو بار بسازیم. چرا مدل کوچکی از هر airfoil نسازیم؟ هر مدل را تا حد ممکن دقیق، با همان مواد و تکنیک‌ها بسازیم و دو تا را مقایسه کنیم. اگر تا اینجا برویم، می‌توانیم در مقیاس کوچک‌تر انجام دهیم و به تونل باد ساده‌تری نیاز داریم.

این تکه‌های کوچک هواپیما ماژول‌اند. بخش‌هایی که قطعاً به رفتار کل هواپیما می‌افزایند، اما روی بخش خاصی از مسئله متمرکز‌اند. درست است که چنین آزمایش‌هایی فقط تصویر نسبتاً درستی می‌دهند. آیرودینامیک هواپیماها پیچیده‌تر از فقط بال‌هاست، اما مدولاریته یعنی می‌توانیم چیزهایی را بسنجیم که بدون آن نمی‌توانستیم، پس بخش — ماژول — قطعاً از کل قابل‌آزمون‌تر است.

در دنیای واقعی، همین‌طور آزمایش می‌کنید تا بفهمید شکل بال‌ها و چیزهای دیگر چطور بر برآ اثر می‌گذارد.

مدولاریته کنترل و دقت بیشتری در چیزهایی که می‌توانیم بسنجیم به ما می‌دهد. بیایید این مثال را به دنیای نرم‌افزار ببریم. تصور کنید روی System B کار می‌کنید که downstream از System A و upstream از System C است (شکل ۹.۱).

System A    →    System B    →    System C

شکل ۹.۱ — سیستم‌های جفت‌شده

این در کار روی سیستم‌های بزرگ در سازمان‌های پیچیده معمول است. این مسئله‌ای ایجاد می‌کند: چطور کارمان را آزمون کنیم؟ بسیاری — شاید اکثر — سازمان‌هایی که با این مسئله روبه‌رو می‌شوند به فرض می‌رسند که برای اطمینان از ایمنی سیستم، آزمون همه‌چیز با هم ضروری است.

مشکلات زیادی با این رویکرد وجود دارد. اول، اگر فقط در این مقیاس بسنجیم، با مسئلهٔ «آزمون کل هواپیما» روبه‌رویم. کل سیستم آنقدر پیچیده است که دقت، تکرارپذیری، کنترل و دید روشن از معنای واقعی نتایج را نداریم.

نمی‌توانیم بخش خودمان از سیستم را با دقت ارزیابی کنیم، چون بخش‌های upstream و downstream — System A و System C — سر راه ما هستند. انواع زیادی از آزمون‌ها به‌خاطر این تصمیم ساده غیرممکن می‌شوند. اگر System A به System B پیام‌های malformed بفرستد چه می‌شود؟

آن حالت غیرقابل سنجش است در حالی که System A واقعی سر جایش است و پیام‌های well-formed می‌فرستد. System B وقتی کانال ارتباطی به System C قطع است چطور باید واکنش نشان دهد؟

باز هم نمی‌توانیم آن سناریو را آزمون کنیم در حالی که System C واقعی با comms سالم سر راه ماست و مانع جعل خطای comms می‌شود.

نتایجی که جمع می‌کنیم چیز زیادی نمی‌گویند. اگر آزمون fail شود، آیا به‌خاطر مشکل سیستم ماست یا یکی از دیگران؟ شاید fail یعنی نسخهٔ اشتباه سیستم‌های upstream یا downstream را داریم. اگر همه‌چیز کار کند، آیا آمادهٔ release هستیم؟ یا چون mega-system قابل‌آزمون نیست، موارد ارزیابی آنقدر ساده‌اند که باگ‌های واقعی را پیدا نمی‌کنند؟

اگر کل سیستم composite را بسنجیم (شکل ۹.۲)، نتایج مبهم و گیج‌کننده‌اند. نمودار کارتونی شکل ۹.۲ مسئلهٔ مهمی را نشان می‌دهد: باید روشن باشیم چه چیزی را می‌سنجیم و ارزش اندازه‌گیری‌هایمان چیست. اگر آزمون end-to-end مثل این نمودار انجام دهیم، هدف آزمون چیست؟ چه چیزی را می‌خواهیم نشان دهیم؟ اگر هدف نشان دادن این است که همهٔ قطعات با هم کار می‌کنند، شاید در بعضی زمینه‌ها مفید باشد، اما این سبک آزمون برای گفتن اینکه System B — که در این سناریو واقعاً مسئول آن هستیم — واقعاً کار می‌کند کافی نیست. چنین آزمون‌هایی فقط به‌عنوان مکمل — مکمل کوچک — یک استراتژی آزمون بهتر، کامل‌تر و مدولارتر منطقی‌اند. قطعاً جایگزین آزمون دقیق‌تر لازم برای نشان دادن کارکرد صحیح System B نمی‌شوند!

System A ── System B ── System C
     (آزمون end-to-end روی کل زنجیره)

شکل ۹.۲ — آزمون سیستم‌های جفت‌شده

پس برای آن آزمون دقیق‌تر چه لازم است؟ به نقطهٔ اندازه‌گیری نیاز داریم — جایی در کل سیستم که بتوانیم probeهایی درج کنیم تا اندازه‌گیری‌ها را به‌طور قابل‌اتکا جمع کنیم. این «نقاط اندازه‌گیری» را در این سری نمودارها به‌صورت کaliperهای فرضی نشان داده‌ام. در واقعیت، منظور توانایی تزریق دادهٔ آزمون به system under test (SUT)، فراخوانی رفتارها در آن و جمع خروجی‌ها برای تفسیر نتایج است. می‌دانم caliperها کمی cheesy هستند، اما این مدل ذهنی من هنگام آزمون سیستم است. سیستم را در test rigی از نوعی قرار می‌دهم تا ارزیابی کنم. به دستگاه اندازه‌گیری (موارد آزمون و زیرساخت آزمون) نیاز دارم که probeها را به SUT وصل کند تا ببینم چطور رفتار می‌کند.

caliperهای من در شکل ۹.۲ به دلایلی که بررسی کردیم و همچنین به‌خاطر بزرگ‌تر و پیچیده‌تر بودن سیستم — که نتایج متغیرتر می‌شود — چندان مفید نیستند. متغیرها را به‌اندازهٔ کافی کنترل نکرده‌ایم تا نتیجهٔ روشن و تکرارپذیر بگیریم.

اگر مجموعه‌ای از آزمون‌های خودکار برای ارزیابی نرم‌افزار و تعیین آمادگی release دارید و آن آزمون‌ها هر بار نتیجهٔ یکسان نمی‌دهند، آن نتایج واقعاً چه معنایی دارند؟

اگر هدف اعمال تفکر مهندسی است، باید اندازه‌گیری‌ها را جدی بگیریم. باید بتوانیم به آن‌ها تکیه کنیم، یعنی باید deterministic باشند. برای هر آزمون یا ارزیابی، با همان نسخهٔ نرم‌افزار تحت آزمون، هر چند بار هم اجرا کنم و هر چه دیگری در حال اجرا باشد، باید همان نتایج را ببینم.

ارزش کافی در این جمله هست که اگر لازم باشد کار اضافه برای دستیابی به آن نتیجهٔ تکرارپذیر انجام دهیم. بر نه‌تنها آزمون‌هایی که می‌نویسیم و نحوهٔ نوشتنشان، بلکه — مهم‌تر — طراحی نرم‌افزارمان اثر می‌گذارد و آنجاست که ارزش واقعی این رویکرد مهندسی آشکار می‌شود.

وقتی صرافی مالی را ساختیم، سیستم تا حدی کاملاً deterministic بود که می‌توانستیم ورودی‌های production را ضبط و بعداً در محیط آزمون replay کنیم تا سیستم دقیقاً به همان حالت برسد. با آن هدف شروع نکردیم. اثر جانبی میزان قابلیت آزمون — و بنابراین determinism — بود.

پیچیدگی و determinism

با رشد پیچیدگی system under test، دقت سنجش کاهش می‌یابد. مثلاً اگر قطعه‌ای از نرم‌افزار performance-critical دارم، می‌توانم آن را isolate کنم و در test rigی قرار دهم که اجراهای کنترل‌شدهٔ متوالی ایجاد کند. می‌توانم اجراهای اولیه را دور بیندازم تا اثر runtime optimization حذف شود و اجراهای کافی برای تکنیک‌های آماری روی داده انجام دهم. اگر همهٔ این‌ها را جدی بگیرم، اندازه‌گیری‌ها می‌توانند تا میکروثانیه و گاهی نانوثانیه دقیق و تکرارپذیر باشند.

انجام همین کارها در آزمون performance کل‌سیستمی، برای سیستمی با اندازهٔ قابل‌توجه، عملاً غیرممکن است. اگر performance کل سیستم را بسنجم، متغیرها منفجر شده‌اند. چه taskهای دیگری همزمان روی کامپیوترهایی که کد من اجرا می‌شود در جریان است؟ شبکه چطور؟ آیا هنگام اندازه‌گیری برای چیز دیگری استفاده می‌شود؟

حتی اگر این‌ها را کنترل کنم و شبکه و دسترسی به محیط آزمون performance را lock down کنم، سیستم‌عامل‌های مدرن پیچیده‌اند. اگر OS هنگام اجرای آزمون housekeeping انجام دهد چه؟ قطعاً نتایج را منحرف می‌کند، نه؟

با رشد پیچیدگی و دامنهٔ سیستم، دستیابی به determinism سخت‌تر می‌شود.

علت ریشه‌ای نبود determinism در سیستم‌های کامپیوتری concurrency است. این می‌تواند اشکال مختلف داشته باشد. ساعت که زمان سیستم را increment می‌کند یک شکل concurrency است؛ OS که وقتی وقت آزاد دارد دیسک را reorganize می‌کند شکل دیگری است. اما در غیاب concurrency، سیستم‌های دیجیتال deterministic‌اند. برای همان دنبالهٔ بایت‌ها و دستورالعمل‌ها، هر بار همان نتیجه را می‌گیریم.

یک محرک مفید مدولاریته isolate کردن concurrency است تا هر ماژول deterministic و قابل‌آزمون پایدار باشد. سیستم‌ها را طوری architect کنید که ورود به ماژول sequenced باشد و نتایجش قابل‌پیش‌بینی‌تر. سیستم‌هایی که این‌طور نوشته شده‌اند بسیار دلپذیر برای کار هستند.

شاید این نکته‌ای نسبتاً esoteric به نظر برسد، اما در سیستمی که هر رفتاری که کاربران observe می‌کنند deterministic باشد — به شیوه‌ای که توصیف کردم — eminently قابل‌پیش‌بینی و قابل‌آزمون با بدون side effectهای غیرمنتظره — حداقل تا محدودیت آزمون‌هایمان — خواهد بود.

بیشتر سیستم‌ها این‌طور ساخته نمی‌شوند، اما اگر رویکرد engineering-led به طراحی‌شان بگیریم، می‌توانند باشند.

اگر بتوانیم caliperها را فقط روی component خودمان بگذاریم (شکل ۹.۳)، می‌توانیم با دقت، precision و reliability بسیار بیشتری بسنجیم. analogy را تا حد شکستن کش می‌دهم — می‌توانیم ابعاد دیگر مسئله را هم بسنجیم.

     System B
   (آزمون ماژول به‌تنهایی)

شکل ۹.۳ — آزمون ماژول‌ها

پس برای این precision و specificity بیشتر چه لازم است؟ می‌خواهیم نقاط اندازه‌گیری پایدار باشند تا هر بار — با فرض برابر بودن سایر شرایط — همان نتیجه را بگیریم. می‌خواهیم ارزیابی‌ها deterministic باشند.

همچنین نمی‌خواهیم هر بار که سیستم تغییر می‌کند نقاط اندازه‌گیری را از صفر دوباره بسازیم.

روشن است که آنچه توصیف می‌کنم رابط مدولار پایدار به بخشی از سیستم است که می‌خواهیم آزمون کنیم. سیستم‌های بزرگ‌تر را از ماژول‌های کوچک‌تر با رابط‌های clearly defined برای ورودی و خروجی compose می‌کنیم. این رویکرد معماری به ما اجازه می‌دهد سیستم را در آن رابط‌ها بسنجیم.

امیدوارم خواندن اینجا واضح به نظر برسد. مسئله این است که سیستم‌های کامپیوتری دنیای واقعی کم‌تر این‌طور به نظر می‌رسند.

اگر آزمون خودکار سیستم‌هایی که می‌سازیم بخش بنیادی کارمان باشد، به‌شدت تشویق می‌شویم — با کار اضافه‌ای که اگر اشتباه کنیم مجبوریم انجام دهیم — سیستم‌های مدولارتر بسازیم. این فراکتالی است. در همهٔ سطوح granularity درست است — از کل سیستم‌های enterprise تا متدها، توابع و کلاس‌های منفرد.

به شیوه‌ای که این کتاب فرض می‌کند، آزمون سیستمی که به نوعی مدولار نباشد ساده‌لوحانه غیرممکن است. به آن «نقاط اندازه‌گیری» نیاز داریم. توانایی آزمون ما توسط مدولاریته پشتیبانی و تقویت می‌شود و مدولاریته با هدایت طراحی‌ها از آزمون‌ها تشویق می‌شود.

این لزوماً به معنای مجموعه‌ای از componentهای کوچک و مستقل نیست. برای سیستم‌های بزرگ پیچیده هم به‌طور مساوی کار می‌کند. کلید این است که دامنهٔ اندازه‌گیری که منطقی است را بفهمیم و برای آسان و پایدار بودن آن اندازه‌گیری‌ها در نتایج تلاش کنیم.

وقتی در ساخت صرافی مالی بودم، کل سیستم enterprise را یک سیستم واحد می‌دیدیم، اما برای هر تعامل خارجی نقطهٔ integration روشن و well-defined برقرار کردیم و آن سیستم‌های خارجی را fake کردیم. اکنون کنترل داشتیم؛ می‌توانستیم ثبت‌نام حساب جدید inject کنیم و داده‌ای که در عمل به بانک‌ها یا clearinghouseها فرستاده می‌شد جمع کنیم.

این به ما اجازه داد برای بعضی آزمون‌ها کل سیستم را black box بدانیم، داده inject کنیم تا سیستم به حالت مناسب آزمون برسد و خروجی‌ها را برای ارزیابی پاسخ جمع کنیم. هر نقطه‌ای که سیستم با سیستم third-party تعامل داشت و هر نقطهٔ integration را نقطهٔ اندازه‌گیری می‌دیدیم که می‌توانستیم به زیرساخت آزمون وصل کنیم. این فقط به‌خاطر طراحی کل سیستم enterprise از روز اول با قابلیت آزمون در ذهن ممکن بود.

سیستم ما همچنین فوق‌العاده مدولار و loosely coupled بود. پس علاوه بر ارزیابی کل سیستم، می‌توانستیم آزمون دقیق‌تر برای componentهای تک‌service-level هم انجام دهیم. ناگزیر همهٔ رفتار داخل یک service هم با تکنیک‌های TDD fine-grained برای تقریباً هر خط کد توسعه یافت. می‌توانستیم تکه‌های کوچک رفتار سیستم را از همه‌چیز isolate آزمون کنیم. همان‌طور که گفتم، مدولاریته و قابلیت آزمون فراکتالی‌اند.

قابلیت آزمون عمیقاً انتخاب‌های معماری ما را inform کرد و با گذشت زمان نه‌تنها بر معیارهای واضح کیفیت — مثل چند باگ پیدا می‌کنیم — بلکه ظریف‌تر و شاید مهم‌تر، بر compartmentalization معماری سیستم اثر عمیق گذاشت.

از نظر تاریخی، به‌عنوان صنعت، اهمیت قابلیت آزمون و به‌طور خاص test-first development را کم‌ارزش گذاشته یا حتی از دست داده‌ایم — به‌عنوان ابزاری برای هدایت طراحی خوب و بازخورد زودهنگام و روشن دربارهٔ کیفیت طراحی‌ها.

سرویس‌ها و مدولاریته

ایدهٔ service در اصطلاح نرم‌افزار نسبتاً لغزان است. مثلاً هیچ زبان mainstreamای مستقیماً ایدهٔ service را پشتیبانی نمی‌کند، با این حال ایده pretty pervasive است. توسعه‌دهندگان دربارهٔ service خوب و بد بحث می‌کنند و سیستم‌هایشان را برای پشتیبانی از این مفهوم architect می‌کنند.

از منظر کاملاً عملی، می‌توانیم service را کدی بدانیم که «خدمتی» به کد دیگر می‌دهد و جزئیات نحوهٔ ارائهٔ آن خدمت را پنهان می‌کند. این فقط ایدهٔ «information hiding» است و برای مدیریت پیچیدگی سیستم‌هایمان با رشد آن‌ها فوق‌العاده مهم است (فصل ۱۲ را ببینید). شناسایی «درزها» (seams) در طراحی سیستم‌هایمان که بقیهٔ سیستم نیازی به دانستن — و نباید اهمیت دهد — جزئیات آنچه آن طرف درزها می‌گذرد، ایدهٔ بسیار خوبی است. این واقعاً جوهرهٔ طراحی است.

پس serviceها ایدهٔ سازمان‌دهی بخش‌های کوچک در سیستم‌هایمان را می‌دهند که جزئیات را پنهان می‌کنند. ایدهٔ مفیدی است. پس service می‌تواند sensibly به‌عنوان ماژولی از سیستممان در نظر گرفته شود. اگر چنین است، آن «درزها»، نقاطی که service یا ماژول مرز خود را لمس می‌کند چطور؟ چیزی که به مفهوم «service» در اصطلاح نرم‌افزار معنا می‌دهد این است که مرزی را نمایندگی می‌کند. تفاوتی بین آنچه known است و آنچه exposed است، در دو طرف این مرزها وجود دارد.

یکی از رایج‌ترین مشکلاتی که در codebaseهای بزرگ‌تر می‌بینم نتیجهٔ نادیده گرفتن این تفاوت است. اغلب کدی که این مرزها را نمایندگی می‌کند از کد در دو طرفش قابل تمایز نیست. همان نوع فراخوانی متد را استفاده می‌کنیم و حتی همان ساختارهای داده را از این مرزها عبور می‌دهیم. در این نقاط validation ورودی یا assembly و انتزاع خروجی وجود ندارد. چنین codebaseهایی سریع به tangle تبدیل می‌شوند که تغییرشان سخت است.

در این زمینه پیشرفت کرده‌ایم، اما گام کوچکی است و تا حدی تصادفی برداشتیم: حرکت به REST APIها.

پس‌زمینهٔ من تا حدی high-performance computing است، پس ایدهٔ استفاده از text، XML یا HTML برای encode کردن اطلاعاتی که بین serviceها جریان دارد کمی سردم می‌کند؛ خیلی کند است! با این حال، به‌شدت ایدهٔ داشتن نقطهٔ translation در لبه‌های service یا API را تشویق می‌کند. پیام ورودی را به شکل tractableتری برای مصرف service ترجمه می‌کنید و خروجی service را به پیام text-based بزرگ، کند و horrible برای خروجی ترجمه می‌کنید. (ببخشید، biasهایم راه یافت.)

با این حال توسعه‌دهندگان هنوز اشتباه می‌کنند. حتی در سیستم‌هایی که این‌طور ساخته شده‌اند، هنوز کدی می‌بینم که HTML را straight through عبور می‌دهد و کل service با آن HTML تعامل دارد — yuck!

درزها یا مرزها باید با دقت بیشتری برخورد شوند. باید نقاط translation و validation برای اطلاعات باشند. نقطهٔ ورود به service باید سد دفاعی کوچکی باشد که بدترین سوءاستفاده‌های consumerهای آن service را محدود کند. آنچه توصیف می‌کنم مدل Ports & Adapters در سطح یک service منفرد است. این رویکرد باید به همان اندازه برای serviceای که با فراخوانی متد یا تابع استاندارد ارتباط برقرار می‌کند درست باشد که برای HTML، XML یا هر شکل messaging دیگر.

ایدهٔ پایه اینجا مدولاریته است! سیستم مدولار نیست اگر کارکرد داخلی ماژول‌های مجاور exposed باشد. ارتباط بین ماژول‌ها (و serviceها) باید کمی guardedتر از ارتباط درون آن‌ها باشد.

قابلیت استقرار و مدولاریته

در کتاب Continuous Delivery، Jez Humble و من راهی برای سازمان‌دهی کار توصیف کردیم که نرم‌افزارمان همیشه releasable باشد. توصیه (و همچنان توصیه) می‌کنیم طوری کار کنید که نرم‌افزار همیشه در حالت releasable باشد. بخشی از دستیابی به توانایی تکرارپذیر و قابل‌اتکا برای release، اطمینان از deployable بودن آسان و ساده است.

از نوشتن کتاب Continuous Delivery، اکنون عمیق‌تر باور دارم که کار طوری که نرم‌افزار هم testable و هم deployable باشد اثر عمیقی بر کیفیت کارمان دارد.

یکی از ایده‌های محوری کتاب قبلی‌ام ایدهٔ deployment pipeline است — مکانیزمی که commitها را از یک طرف می‌گیرد و «نتیجهٔ releasable» در طرف دیگر تولید می‌کند. این ایدهٔ کلیدی است. deployment pipeline فقط workflow کوچکی از گام‌های build یا test نیست؛ مسیر mechanized از commit تا production است.

این تفسیر پیامدهایی دارد. یعنی همهٔ چیزهایی که «releasability» را می‌سازند در دامنهٔ deployment pipeline شماست. اگر pipeline بگوید همه‌چیز خوب است، نباید کار بیشتری برای راحتی release لازم باشد — هیچ… بدون integration check، sign-off یا staging test بیشتر. اگر pipeline بگوید «good»، «good to go» است!

این بدور پیامدهایی برای دامنهٔ sensible یک deployment pipeline دارد. اگر خروجی آن «releasable» است، باید «independently deployable» هم باشد. دامنهٔ deployment pipeline مؤثر همیشه «واحد نرم‌افزاری independently deployable» است.

این بر مدولاریته اثر دارد. اگر خروجی deployment pipeline deployable باشد، یعنی pipeline ارزیابی definitive نرم‌افزار ماست — حداقل تا درجه‌ای که برای ما مهم است و آمادگی release را safe و sensible می‌دانیم.

اگر بخواهیم آن ایده را به نتیجهٔ منطقی‌اش برسانیم، فقط دو استراتژی منطقی است: همهٔ چیزهایی که سیستم ما را می‌سازند را با هم build، test و deploy کنیم، یا بخش‌هایی از آن سیستم را جدا build، test و deploy کنیم. راه میانه‌ای نیست. اگر به خروجی deployment pipeline به‌اندازهٔ کافی اعتماد نداریم و احساس می‌کنیم باید نتایجش را با خروجی pipelineهای دیگر آزمون کنیم، مشکل داریم؛ پیام‌هایی که deployment pipeline به ما می‌فرستد اکنون unclear است و چون می‌خواهیم مهندس باشیم، این کافی نیست!

اکنون دامنهٔ ارزیابی ما compromised است. کی تمام می‌شویم؟ وقتی pipeline ما تمام شد یا وقتی هر pipeline دیگری که برای verify کردن خروجی pipeline ما لازم است اجرا شد؟ اگر دومی، «cycle time»[^2] تغییرات ما cycle time تغییرات همهٔ دیگران را هم شامل می‌شود — پس سیستم ارزیابی monolithic داریم.

مقیاس‌پذیرترین رویکرد توسعهٔ نرم‌افزار توزیع آن است. coupling و وابستگی بین تیم‌ها و محصولاتشان را تا حدی کاهش دهید که هر تیم بتواند مستقل کارش را بدون ارجاع به تیم دیگر create، test و deploy کند. این رویکردی است که به Amazon — با «two-pizza teams»[^3] مشهورش — اجازه داد با نرخ بی‌سابقه رشد کند.

از نظر فنی، یک راه دستیابی به این independence این است که مدولاریته سیستم را آن‌قدر جدی بگیریم که هر ماژول از نظر build، test و deployment از هر ماژول دیگر مستقل باشد. microserviceها همین‌اند. آن‌قدر مدولارند که قبل از release نیازی به آزمون با serviceهای دیگر نداریم. اگر microserviceهایتان را با هم آزمون می‌کنید، واقعاً microservice نیستند. بخشی از تعریف microservice این است که «independently deployable» باشند.

قابلیت استقرار می‌تواند روی مدولاریته شرط بگذارد. همان‌طور که دیدیم، قابلیت استقرار دامنهٔ مؤثر deployment pipeline را تعریف می‌کند. انتخاب‌هایی که واقعاً کار می‌کنند — اگر کار با کیفیت بالا بر پایهٔ بازخورد سریع و کارآمد ارزش می‌نهیم — واقعاً محدودند.

می‌توانیم انتخاب کنیم همهٔ چیزهایی که سیستم را می‌سازند را با هم build، test و deploy کنیم و مشکلات dependency-management را کاملاً حذف کنیم (همه‌چیز در یک repository)، اما باید مسئولیت ایجاد بازخورد به‌اندازهٔ کافی سریع برای کار خوب توسعه‌دهندگان را بپذیریم — که ممکن است سرمایه‌گذاری مهندسی بزرگی برای بازخوردی که هر فرایند با کیفیت بالا را به‌اندازهٔ کافی سریع هدایت می‌کند لازم باشد.

یا می‌توانیم طوری کار کنیم که هر ماژول اساساً از هر ماژول دیگر مستقل باشد. هر کدام را جدا build، test و deploy کنیم، بدون نیاز به آزمون با هم. دامنهٔ build، test و deployment کوچک می‌شود. هر کدام ساده‌تر است، پس دستیابی به نتایج سریع و با کیفیت بالا آسان‌تر است.

با این حال این گاهی با هزینهٔ بسیار قابل‌توجه معماری پیچیده‌تر و توزیع‌شده‌تر در سیستم‌هایمان همراه است. اکنون مجبوریم مدولاریته را واقعاً جدی بگیریم.

باید برای آن طراحی کنیم؛ باید در تکنیک‌های protocol design ماهر باشیم تا تعامل بین ماژول‌ها — پروتکل تبادل اطلاعات — پایدار باشد و به‌شکلی تغییر نکند که تغییر را روی ماژول‌های دیگر تحمیل کند. احتمالاً باید ایده‌هایی مثل runtime version management برای APIها را در نظر بگیریم.

تقریباً همه می‌خواهند میانهٔ ایده‌آلی بین این دو extreme، اما در واقعیت وجود ندارد. میانه fudge است و اغلب از رویکرد monolithic که همه assiduously از آن اجتناب می‌کنند کندتر و پیچیده‌تر است. رویکرد سازمانی توزیع‌شده‌تر که microservice است بهترین راهی است که برای scale up توسعهٔ نرم‌افزار می‌شناسیم، اما رویکرد ساده‌ای نیست و هزینه دارد.

مدولاریته در مقیاس‌های مختلف

مدولاریته در هر مقیاسی مهم است. قابلیت استقرار ابزار مفیدی برای فکر کردن به ماژول‌های سطح سیستم است، اما به‌تنهایی برای ایجاد کد با کیفیت بالا کافی نیست. pre-occupation مدرن با serviceها وجود دارد.

این ابزار معماری مفیدی است و حداقل سه دههٔ گذشته در قلب رویکرد من به طراحی سیستم بوده. اما اگر مدولاریته طراحی شما آنجا متوقف شود، ممکن است هنوز سیستم‌های ضعیف و سخت‌کار داشته باشید.

اگر — همان‌طور که استدلال می‌کنم — اهمیت مدولاریته ابزاری برای مدیریت پیچیدگی است، باید آن را تا نقطهٔ کد خوانا ببریم. هر class، method یا function باید ساده و readable باشد و در صورت مناسب از submoduleهای کوچک‌تر و independently understandable compose شود.

باز هم TDD به تشویق چنین کد fine-grained کمک می‌کند. برای قابل‌آزمون بودن کد در این resolution، تکنیک‌هایی مثل dependency injection کد با surface area بزرگ‌تر را تشویق می‌کنند. این به‌شدت بر مدولاریته طراحی‌هایمان اثر می‌گذارد.

در مقیاس‌های کوچک‌تر، dependency injection مؤثرترین ابزار برای فشار روی کد است که سیستم‌های compose‌شده از قطعات کوچک زیاد بسازیم. وابستگی‌ها caliperها، نقاط اندازه‌گیری‌اند که می‌توانیم در سیستم inject کنیم تا نتیجهٔ thoroughly testable به دست آوریم. باز هم اطمینان از قابل‌آزمون بودن کد طراحی‌های genuinely modular و در نتیجه کد readableتر را تشویق می‌کند.

بعضی این سبک طراحی را criticise می‌کنند. criticise معمولاً این است که دنبال کردن flow of control در سیستم با surface area بزرگ‌تر این‌طور سخت‌تر است. این criticise نکته را از دست می‌دهد. اگر برای آزمون آن کد لازم باشد آن surface area exposed شود، پس همان surface area کد است. اگر با poor interface design و نبود آزمون obscure شود، درکش چقدر سخت‌تر است؟ ریشهٔ این criticise واقعاً دربارهٔ «طراحی خوب» است. پیشنهاد می‌کنم تمرکز طراحی روی مدیریت پیچیدگی benchmark ارزشمندی برای تعریف «کیفیت بالا» در کد باشد.

آزمون، وقتی خوب انجام شود، چیز مهمی و درست دربارهٔ ماهیت کد، طراحی‌ها و مسئله‌ای که حل می‌کنیم را expose می‌کند که در غیر این صورت به‌راحتی در دسترس نیست. در نتیجه یکی از مهم‌ترین ابزارهای arsenal ما برای ساخت سیستم‌ها و کد بهتر و مدولارتر است.

مدولاریته در سیستم‌های انسانی

تأثیر این تفکر مهندسی را در فصل ۱۵ به‌تفصیل بررسی می‌کنم، اما مفید است اهمیت خاص مدولاریته در این زمینه را برجسته کنم. بخش زیادی از حرفهٔ حرفه‌ای من صرف کار روی سیستم‌های کامپیوتری بزرگ شده. در این دنیا، refrain مداوم «چطور scale کنیم؟» بوده. گاهی — rarely — دربارهٔ نرم‌افزار است، اما بیشتر وقتی مردم در سازمان‌های بزرگ این سؤال را می‌پرسند، واقعاً منظورشان «چطور افراد بیشتری اضافه کنیم تا سریع‌تر نرم‌افزار تولید کنیم؟» است.

پاسخ واقعی این است که برای هر سیستم کامپیوتری given محدودیت‌های بسیار جدی وجود دارد. همان‌طور که Fred Brooks مشهور گفت:

نمی‌توانید با ۹ زن در یک ماه بچه بسازید.[^4]

با این حال گزینه‌های دیگری هست؛ می‌توانید با ۹ زن در ۹ ماه ۹ بچه بسازید که میانگین یک بچه در ماه می‌شود. در این نقطه analogy من — یا Fred — می‌شکند!

در اصطلاح نرم‌افزار، اگر نه با بچه‌ها، مسئله دربارهٔ coupling است. تا وقتی قطعات واقعاً از هم مستقل و genuinely decoupled باشند، می‌توانیم هر اندازه بخواهیم parallelize کنیم. به محض coupling، محدودیت روی درجهٔ parallelization وجود دارد. هزینهٔ integration قاتل است!

چطور کار جریان‌های توسعهٔ جدا را integrate کنیم؟ اگر به اندازهٔ من nerdy باشید، شاید زنگ بزند؛ این دربارهٔ چیزی عمیقاً بنیادی است. دربارهٔ information و concurrency است. وقتی جریان‌های مستقل information دارید، هزینهٔ کنار هم آوردنشان برای تشکیل کل coherent می‌تواند اگر overlap باشد فوق‌العاده بالا باشد. بهترین راه parallelize کردن این است که re-integrate لازم نباشد (نه بچه). اساساً این رویکرد microservice است. microserviceها play سازمانی scalability هستند؛ واقعاً مزیت دیگری ندارند — اما روشن باشیم، اگر scalability مسئلهٔ شماست این مزیت بزرگی است!

می‌دانیم فقط اضافه کردن افراد بیشتر به تیم، تیم را سریع‌تر نمی‌کند. در meta-study زیبایی از بیش از ۴۰۰۰ پروژهٔ نرم‌افزاری که عملکرد نسبی (زمان ساخت ۱۰۰٬۰۰۰ خط کد) در تیم‌های ۵ نفر یا کمتر و تیم‌های ۲۰ نفر یا بیشتر را مقایسه کرد، تیم‌های ۵ نفره فقط یک هفته بیشتر از تیم‌های ۲۰ نفره در ۹ ماه طول کشید. پس تیم‌های کوچک تقریباً ۴ برابر per person productiveترند.[^5]

اگر برای ساخت کار با کیفیت بالا به تیم‌های کوچک نیاز داریم، باید راه‌هایی برای seriously محدود کردن coupling بین آن تیم‌های کوچک پیدا کنیم. این حداقل به همان اندازه مسئلهٔ استراتژی سازمانی است که فنی. به سازمان‌های مدولار هم نیاز داریم، نه فقط نرم‌افزار مدولار.

پس اگر می‌خواهیم سازمان‌هایمان scale up کنند، راز ساخت تیم‌ها و سیستم‌هایی است که حداقل ممکن نیاز به هماهنگی داشته باشند — باید decouple شوند. تلاش سخت برای حفظ این مدولاریته سازمانی مهم است و یکی از نشانه‌های واقعی سازمان‌های genuinely high-performing و scalable.

خلاصه

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

مدولاریته به‌عنوان ایدهٔ طراحی فراکتالی است. فراتر از فقط «ماژول‌ها» — هر شکلی که زبان‌های برنامه‌نویسی پشتیبانی می‌کنند — است. پیچیده‌تر و مفیدتر از آن است. در قلبش ایده‌ای است که باید توانایی تغییر کد و سیستم‌ها در یک جا را حفظ کنیم، بدون نگرانی از اثر آن تغییرات در جای دیگر.

این ما را به فکر کردن دربارهٔ سایر جنبه‌های این مسئله وادار می‌کند، پس مدولاریته intimately با ایده‌های دیگری که برای مدیریت پیچیدگی سیستم‌هایمان باید در نظر بگیریم — انتزاع، تفکیک دغدغه‌ها، coupling و cohesion — مرتبط است.

[^1]: منبع: تعریف فرهنگ از Merriam-Webster. با اجازه استفاده شده. Wikipedia، https://en.wikipedia.org/wiki/Modularity.

[^2]: cycle time معیاری از کارایی فرایند توسعهٔ شماست. از idea تا useful software در دست کاربران چقدر طول می‌کشد؟ در continuous delivery، بهینه‌سازی cycle time را ابزاری برای هدایت به رویکردهای کارآمدتر استفاده می‌کنیم.

[^3]: Amazon مشهوراً پس از memo از CEO Jeff Bezos reorganize شد. Bezos در memo گفت: «…هیچ تیمی نباید بزرگ‌تر از آن باشد که با دو پیتزا سیر شود.»

[^4]: نقل‌قول از کتاب تأثیرگذار و هنوز درست Fred Brooks از دههٔ ۱۹۷۰، The Mythical Man Month.

[^5]: در مطالعه‌ای Quantitative Software Management (QSM) هم یافت که تیم‌های بزرگ‌تر ۵ برابر defect بیشتری در کد تولید کردند. https://bit.ly/3lI93oe