معماری Hexagonal

معماری Hexagonal

معرفی معماری Hexagonal

معماری Hexagonal یکی از تاثیرگذارترین معماری ها در جدا کردن لایه های کد بوده است که مسئولیت هر قسمت از برنامه را جدا می کند و مشخص می کند که دقیقا چگونه و چرا باید از لایه ها استفاده کنیم و در بین لایه ها رابط هایی قرار دهیم؟

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

در نظر داشته باشیم که معماری Hexagonal یک راه جدید برای برنامه نویسی نیست. در واقع این معماری یک شیوه درست و بهتر برای یک برنامه ی لایه ای است. به ویژه هنگامی که بخواهیم برنامه ی خود را در ارتباط با تعدادی عملکرد قرار دهیم تا کارا باشد از این معماری استفاده می کنیم.

چالش یا مسئله

خب قبل از اینکه وارد موضوع اصلی این مقاله بشویم بهتر است چالشی که نرم افزار ها با آن مواجه بودند را، مشخص کنیم تا رسالت معماری و معماری Hexagonal را بهتر متوجه شویم.

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

user interface and logic
شکل 1- نرم افزاری که منطق تجاری و رابط کاربری و داده ها در یک لایه پیاده شدند و وابستگی شدیدی به یکدیگر پیدا کرده اند

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

حالا فرض کنیم که برنامه ای داریم که به واسطه ی ارتباط با برنامه ها یا سرویس های دیگر غنی تر می شود و می تواند خدمات بیشتری را ارائه کند(غنی شدن از نظر داده ای و یا کارایی). برای مثال برنامه ی ما در صورتی که با یک موتور جستجو در ارتباط باشد، می تواند به کاربر نهایی قابلیت جستجو بین محتوای موجود در محصول را ارائه دهد. اینجا نیز مهم است که این ارتباط به صورتی باشد که منطق برنامه (سرویس دهنده یا سرویس گیرنده) روی ارتباط تاثیر نگذارد.

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

layered applications
شکل 2 - لایه بندی کردن برنامه در سه سطح رابط کاربری، منطق تجاری و سرویس دهنده های زیر ساختی

معماری

معماری به این موضوع می پردازد که چه چیز را در کجای برنامه قرار دهیم و چگونه موجودیت های مختلف برنامه با یکدیگر ارتباط برقرار کنند.

زمانی اهمیت معماری بیشتر مشخص می شود که ما با دو موضوع مهم نگه داری و بدهی فنی یک برنامه مواجه هستیم.

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

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

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

در برنامه اگر برای هر تغییر، تصمیمی بیرون از معماری بگیریم یا اینکه معماری را کامل و خوب در نظر نگرفته باشیم. ممکن است بدهی فنی به جا بگذاریم که در آینده انرژی و زمان را هدر دهد.

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

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

معرفی معماری Hexagonal

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

hexagonal
شکل 3- نمایی از نرم افزاری که از معماری Hexagonal استفاده می کند

نام دیگر و اصلی این معماری Ports And Adaptors هست زیرا پورت ها و آداپتور های مختلف و خاص باعث شده که بتوان لایه ها را از هم دیگر تفکیک کرد و برای هر ارتباطی در هر وجه، پورت و آداپتور مجزایی در نظر گرفت.

در ادامه به این دو مفهوم در این معماری بیشتر می پردازیم:

پورت چیست؟ 

یک نقطه ای برای ورود یا خروج یک برنامه است که از طریق آن می توان با آن برنامه ارتباط برقرار کرد و استفاده کرد. در خیلی از زبان های برنامه نویسی یک رابط یا همان Interface باز تعریف می شود. در واقع Port در هر وجه این معماری یک ارتباط با یک برنامه است که داده ارسال و دریافت می کند. این port می تواند مسیری برای ارسال یا پاسخ دهی به یک درخواست باشد یا اینکه مسیری برای دسترسی به دیتای بیشتر باشد. برای مثال پورتی در برنامه وجود دارد که اطلاعات و درخواست های ارسالی و دریافتی مربوط به یک جستجوی پیشرفته را به یک موتور جستجو ارسال می کند. پورت ها رابط هایی هستند که بدون نیاز به دانش پیاده سازی برنامه یا سرویس های دیگر (در این جا موتور جستجو)، باید پیاده سازی شود و تنها بستری آماده کند که ارتباط با نیازمندی که وجود دارد برقرار شود.

آداپتور چیست؟ 

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

در سطح برنامه نویسی، آداپتور، كلاسی است كه يك رابط را به رابط ديگری تبديل می كند. به عنوان مثال، یک آداپتور یک رابط A را پیاده سازی می کند و به یک رابط B نتایج را تحویل می دهد. و این گونه ما می توانیم حتی وقتی دو برنامه می خواهند با یکدیگر تعامل داشته باشند، بدون این که برنامه ها از منطق هم دیگر با خبر شوند فقط با پیاده سازی کلاسی بین دو رابط آنها، ارتباط را برقرار کنیم.

