حالت تاریک
فصل ۱۳ — مدیریت اتصال
اتصال (coupling) یکی از مهمترین ایدههاست که باید هنگام فکر کردن به مدیریت پیچیدگی در نظر بگیریم.
اتصال بهعنوان «میزان وابستگی متقابل بین ماژولهای نرمافزار؛ معیاری از میزان نزدیکی ارتباط دو routine یا ماژول؛ قدرت روابط بین ماژولها» تعریف میشود.¹
اتصال بخش ضروری هر سیستمی است و در نرمافزار اغلب در بحث دربارهٔ آن سهلانگاری میکنیم. اغلب دربارهٔ ارزش سیستمهای loosely coupled صحبت میکنیم، اما بیایید روشن باشیم: اگر اجزای سیستم نرمافزاری شما کاملاً decoupled باشند، نمیتوانند با یکدیگر ارتباط برقرار کنند. این ممکن است مفید باشد یا نباشد.
اتصال چیزی نیست که همیشه بتوانیم یا باید بخواهیم آن را بهطور کامل حذف کنیم.
هزینهٔ اتصال
با این حال، اتصال همان چیزی است که مستقیماًترین تأثیر را بر توانایی ما برای ایجاد و تحویل نرمافزار بهصورت قابلاعتماد، قابل تکرار و پایدار دارد. مدیریت اتصال در سیستمهایمان — و در سازمانهایی که آنها را میسازند — در مرکز توانایی ما برای ساخت نرمافزار در هر مقیاس یا پیچیدگیای قرار دارد.
دلیل واقعی اینکه ویژگیهایی مثل modularity و cohesion و تکنیکهایی مثل abstraction و separation of concerns اهمیت دارند این است که به ما کمک میکنند اتصال در سیستمهایمان را کاهش دهیم. این کاهش تأثیر مستقیمی بر سرعت و کارایی پیشرفت ما و بر مقیاسپذیری و قابلیت اطمینان هم نرمافزار و هم سازمانهایمان دارد.
اگر مسائل و هزینههای اتصال را جدی نگیریم، در نرمافزار big balls of mud میسازیم و سازمانهایی ایجاد میکنیم که هر تغییری را در production غیرممکن مییابند. اتصال موضوع بزرگی است!
در فصل قبل، بررسی کردیم abstraction چگونه میتواند به شکستن برخی زنجیرهایی که حتی قطعات کوچک نرمافزار را به هم میبندد کمک کند. اگر تصمیم بگیریم abstract نکنیم، کد ما tightly coupled است و مجبوریم نگران تغییرات در یک بخش سیستم باشیم که رفتار کد در بخش دیگر را به خطر میاندازد.
اگر concerns پیچیدگی essential و accidental را از هم جدا نکنیم، کد tightly coupled است و حالا باید همزمان نگران ایدههای گاهی horribly complex مثل concurrency باشیم و هم مطمئن شویم موجودی حساب درست جمع میشود. این روش خوشایندی برای کار نیست!
این به معنای آن نیست که tight coupling بد است و loose coupling خوب؛ متأسفانه اینقدر ساده نیست.
بهطور کلی، با فاصلهٔ زیاد رایجترین اشتباه بزرگ توسعهدهندگان و تیمها در جهت overly tight coupling است. هزینههای «اتصال بیش از حد شل» وجود دارد، اما معمولاً بسیار کمتر از هزینههای «اتصال بیش از حد سخت» است. پس بهطور کلی باید loose coupling را بر tight coupling ترجیح دهیم، اما trade-offهایی که با این انتخاب میپردازیم را هم درک کنیم.
مقیاسبندی
شاید بزرگترین تأثیر تجاری اتصال بر توانایی ما برای scale up کردن توسعه باشد. شاید پیام هنوز به همه نرسیده باشد، اما مدتها پیش فهمیدیم که با پرتاب کردن افراد به مشکل، نرمافزار بهتر و سریعتر نمیسازیم. محدودیت نسبتاً جدی روی اندازهٔ تیم توسعهٔ نرمافزار وجود دارد؛ قبل از اینکه افزودن افراد بیشتر آن را کند کند (به فصل ۶ مراجعه کنید).
دلیلش اتصال است. اگر تیم شما و تیم من developmentally coupled باشیم، شاید بتوانیم releaseها را هماهنگ کنیم. میتوانیم تغییرات را track کنیم و هر بار که کدم را تغییر میدهم، به نحوی به شما اطلاع داده شود. این برای تعداد بسیار کمی از افراد و تیمها ممکن است کار کند، اما بهسرعت از کنترل خارج میشود. overhead نگه داشتن همه در یک قدم بهسرعت از کنترل خارج میشود.
راههایی برای minimize کردن این overhead و کارآمدترین حالت ممکن هماهنگی وجود دارد. بهترین راه continuous integration است. همهٔ کد را در فضای مشترک — repository — نگه میداریم و هر بار هر کدام چیزی تغییر میدهد، بررسی میکنیم همهچیز هنوز کار میکند. این برای هر گروهی که با هم کار میکند مهم است؛ حتی گروههای کوچک از وضوحی که CI میآورد سود میبرند.
این رویکرد هم بسیار بهتر از آنچه تقریباً همه انتظار دارند scale میشود. مثلاً Google و Facebook این کار را برای تقریباً تمام کدشان انجام میدهند. downside scale up به این شکل این است که باید سنگین در engineering اطراف repositoryها، buildها، CI و automated testing سرمایهگذاری کنید تا feedback روی تغییرات بهاندازهٔ کافی سریع باشد تا فعالیتهای توسعه را هدایت کند. بیشتر سازمانها نمیتوانند یا نمیخواهند بهاندازهٔ کافی در تغییرات لازم سرمایهگذاری کنند.²
میتوانید این استراتژی را مدیریت علائم اتصال بدانید. feedback را آنقدر سریع و کارآمد میکنیم که حتی وقتی کد و تیمهایمان coupled هستند، بتوانیم کارآمد پیش برویم.
Microservices
استراتژی دیگر که منطقی است decouple کردن یا حداقل کاهش سطح اتصال است. این رویکرد microservices است. Microservices مقیاسپذیرترین راه ساخت نرمافزارند، اما آنچه بیشتر مردم فکر میکنند نیستند. رویکرد microservice بهمراتب پیچیدهتر از آنچه به نظر میرسد است و برای دستیابی به آن به درجهٔ قابلتوجهی sophistication در طراحی نیاز دارد.
همانطور که از این کتاب برداشت کردهاید، به مدل service برای سازماندهی سیستمهایمان باور دارم. ابزار مؤثری برای کشیدن خط دور ماژولها و concrete کردن seams abstraction است که در فصل قبل بحث شد. مهم است بشناسیم این مزایا مستقل از نحوهٔ deploy نرمافزار درست هستند و دهها سال قبل از ایدهٔ microservices وجود داشتند.
اصطلاح microservices نخست در ۲۰۱۱ به کار رفت. در microservices چیز جدیدی نبود. همهٔ practices و رویکردها قبلاً استفاده شده بودند و اغلب بهطور گسترده، اما رویکرد microservice آنها را کنار هم گذاشت و مجموعهای از این ایدهها را برای تعریف microservice به کار برد.
چند تعریف مختلف وجود دارد، اما این فهرستی است که من استفاده میکنم:
Microservices:
- کوچک
- متمرکز بر یک task
- همراستا با bounded context
- autonomous
- independently deployable
- loosely coupled
مطمئناً میبینید این تعریف نزدیک به نحوهٔ توصیف طراحی خوب نرمافزار است.
سختترین ایده اینجا «independently deployable» بودن سرویسهاست. اجزای independently deployable نرمافزار مدتها در contextهای مختلف وجود داشتند، اما حالا بخشی از تعریف یک architectural style و محور اصلی آن هستند.
این ویژگی تعیینکنندهٔ کلیدی microservices است؛ بدون این ایده، چیز جدیدی معرفی نمیکنند.
سیستمهای service-based از حداقل اوایل ۱۹۹۰ semantic messaging استفاده میکردند و همهٔ ویژگیهای رایج دیگر microservices هم توسط تیمهایی که سیستمهای service-based میساختند fairly common بود. ارزش واقعی microservices این است که میتوانیم آنها را مستقل از سرویسهای دیگری که کنارشان اجرا میشوند — و حتی سرویسهایی که با آنها interact میکنند — build، test و deploy کنیم.
لحظهای فکر کنید این چه معنایی دارد. اگر بتوانیم سرویسی را deploy کنیم مستقل از سرویسهای دیگر، یعنی version آن سرویسهای دیگر برایمان مهم نیست. یعنی قبل از release نمیتوانیم سرویسمان را با آنها test کنیم. این توانایی آزادی میدهد روی ماژول سادهٔ جلویمان — سرویس خودمان — تمرکز کنیم.
سرویس باید cohesive باشد تا به سرویسها یا کدهای دیگر وابسته نباشد. باید نسبت به سرویسهای دیگر very loosely coupled باشد تا آن یا آنها بدون شکستن یکدیگر تغییر کنند. وگرنه نمیتوانیم سرویس را بدون test با سرویسهای دیگر قبل از release deploy کنیم؛ پس independently deployable نیست.
این independence و پیامدهایش اغلب توسط تیمهایی که فکر میکنند microservice پیاده میکنند از دست میرود، اما بهاندازهٔ کافی decouple نکردهاند تا به deploy سرویس بدون test با سرویسهای همکار اعتماد کنند.
Microservices یک organizational-scaling pattern است. این مزیت آن است. اگر نیازی به scale up توسعه در سازمان ندارید، microservices لازم نیست (هرچند «services» ممکن است ایدهٔ عالی باشد).
Microservices با decouple کردن سرویسها از یکدیگر — و vitally decouple کردن تیمهایی که آن سرویسها را تولید میکنند — به scale کردن تابع توسعه کمک میکنند.³
حالا تیم شما با سرعت خودش — مستقل از سرعت تیم من — پیش میرود. version سرویس من برایتان مهم نیست چون سرویس شما sufficiently loosely coupled است.
هزینهای برای این decoupling وجود دارد. سرویس باید در برابر تغییر با collaborators انعطافپذیرتر طراحی شود. باید design strategies اتخاذ کنیم که سرویس را از تغییر در جاهای دیگر عایق کند. باید developmental coupling را بشکنیم تا مستقل کار کنیم. این هزینه دلیل است که microservice شاید انتخاب اشتباه باشد اگر نیازی به scale up تیم ندارید.
Independent deployability مثل همهچیز هزینه دارد. هزینه این است که سرویس را better abstracted، better insulated و loosely coupledتر در تعامل با سرویسهای دیگر طراحی کنیم. تکنیکهای مختلفی برای این کار وجود دارد، اما همه پیچیدگی سرویس و مقیاس challenge طراحی را افزایش میدهند.
Decoupling ممکن است به معنای کد بیشتر باشد
بیایید برخی از این هزینهها را باز کنیم. همانطور که همیشه، برای تصمیمهایی که میگیریم هزینهای میپردازیم. ماهیت engineering همیشه بازی trade-offهاست. اگر code را decouple کنیم، تقریباً قطعاً — حداقل در ابتدا — کد بیشتری مینویسیم.
این یکی از اشتباهات طراحی رایج برنامهنویسان است. فرض «کد کمتر خوب است» و «کد بیشتر بد» همیشه درست نیست، و اینجا نقطهٔ کلیدی است که قطعاً درست نیست. بیایید دوباره مثال سادهٔ فصلهای قبل را ببینیم. Listing 13.1 دوباره کد افزودن item را نشان میدهد.
Listing 13.1 یک مثال Cohesion (باز هم)
python
def add_to_cart1(self, item):
self.cart.add(item)
conn = sqlite3.connect('my_db.sqlite')
cur = conn.cursor()
cur.execute('INSERT INTO cart (name, price) values (item.name, item.price)')
conn.commit()
conn.close()
return self.calculate_cart_total();اینجا هشت خط کد داریم، اگر خطوط خالی را نادیده بگیریم. اگر با abstract کردن method کد را بهتر کنیم — امیدوارم همه موافق باشیم بهتر است — باید چند خط بیشتر اضافه کنیم. در Listing 13.2، کاهش coupling، بهبود cohesion و separation of concerns بهتر، دو خط اضافی هزینه داشته. اگر قدم بعد — معرفی module یا class جدیدی که بهعنوان parameter پاس میدهیم — را برداریم، چند خط دیگر برای بهبود طراحی اضافه میکنیم.
Listing 13.2 کاهش Coupling
python
def add_to_cart1(self, item):
self.cart.add(item)
self.store_item(item)
return self.calculate_cart_total();
def store_item(self, item):
conn = sqlite3.connect('my_db.sqlite')
cur = conn.cursor()
cur.execute('INSERT INTO cart (name, price) values (item.name, item.price)')
conn.commit()
conn.close()برنامهنویسانی را شنیدهام که رویکرد طراحی این کتاب را رد میکنند و دیگرانی که automated testing را رد میکنند چون «باید بیشتر تایپ کنم». این برنامهنویسان برای چیزهای اشتباه optimize میکنند.
کد وسیلهٔ ارتباط است و عمدتاً وسیلهٔ ارتباط با انسانهای دیگر است، نه کامپیوترها.
هدف ما آسانتر کردن زندگی خودمان و انسانهایی است که با کدمان interact میکنند. یعنی readability ویژگی effete و abstract نیست که فقط برای کسانی که دربارهٔ style و aesthetics nerd out میکنند معنا دارد. Readability ویژگی بنیادی کد خوب است و تأثیر اقتصادی مستقیم بر ارزش آن دارد.
پس مراقبت از understandable بودن کد و سیستمها مهم است. بیش از آن، ایدهٔ ارزیابی efficiency با شمارش کاراکترهایی که تایپ میکنیم مضحک است. کد unstructured و coupled در Listing 13.1 شاید خطوط کمتری داشته باشد — هشت خط. اما اگر function ۸۰۰ خط بود، احتمال duplication و redundancy بیشتر است. مدیریت complexity کد به دلایل زیادی مهم است؛ یکی کمک به spotting و حذف redundancy و duplication است.
در سیستمهای واقعی، با فکر کردن، طراحی خوب و ارتباط واضح از طریق کد به کد کمتر میرسیم، نه با شمارش کاراکترهای تایپشده.
باید برای thinking optimize کنیم، نه typing!
Loose coupling تنها نوعی نیست که اهمیت دارد
Michael Nygard⁴ مدل عالی برای توصیف coupling دارد. آن را به دستههایی تقسیم میکند (جدول ۱۳.۱ را ببینید).
جدول ۱۳.۱ مدل Nygard از Coupling
| نوع | اثر |
|---|---|
| Operational | consumer بدون provider نمیتواند اجرا شود |
| Developmental | تغییرات در producerها و consumerها باید هماهنگ شوند |
| Semantic | بهخاطر مفاهیم مشترک با هم تغییر میکنند |
| Functional | بهخاطر مسئولیت مشترک با هم تغییر میکنند |
| Incidental | بدون دلیل خوب با هم تغییر میکنند (مثلاً breaking API changes) |
مدل مفیدی است و طراحی سیستمهایمان بر همهٔ این انواع coupling تأثیر دارد.
اگر نتوانید تغییرات را در production release کنید مگر اینکه من کارم را تمام کرده باشم، developmentally coupled هستیم. میتوانیم با انتخابهای طراحی این coupling را address کنیم.
اگر سرویس من نتواند start شود مگر سرویس شما در حال اجرا باشد، سرویسها operationally coupled هستند و باز هم میتوانیم از طریق طراحی address کنیم.
شناخت این انواع coupling قدم خوبی است. فکر کردن به آنها و تصمیم اینکه کدام را و چگونه address کنیم، قدم بعدی است.
ترجیح Loose Coupling
همانطور که دیدیم، loose coupling هزینه دارد و هزینهٔ خطوط کد بیشتر میتواند هزینهٔ performance هم باشد.
Coupling میتواند بیش از حد شل باشد
سالها پیش برای یک شرکت مالی بزرگ مشاوره کردم. مشکل performance جدی در سیستم مدیریت سفارش مهم داشتند. آمدم ببینم آیا میتوانم performance را بهبود دهم.
معمار مسئول طراحی بسیار افتخار میکرد که «best practice» را دنبال کردهاند. تفسیرش از best practice کاهش coupling و افزایش abstraction بود — هر دو در نظر من خوب — اما یکی از راههایی که تیم این کار را کرده بود، schema کاملاً abstract برای relational database بود. تیم افتخار میکرد میتواند «هر چیزی» در database ذخیره کند.
در essence، «name-value pair» store مخلوط با «star schema» سفارشی ساخته بودند که relational database را بهعنوان store استفاده میکرد. بیش از آن، هر element در «record» از نظر application، record جداگانهای در database بود، همراه linkهایی برای retrieve کردن sibling recordها. یعنی highly recursive بود.
کد very general و very abstract بود، اما برای load کردن تقریباً هر چیزی، صدها و گاهی هزاران interaction با database لازم بود قبل از operate کردن روی داده.
Abstraction و decoupling بیش از حد میتواند مضر باشد!
پس مهم است از این هزینههای بالقوه آگاه باشیم و abstraction و decoupling را بیش از حد نبریم، اما همانطور که گفتم، شکست vastly رایجتر inverse است. Big balls of mud بسیار رایجتر از طراحیهای overly abstract و overly decoupled هستند.
بخش پایانی careerام را در سیستمهای very high-performance کار کردم، پس performance در طراحی را جدی میگیرم. با این حال، فرض رایج که کد high-performance messy است و نمیتواند function یا method call زیاد تحمل کند، thinking قدیمی است و باید رد شود.
مسیر high performance کد simple و efficient است و امروز برای بیشتر زبانها و platformهای رایج، کد simple و efficient که بهراحتی و حتی بهتر predictably توسط compilerها و hardware درک شود. Performance بهانهٔ big ball of mud نیست!
با این حال، میتوانم بپذیرم در بلوکهای high-performance، کمی با سطح decoupling محتاط باشیم.
ترفند این است که seams abstraction را طوری بکشیم که بخشهای high-performance سیستم در یک طرف خط باشند تا cohesive باشند، با پذیرش اینکه transition از یک service یا module به دیگری هزینهٔ اضافی دارد.
این interfaceها بین serviceها loose coupling را ترجیح میدهند تا هر service جزئیات را از دیگری پنهان کند. این interfaceها نقاط مهمتری در طراحی سیستم هستند و باید با مراقبت بیشتر و با هزینهٔ کمی بالاتر از نظر runtime overhead و خطوط کد treat شوند. این trade-off قابل قبول و قدم ارزشمند به سمت سیستمهای modularتر و flexibleتر است.
این چگونه با Separation of Concerns تفاوت دارد؟
ممکن است loose coupling و separation of concerns ایدههای مشابه به نظر برسند و قطعاً مرتبطاند. اما perfectly reasonable است دو قطعه کد tightly coupled اما با separation of concerns بسیار خوب داشته باشیم، یا loosely coupled با separation of concerns ضعیف.
اولی آسان است تصور کنیم. سرویسی که order پردازش میکند و سرویسی که order ذخیره میکند. separation of concerns خوب است، اما اطلاعاتی که بین آنها میفرستیم detailed و precise ممکن است باشد. ممکن است هر دو service با هم تغییر کنند. اگر یک service مفهوم «order» را تغییر دهد، دیگری را بشکند؛ پس tightly coupled هستند.
دومی — loosely coupled با separation of concerns ضعیف — در سیستم واقعی کمی سختتر است تصور کرد، اما در abstract آسان است.
دو service که دو account جدا مدیریت میکنند و یکی پول میفرستد تا دیگری credit شود. دو account اطلاعات را asynchronously، via message، exchange میکنند.
Account A پیام «Account A Debited by X, Credit Account B» میفرستد. بعداً Account B پیام را میبیند و خودش را credit میکند. transaction بین دو service مجزا تقسیم شده. رفتار مورد نظر انتقال پول از یک account به دیگر است، اما cohesive نیست: در یک جا کم میکنیم و در جای دیگر اضافه میکنیم، در حالی که باید حس «transaction» کلی وجود داشته باشد.
اگر همانطور که توصیف کردم پیاده کنیم، ایدهٔ very bad است. overly simplistic و doomed to failure. اگر مشکل transmission باشد، پول میتواند ناپدید شود.
قطعاً باید بیشتر از آن کار کنیم — protocolی که دو طرف transaction در sync باشند. سپس تأیید کنیم اگر پول از account اول حذف شد، حتماً به دوم رسید. اما هنوز میتوانیم loosely coupled — technically اگر نه semantically — این کار را انجام دهیم.
DRY بیش از حد ساده است
DRY مخفف «Don't Repeat Yourself» است. shorthand برای تمایل به داشتن یک canonical representation از هر piece of behavior در سیستم. توصیهٔ خوبی است، اما همیشه خوب نیست. همانطور که همیشه، پیچیدهتر است.
DRY در context یک function، service یا module عالی است. فراتر از آن، DRY را تا scope version control repository یا deployment pipeline گسترش میدهم. اما هزینه دارد — گاهی very significant — بهویژه بین serviceها یا moduleهایی که independently توسعه مییابند.
مشکل این است که هزینهٔ داشتن یک canonical representation از هر idea در کل سیستم coupling را افزایش میدهد و هزینهٔ coupling میتواند از هزینهٔ duplication بیشتر شود. این balancing act است.
Dependency management شکل insidious developmental coupling است. اگر service شما و service من library مشترک استفاده کنند و شما مجبور باشید service را update کنید وقتی من mine را update میکنم، serviceها و تیمها developmentally coupled هستند.
این coupling تأثیر profound بر توانایی autonomous کار کردن و پیشرفت روی چیزهایی که برایمان مهم است دارد. ممکن است مشکل باشد release را hold کنید تا version جدید library که تیم من تحمیل کرد consume کنید. یا در وسط کار دیگری باشید که این تغییر سختترش میکند.
مزیت DRY: وقتی چیزی تغییر میکند، فقط در یک جا تغییر میدهیم. disadvantage: هر جایی که از آن کد استفاده میکند به نوعی coupled است.
از نظر engineering، ابزارهایی داریم. مهمترینش deployment pipeline است.
در continuous delivery، deployment pipeline باید feedback واضح و definitive روی releasability سیستمها بدهد. اگر pipeline بگوید «همهچیز خوب است»، بدون کار بیشتر safe to release هستیم. این چیز مهمی دربارهٔ scope deployment pipeline میگوید: باید «independently deployable unit of software» باشد.
پس اگر pipeline بگوید همه خوب است، میتوانیم release کنیم؛ scope منطقی برای DRY. DRY باید principle راهنما در scope deployment pipeline باشد، اما actively بین pipelineها avoid شود.
پس اگر سیستم microservice-based میسازید، هر service independently deployable و هر service deployment pipeline خودش را دارد، DRY را بین microserviceها apply نکنید. کد را بین microserviceها share نکنید.
این interesting و sort of foundational برای thinking است که مرا به نوشتن این کتاب واداشت. advice من دربارهٔ coupling تصادفی یا accident به چیزی که distant به نظر میرسد — deployment pipeline — مرتبط نیست.
این خط reasoning از ایدهٔ fairly basic در computer science — coupling — از طراحی و architecture به چیزی که seemingly مربوط به build و test نرمافزار است میرسد.
این بخشی از engineering philosophy و رویکردی است که اینجا describe و promote میکنم.
اگر خط reasoning را دنبال کنیم — از اهمیت feedback عالی، ایجاد رویکردهای efficient و effective برای learning، تقسیم کار به بخشهایی که complexity سیستمها و human systemهایی که آنها را میسازند را مدیریت کنیم — به اینجا میرسیم.
با کار کردن طوری که نرمافزار همیشه releasable باشد — core tenet continuous delivery — مجبوریم deployability و scope deployment pipelineها را در نظر بگیریم. با optimize کردن برای learn سریع و fail fast — هدف بخش اول کتاب — مجبوریم testability را address کنیم. این ما را به کد modularتر، cohesiveتر، با separation of concerns بهتر و خطوط abstraction بهتر که change را isolated و loosely coupled نگه میدارد راهنمایی میکند.
همهٔ این ایدهها linked هستند. همه یکدیگر را reinforce میکنند و اگر جدی بگیریم و foundation کارمان کنیم، نرمافزار بهتر و سریعتر میسازیم.
هر چه software engineering باشد، اگر به ساخت نرمافزار بهتر و سریعتر کمک نکند، «engineering» محسوب نمیشود.
Async بهعنوان ابزار Loose Coupling
فصل قبل دربارهٔ leakiness abstractionها بحث کرد. یکی از آن leaky abstractionها ایدهٔ synchronous computing در فراسوی process boundary است.
بهمحض establish کردن چنین boundary، هر ایدهٔ synchrony illusion است و این illusion هزینه دارد.
Leakiness این abstraction در distributed computing dramaticترین است. اگر service A با service B communicate کند، همهٔ جاهایی که communication میتواند fail شود اگر network جدا کند را در نظر بگیرید.
Illusion synchrony میتواند وجود داشته باشد، اما تا وقتی یکی از failureها رخ دهد — و رخ خواهد داد. شکل ۱۳.۱ جاهایی که conversation توزیعشده میتواند اشتباه شود را نشان میدهد.
1 2 3 4
'A' 5 'B'
9 8 7 6شکل ۱۳.۱ — نقاط شکست در ارتباطات synchronous
- ممکن است bug در A باشد.
- A ممکن است connection به network برقرار نکند.
- پیام در transmission گم شود.
- B ممکن است connection به network برقرار نکند.
- ممکن است bug در B باشد.
- connection قبل از ارسال response از B fail شود.
- response در transmission گم شود.
- A قبل از دریافت response connection را از دست بدهد.
- ممکن است bug در handling response توسط A باشد.
بهجز ۱ و ۹، هر نقطهٔ failure leak در abstraction ارتباطات synchronous است و complexity مدیریت خطا را افزایش میدهد. تقریباً همهٔ این خطاها میتوانند A و B را out of step کنند. فقط برخی توسط sender A detectable هستند.
حالا تصور کنید A و B دربارهٔ business-level behavior صحبت میکنند گویی conversation synchronous است. وقتی connection problem یا dropped message رخ دهد، این technical failure وارد conversation business-level میشود.
این leak با نمایندگی نزدیکتر از آنچه واقعاً رخ میدهد significantly mitigate میشود. Networkها واقعاً asynchronous هستند؛ communication در دنیای واقعی asynchronous است.
وقتی با شما صحبت میکنم، مغزم بعد از پرسیدن سؤال freeze نمیکند؛ کارهای دیگر میکند. Abstraction بهتر، نزدیکتر به واقعیت، کمتر unpleasant leak میکند.
اینجا جای detail زیاد رویکردهای طراحی خاص نیست، اما به treat کردن process boundaryها بهعنوان asynchronous و communicate کردن بین distributed serviceها و moduleها فقط via asynchronous event باور دارم. برای distributed systemهای complex، این رویکرد تأثیر abstraction leak را significantly کاهش میدهد و coupling به accidental complexity زیرین را کم میکند.
لحظهای تأثیر reliable asynchronous messaging system روی فهرست failure points در شکل ۱۳.۱ را تصور کنید. همهٔ همان failureها میتوانند رخ دهند، اما اگر Service A فقط async message بفرستد و بعداً فقط async message جدید دریافت کند، Service A دیگر نگران هیچکدام بعد از step ۲ نیست. اگر meteorite به data center Service B خورده، data center را rebuild، Service B را redeploy و message اصلی Service A را resubmit میکنیم. اگرچه دیر، همهٔ processing دقیقاً مثل conversation چند microsecond ادامه مییابد.
این فصل دربارهٔ coupling است، نه asynchronous programming یا design. intent من متقاعد کردن به merits asynchronous programming نیست — هرچند many دارد — بلکه استفاده از آن بهعنوان مثال برای نشان دادن smart use ایدههای کاهش coupling، در این مورد بین accidental complexity network و remote comms و essential complexity business function سرویسهایم، تا یک piece of code بنویسم که هم وقتی سیستم خوب کار میکند و هم وقتی نمیکند کار کند. این پاسخ well-engineered به class خاصی از problem است.
طراحی برای Loose Coupling
باز هم، striving برای testable code فشار مفیدی روی design میآورد که — اگر توجه کنیم — به سیستمهای loosely coupledتر هدایت میکند. اگر کد hard to test باشد، معمولاً نتیجهٔ unfortunate degree of coupling است.
پس میتوانیم به feedback design react کنیم و آن را تغییر دهیم تا coupling کم شود، testing آسانتر و design با کیفیت بالاتر شود. این ability برای amplify کردن quality کد و design حداقلی است که از genuine engineering approach برای software انتظار دارم.
Loose Coupling در Human Systemها
بهتدریج فکر میکنم coupling بهطور کلی در heart توسعهٔ نرمافزار است. چیزی است که نرمافزار را difficult میکند.
بیشتر مردم میتوانند در چند ساعت برنامهٔ ساده بنویسند. انسانها در زبانها — حتی چیزهای weird و grammatically constrained مثل programming language — بسیار خوباند. مشکل این نیست. در واقع، ease یادگیری چند concept برای نوشتن چند خط کد مشکل دیگری است: sufficiently simple است که false sense of capability ایجاد کند.
Professional programming دربارهٔ translate کردن instruction از human language به programming language نیست. ماشینها میتوانند.⁵ Professional programming دربارهٔ ایجاد solution برای problem است و code ابزاری است که solution را capture میکند.
چیزهای زیادی برای یادگیری هنگام coding وجود دارد، اما میتوانید سریع شروع کنید و روی problemهای آسان بهتنهایی پیشرفت خوب داشته باشید. بخش سخت وقتی است که سیستمها و تیمهایی که با آنها میسازیم در size و complexity رشد میکنند. آنوقت coupling اثرش را میگذارد.
همانطور که hint دادم، فقط دربارهٔ code نیست؛ vitally دربارهٔ coupling در سازمانهایی است که آن را میسازند. Developmental coupling مشکل common و expensive در سازمانهای بزرگ است.
اگر با integrate کردن کارمان حل کنیم، هر طور که deal کنیم، integration هزینه دارد. کتاب دیگرم Continuous Delivery fundamentally دربارهٔ strategyهای مدیریت efficient این coupling است.
در professional life، سازمانهای بزرگ زیادی را میبینم که organizational coupling آنها را hamstrung کرده. release هر تغییر در production تقریباً impossible است، چون سالها هزینههای coupling را ignore کردهاند و کوچکترین تغییر نیاز به coordinate کردن دهها یا صدها نفر دارد.
فقط دو strategy منطقی است: coordinated approach یا distributed approach. هر کدام هزینه و benefit دارد. این بخشی از nature engineering به نظر میرسد.
هر دو — importantly — deeply تحت تأثیر efficiency جمعآوری feedback هستند؛ به همین دلیل continuous delivery concept مهمی است. CD بر optimize کردن feedback loopها در development built است تا در essence continuous feedback روی quality کار داشته باشیم.
اگر consistency در software بزرگ و complex میخواهید، coordinated approach را adopt کنید: همهچیز را با هم store، build، test و deploy کنید.
واضحترین و accurate picture را میدهد، اما هزینهٔ توانایی انجام همهٔ اینها quickly و efficiently را دارد. معمولاً توصیه میکنم multiple times per day به این feedback برسید. این میتواند investment significant در time، effort و technology برای feedback بهاندازهٔ کافی سریع باشد.
این مانع multiple team روی سیستم نمیشود، و implied نمیکند سیستمهایی که تیمها اینطور میسازند tightly coupled هستند. اینجا دربارهٔ scope evaluation برای production release صحبت میکنیم — در این مورد کل سیستم.
وقتی تیمهای جدا semi-independently کار میکنند، از طریق shared codebase و continuous delivery deployment pipeline برای کل سیستم coordinate میکنند.
این رویکرد به تیمها اجازه میدهد روی code، service یا moduleهای tightly coupledتر با minimum cost از نظر feedback پیشرفت کنند، اما — repeat — باید سخت کار کنید تا بهاندازهٔ کافی fast باشد.
Distributed approach فعلاً more in favor است: microservices approach. در microservice organizationها، decision-making intentionally distributed است. تیمهای microservice مستقل کار میکنند، هر service independently deployable است و direct coordination cost بین تیمها نیست. اما indirect cost وجود دارد — در design.
برای کاهش organizational coupling، مهم است نیاز به test کردن serviceها با هم later in process را avoid کنیم. اگر independently deployable باشند، independently test هم میشوند — چگونه deployability را بدون test judge کنیم؟ اگر دو service را با هم test کنیم و version ۴ یکی با version ۶ دیگری کار کند، واقعاً version ۴ و version ۱۷ را بدون test release میکنیم؟ پس independent نیستند.
Microservice approach scalableترین strategy برای توسعهٔ نرمافزار است. هر تعداد team که بخواهید — یا حداقل هر تعداد که people و funds داشته باشید.
هزینه: coordination را give up میکنید — یا حداقل به سادهترین و genericترین terms کاهش میدهید. centralized guidance میتوانید offer کنید، اما enforce نمیتوانید چون enforcement coordination cost دارد.
سازمانهایی که microservices را seriously میگیرند consciously control را loosen میکنند؛ microservices approach بدون این loosening little or no sense دارد.
هر دو رویکرد — تنها دو رویکردی که real sense دارند — دربارهٔ strategyهای مختلف مدیریت coupling بین تیمهاست. coupling را با speed up کردن frequency check برای mistake وقتی coupling high است مدیریت میکنید، یا وقتی coupling low است prior to release اصلاً check نمیکنید.
به هر حال هزینه دارد، اما middle ground real نیست — هرچند many organization اشتباه سعی میکنند یکی بسازند.
خلاصه
Coupling monster در heart توسعهٔ نرمافزار است. وقتی complexity فراتر از trivial برود، coupling درست — یا حداقل manage کردن هر سطح coupling که design کردهاید — اغلب تفاوت success و failure است.
اگر تیم شما و تیم من بدون coordinate کردن activities پیشرفت کنیم، گزارشهای «State of DevOps» میگویند احتمال بیشتری دارد code با کیفیت بالا و regular supply کنیم.
سه راه داریم: با code و systemهای more coupled کار کنیم اما با CI و CD feedback بهاندازهٔ کافی fast بگیریم. systemهای more decoupled طراحی کنیم که با confidence تغییر دهیم بدون force کردن change روی دیگران. یا با interfaceهای agreed و fixed کار کنیم که هرگز تغییر نکنند. واقعاً فقط این strategyها در دسترساند.
هزینههای coupling — در software و organization — را ignore کنید به خطر خودتان.
¹ منبع: Wikipedia، https://en.wikipedia.org/wiki/Coupling_(computer_programming)
² کتاب دیگرم Continuous Delivery practices لازم برای scale up این جنبههای software engineering را describe میکند. https://amzn.to/2WxRYmx
³ در ۱۹۶۷، Mervin Conway چیزی به نام Conway's law ایجاد کرد: «Any organization that designs a system (defined broadly) will produce a design whose structure is a copy of the organization's communication structure.»
⁴ Michael Nygard software architect و نویسندهٔ Release It است. مدل coupling را در چند conference ارائه داد: https://bit.ly/3j2dGIP
⁵ GPT3 machine learning system است که روی اینترنت train شده. با instruction انگلیسی میتواند appهای ساده code کند. https://bit.ly/3ugOpzQ