معماری شش ضلعی (Hexagonal Architecture)
هدف معماری لایهای سنتی، تفکیک یک اپلیکیشن به لایههای مختلف است، به گونهای که هر لایه شامل ماژولها و کلاسهایی باشد که مسئولیت های مشترک یا مشابهی دارند و برای انجام وظایف خاص با هم کار میکنند.
انواع مختلفی از معماری های لایه ای موجود است و هیچ قانونی وجود ندارد که بخواهد تعیین کند چه تعداد لایه باید وجود داشته باشد. رایج ترین الگو، معماری 3 لایه است که در آن اپلیکیشن به لایههای ارائه (Presentation Layer)، لایه منطقی (Logic Layer) و لایه داده (Data Layer) تقسیم می شود.
در کتاب Domain-Driven Design، Eric Evans برای مقابله با پیچیدگی در قلب نرمافزار، یک معماری 4 لایه را پیشنهاد میکند تا بین لایه دامنه که منطق کسبوکار را نگه میدارد، و 3 لایه پشتیبان دیگر، امکان جداسازی ایجاد شود: رابط کاربری (User Interface)، برنامه کاربردی (Application) و زیرساخت (Infrastructure).
پیروی از معماری لایهای از بسیاری جهات سودمند است، یکی از مهمترین این جهات، تفکیک دغدغه ها (separation of concerns) است. اما همیشه یک ریسک وجود دارد. از آنجایی که هیچ مکانیسم طبیعی برای تشخیص ریزش منطق بین لایه ها وجود ندارد، ممکن است - و احتمالاً - با ریزش منطق کسب و کار در رابط کاربری یا دغدغه های زیرساخت که در منطق کسب و کار تلفیق شدهاند، مواجه شویم.
در سال 2005، Alistair Cockburn متوجه شد که تفاوت زیادی بین نحوه تعامل رابط کاربری یا پایگاه داده با یک برنامه وجود ندارد. چرا که آنها هر دو کنشگر خارجی (external actors) هستند که با اجزای مشابه قابل تعویض می باشند و از طریق روش هایی معادل با یک برنامه کاربردی تعامل برقرار می کنند. با مشاهده به این شیوه، می توان بر روی ناشناس نگه داشتن برنامه کاربردی این کنشگرهای خارجی تمرکز داشت و به آنها اجازه داد که از طریق پورت ها و آداپتورها تعامل داشته باشند. در نتیجه از درهم تنیدگی و ریزش منطق بین منطق کسب و کار و مولفه های خارجی جلوگیری می شود.
در این مقاله تلاش شده که روی مفاهیم اصلی معماری Hexagonal، مزایا و هشدارهای مربوط به آن صحبت شود تا به شکلی ساده نحوه ی بهره بردن از این الگو در پروژهها درک گردد.
معماری Hexagonal که از آن تحت عنوان پورت ها و آداپتورها نیز یاد می شود، الگوی معماری است که به کاربران یا سیستم های خارجی اجازه می دهد تا وارد برنامه در یک پورت از طریق یک آداپتور شوند. این معماری اجازه می دهد تا خروجی از برنامه به وسیله ی پورت به آداپتور ارسال گردد. این نوع معماری یک لایه انتزاعی ایجاد می کند که از هسته برنامه محافظت کرده و آن را از ابزارها و تکنولوژی های خارجی - و به نوعی نامربوط - جدا کند.
پورت ها (Ports)
میتوانیم یک پورت را به عنوان یک نقطه ورودی تکنولوژی-آگنوستیک ببینیم، که رابطی را که به کنش گرهای خارجی اجازه میدهد با برنامه، صرف نظر از اینکه چه کسی یا چه چیزی رابط مذکور را پیادهسازی کرده، ارتباط برقرار کنند را تعیین نماید. درست مثل یک پورت USB که به دستگاه های مختلف اجازه می دهد که اگر یک آداپتور USB دارند، با یک کامپیوتر ارتباط برقرار کنند. پورت ها همچنین به اپلیکیشن اجازه میدهند تا با سیستم ها یا خدمات خارجی مانند پایگاه های داده، کارگزاران پیام (message brokers)، سایر اپلیکیشن ها و ... ارتباط برقرار کنند.
نکته: یک پورت همیشه باید دو آیتم به آن متصل باشد، که یکی از آنها همواره تست است.
آداپتورها (Adapters)
یک آداپتور با استفاده از یک فناوری خاص، تعامل با برنامه را از طریق یک پورت آغاز می کند. به عنوان مثال، کنترلر REST یک آداپتور را نشان می دهد که به مشتری اجازه می دهد با برنامه ارتباط برقرار کند. میتوان به تعداد مورد نیاز برای هر پورت آداپتور وجود داشته باشد بدون اینکه خطری برای پورتها یا خود برنامه ایجاد کند.
اپلیکیشن
اپلیکیشن هسته ی سیستم است و شامل خدمات کاربردی است که عملکرد یا موارد استفاده را هماهنگ میکند. همچنین شامل مدل دامنه (Domain Model) ، که منطق کسب و کار در Aggregates، Entities و Value Objects تعبیه شده است، می باشد. این برنامه توسط یک شش ضلعی نمایش داده می شود که دستورات یا کوئری ها را از پورت ها دریافت کرده و درخواست ها را از طریق پورت ها به دیگر کنش گرهای خارجی مانند پایگاه های داده ارسال می کند. هنگامی که با طراحی دامنه محور جفت می شود، برنامه یا شش ضلعی شامل لایه های برنامه و دامنه می باشد و لایه های رابط کاربری و زیرساخت را بیرون می گذارد.
چرا شش ضلعی؟
ایده ی Alistair برای استفاده از شش ضلعی صرفاً نمایش تصویری از ترکیبات چند پورت/آداپتور یک اپلیکیشن و همچنین به تصویر کشیدن چگونگی تعاملات و پیاده سازی های متفاوت سمت چپ برنامه، یا "سمت driving "، نسبت به سمت راست یا "سمت driven " است.
سمت Driving در مقابل سمت Driven
کنشگرهای driving (یا اصلی) آنهایی هستند که تعامل را آغاز می کنند و همیشه در سمت چپ تصویر می شوند. به عنوان مثال، یک آداپتور driving می تواند یک کنترل کننده باشد که ورودی (کاربر) را می گیرد و آن را از طریق یک پورت به برنامه ارسال می کند.
کنشگر driven (یا ثانویه) آنهایی می باشند که توسط برنامه behavior را به کار می اندازند. به عنوان مثال، یک آداپتور پایگاه داده توسط اپلیکیشن به منظور واکشی یک مجموعه داده خاص، فراخوانی می شود.
هنگامی که نوبت به اجرا می رسد، چند نکته مهم وجود دارد که نباید از آنها غافل شد:
· پورت ها (بیشتر اوقات، بسته به زبانی که انتخاب می کنید) به عنوان رابط در کد نشان داده می شوند.
· آداپتورهای driving از یک پورت استفاده میکنند و یک سرویس برنامه رابط تعریف شده توسط پورت را پیادهسازی میکند، در این مورد هم رابط پورت و هم پیادهسازی آن در داخل ششضلعی هستند.
· آداپتورهای Driven پورت را پیاده سازی می کنند و یک سرویس برنامه از آن استفاده می کند، در این مورد پورت در داخل شش ضلعی است، اما پیاده سازی در آداپتور و در نتیجه خارج از شش ضلعی است.
وابستگی معکوس (Dependency Inversion) در در زمینه معماری شش ضلعی
اصل Dependency Inversion یکی از 5 اصلی است که توسط (عمو) باب مارتین در Paper OO Design Quality Metrics و بعداً در کتاب Agile Software Development Principles, Patterns and Practices ابداع شده و به شرح زیر تعریف می شود:
· ماژول های سطح بالا نباید به ماژول های سطح پایین وابسته باشند بلکه هر دو باید به انتزاعات وابسته باشند.
· انتزاعات نباید به جزئیات وابستگی داشته باشند. بلکه جزئیات باید به انتزاعات وابسته باشند.
همانطور که قبلا ذکر شد، سمت چپ و راست شش ضلعی شامل 2 نوع مختلف کنشگر است، Driving و Driven که در آن هر دوی پورت و آداپتور وجود دارند.
در سمت Driving، آداپتور وابسته به پورتی است که توسط Application Service پیادهسازی میشود، بنابراین آداپتور نمیداند چه کسی به فراخوانی های آن واکنش نشان خواهد داد. بلکه فقط می داند چه متدهایی تضمین شده که در دسترس باشند، بنابراین به یک انتزاع وابسته است.
در سمت Driven، Application Service وابسته به پورت است و آداپتور آن رابط پورت را پیادهسازی کرده و به طور موثر وابستگی را معکوس میکند، زیرا آداپتور «سطح پایین» مجبور به پیادهسازی انتزاع تعریف شده در هسته برنامه، که "سطح بالاتر" است، می باشد.
چرا باید از پورت ها و آداپتورها استفاده کرد؟
استفاده از معماری پورت ها و آداپتورها مزایای زیادی دارد، یکی از آنها این است که بتوانید منطق برنامه و منطق دامنه خود را به صورت کاملاً آزمایشی جداسازی کنید. از آنجایی که به عوامل خارجی وابسته نیست، تست آن طبیعی می شود. همچنین به شما این امکان را میدهد که تمام رابطهای سیستم خود را «بر اساس هدف» و نه با تکنولوژی طراحی کنید، از قفل شدن شما جلوگیری میکند، و توسعه ی پشته تکنولوژی برنامهتان را با گذشت زمان آسانتر میکند. اگر نیاز به تغییر لایه persistence دارید، این کار را انجام دهید. اگر باید اجازه دهید برنامه شما به جای انسان توسط ربات های Slack فراخوانی شود، معطل نکنید! تنها کاری که باید انجام دهید این است که آداپتورهای جدید را پیاده سازی کنید. فقط مراقب ریزش بین لایه های Application و Domain باشید.
ساختار بندی برنامه و نمونه کد
در این بخش، نسخه بسیار ساده شدهای از یک سرویس را پیادهسازی میکنیم که درخواستهای ایجاد سفارش برای یک اپلیکیشن تجارت الکترونیک ساختگی را پردازش میکند. لطفاً توجه داشته باشید که نمونهها آگاهانه بخشهای خاصی را حذف کرده اند و جنبههای مهمی از کدهای به خوبی نوشته شده، مانند مدیریت خطا و نامگذاری مناسب را نادیده میگیرند تا بر مفاهیم اساسی پیادهسازی پورتها و آداپتورها تمرکز کنند. مهم است که تاکید کنیم که تمام لایههای معماری لایهای Domain-Driven Design هنوز در هنگام ساختاربندی یک برنامه کاربردی بر اساس پورتها و آداپتورها مناسب هستند، زیرا تفکیک ایدهآلی برای همه مولفه ها فراهم میکنند. در برنامه ساختگی ما، کنترلر آداپتور Driving است که از پورت Driving استفاده می کند.
پورت Driving یک رابط در لایه برنامه یا شش ضلعی خواهد بود.
سرویس اپلیکیشن پورت Driving را پیاده سازی می کند.
همانطور که می بینید، Application Service که مولفه ی هماهنگ کننده است، از پورت Driven استفاده می کند.
در نهایت، پورت Driven توسط آداپتور Driven پیاده سازی می شود.
نتیجه گیری:
معماری شش ضلعی یا پورت ها و آداپتورها، گلوله نقره ای برای همه ی برنامه ها نیست. این معماری شامل سطح معینی از پیچیدگی است، که وقتی با دقت از آن استفاده شود، مزایای زیادی برای سیستم شما به همراه خواهد داشت.
پورت ها و آداپتورها هنگامی که به درستی پیاده سازی و با سایر متدولوژی ها، مانند Domain-Driven Design، جفت شوند، می توانند پایداری و توسعه پذیری طولانی مدت برنامه را تضمین کنند و ارزش زیادی برای سیستم و سازمان به ارمغان بیاورند.
این مطلب یکی از تمرینات درس #معماری_نرم_افزار در #دانشگاه_شهید_بهشتی تهران می باشد.
منابع:
Alistair Cockburn’s original paper on Hexagonal Architecture
Awesome read on Domain-Driven Design: Everything You Always Wanted to Know About it, But Were Afraid to Ask
Eric Evans’ Domain-Driven Design: Tackling Complexity in the Heart of Software
Bob Martin’s OO Design Quality Metrics
Bob Martin’s Agile Software Development Principles, Patterns and Practices