ما در معماری Hexagonal دو نوع آداپتور مختلف داریم. با توجه به شکل زیر آداپتورهای سمت چپ، که سمت رابط کاربری است، آداپتورهای اصلی یا Driving Adapter نامیده می شوند زیرا آنها هستند که باعث شروع عملیات یک برنامه می شوند، در حالی که آداپتورهای سمت راست، نشان دهنده ی اتصال ها به ابزارهای Backend، آداپتورهای ثانویه یا رانده شده هستند زیرا همیشه نسبت به درخواست آداپتور اصلی واکنش نشان می دهند.

hexagonal architecture

فواید معماری Hexagonal

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

با استفاده از این معماری این قابلیت را داریم که پیاده سازی فناوری ها را به صورت ایزوله انجام دهیم. برای این موضوع مثالی را در نظر بگیرید.

فرض کنید که برنامه ی ما نیاز به یک موتور جستجو دارد و می خواهد از SOLR و کتابحانه ی متن باز آن استفاده کند: 

اگر از روش قدیمی پیش بریم ما از کتابخانه ی مربوط به SOLR مستقیم در کد برنامه ی اصلی استفاده می کنیم.

ولی اگر از روش Hexagonal پیش بریم، نیاز داریم که یک رابط ایجاد کنیم مثلا با نام UserSearchInterface، که درصورت نیاز، به عنوان یک اعلام نوع در کد خود استفاده خواهیم کرد. ما همچنین آداپتور SOLR را ایجاد خواهیم کرد که این رابط را پیاده سازی می کند، بگذارید نام آن را UserSearchSolrAdapter بگذاریم. این پیاده سازی یک آداپتور برای کتابخانه ی SOLR است، که در آن، کتابخانه را تزریق می کنیم و از آن برای پیاده سازی روش های مورد نیاز در رابط، استفاده می کنیم.

در برهه ای از زمان، ما می خواهیم از SOLR به Elasticsearch تغییر دهیم. افزون بر این، برای همان جستجو، گاهی اوقات ما می خواهیم از SOLR و بار دیگر می خواهیم از Elasticsearch استفاده كنیم، این تصمیم در زمان اجرا گرفته می شود. اگر از روش سنتی استفاده کنیم، باید کتابخانه ی SOLR را برای کتابخانه ی Elasticsearch جستجو و جایگزین کنیم. با این حال، این یک جستجوی ساده و جایگزین معمولی نیست. کتابخانه ها روش های مختلفی برای استفاده دارند، روش های مختلف با ورودی ها و خروجی های مختلف، بنابراین جایگزینی کتابخانه ها کار ساده ای نیست. و استفاده از یک کتابخانه به جای کتابخانه ی دیگر، در زمان اجرا، حتی امکان پذیر نخواهد بود. با این حال، اگر ما از پورت و آداپتور استفاده کردیم، فقط باید یک آداپتور جدید ایجاد کنیم، مثلا نام آن را UserSearchElasticsearchAdapter بگذاریم و آن را به جای آداپتور SOLR تزریق کنیم.

از طرف دیگر در نظر بگیرید که برنامه ای داریم که به یک رابط کاربری گرافیکی وب، یک CLI و یک رابط برنامه ی کاربردی وب نیاز دارد. ما همچنین برخی از قابلیت ها را داریم که می خواهیم در هر سه رابط کاربر در دسترس قرار دهیم، با استفاده از Hexagonal، ما این قابلیت را به روش سرویس دهنده پیاده سازی می کنیم و آن را به عنوان یک مورد استفاده(use case) تصور می کنیم. این سرویس، رابطی را تعیین می کند که روش ها، ورودی ها و خروجی ها را مشخص می کند.

همچنین تست نویسی نرم افزار آسان تر می شود. در اولین مثالی که زده شد، ما می توانیم رابط (Port) را Mock  کنیم و برنامه خود را بدون استفاده از اصل سرویس که SOLR یا Elasticsearch هست، تست کنیم. 

جمع بندی

در نهایت معماری Hexagonal تنها یک هدف دارد: جدا کردن منطق تجارت از ساز و کار ارتباط با رابط کاربری و همچنین ابزارهای استفاده شده توسط سیستم. معماری Hexagonal این کار را با استفاده از یک ساختار مشترک زبان برنامه نویسی انجام می دهد به نام رابط ها. در کل در این معماری در سمت UI یا در سمت کنترل کننده ها در زیرساخت، آداپتورهایی ایجاد می کنیم که رابط های برنامه ما را اجرا می کنند و از این طریق معماری لایه ای چند وجه خواهیم داشت.

منابع : 

https://fideloper.com/hexagonal-architecture

https://herbertograca.com/2017/09/14/ports-adapters-architecture

http://wiki.c2.com/?HexagonalArchitecture

 

نظرات

پیشنهادات بیشتر سکان بلاگ برای شما

اگر login نکردی برامون ایمیلت رو بنویس: