آشنایی با معماری clean

با سلام و درود، چند وقتی میشه دارم در مورد زبان برنامه نویسی dartو flutter یه مقدار کسب مهارت می کنم. یکی از ویدیوهای آموزشی Youtube که به تازگی باهاش آشنا شدم، این بود که از معماری clean برای پیاده سازی اپلیکیشن استفاده می کرد (The Clean Architecture)، با اینکه معماری چندان جدیدی ام نیست و تقریبا ۱۰ سالی از عمرش می گذره ولی نوشتن در موردش خالی از لطف نیست و احتمال زیاد میدم که برای خیلی از دوستان کاربردی باشه و به کار بیاد.

معماری Clean – The Clean Architecture

مرجع اصلی این نوشته این مطلب هست که سعی کردم با اضافات از مطالب و مقالات دیگه، مبحث The Clean Architecture رو کامل تر کنم.

در طی سال های اخیر، شاهد گستره ی زیادی از ایده های مرتبط با معماری سیستم ها بوده ایم که شامل موارد زیر می شوند:

اگرچه این معماری ها همگی از لحاظ جزییات تا حدودی متفاوت هستند، اما بسیار شبیه به هم هستند. همه شان هدف مشترکی جهت جداسازی نگرانی ها دارند. همه شان با تقسیم نرم افزار به لایه به این جدا سازی می رسند. هر کدام حداقل یک لایه برای business rules و لایه دیگری برای رابط ها (interfaces) دارند.

هر کدام از این معماری ها، سیستمی را تولید می کنند که:

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

۲- تست پذیر بودن، business rules می توانند بدون UI، Database، Web Server یا هر المان خارجی، تست شوند.

۳- مستقل از UI هستند، UI می تواند به راحتی بدون اینکه در باقی سیستم تغییری ایجاد کند، تغییر کند.

۴- مستقل از DB، شما می توانید از Oracle به SQL Server یا مونگو و غیره مهاجرت کنید. Business rules های شما، محدود به پایگاه داده نیستند.

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

شکل ۱ – معماری Clean

قانون وابستگی

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

قانون اصلی که این معماری را عملیاتی می کند، قانون وابستگی است. این قانون می گوید که «وابستگی سورس کد فقط می تواند به سمت داخل باشد». هیچ چیز در یک دایره داخلی نمی تواند چیزی در مورد یک دایره بیرونی بداند. به طور خاص، نام چیزی که در یک دایره بیرونی آورده شده است، نباید توسط کد داخل یک دایره داخلی ذکر(mention) شود. این شامل کلاس ها، توابع، متغیر ها یا هر موجودیت دارای اسم در نرم افزار می شود.

با همین روال، فرمت های دایره ای که در یک دایره بیرونی استفاده می شوند، نباید توسط یک دایره داخلی مورد استفاده قرار گیرند. مخصوصا اگر فرمت های مورد نظر، توسط یک فریمورک در دایره خارجی تولید شده باشند. نمی خواهیم چیزی از دایره های بیرونی روی دایره های داخلی تاثیر بگذارد.

موجودیت ها (Entities)

موجودیت ها business rule های گسترده و مهم را کپسوله (encapsulate) می کنند. موجودیت می تواند یک object با متدهایش باشد یا می تواند مجموعه ای از ساختمان داده ها یا توابع باشد. این مهم نیست که موجودیت ها می توانند توسط اپلیکیشن های متفاوت در تشکیلات(سازمان) مورد نظر استفاده شوند.

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

Use Cases

نرم افزار در این لایه شامل قوانین خاص اپلیکیشنِ بیزینس می شود. تمامی use case های سیستم را پیاده سازی و در بر می گیرد. این use case ها جریان داده به سمت موجودیت ها یا از سمت موجودیت ها را هماهنگ می کند و آن موجودیت ها (entities) را جهت استفاده از قوانین گسترده بیزینس جهت دستیابی به اهداف use case ، هدایت می کند.

ما انتظار تغییر در این لایه جهت تاثیر روی موجودیت ها را نداریم. ما همچنین انتظار نداریم این لایه توسط تغییرات خارجی مانند پایگاه داده، UI یا هر فریمروک رایج دیگری تاثیر بگیرد.

با این حال انتظار داریم که تغییرات در عملکرد اپلیکیشن، روی use case ها و در نتیجه نرم افزار در این لایه تاثیر خواهد گذاشت. اگر جزییات یک use case تغییر کند، در نتیجه برخی از کد های این لایه قطعا تحت تاثیر قرار خواهند گرفت.

  • use case ها جایی هستند که business logic اجرا می شوند.
  • عمل هایی که بیزینس شما را نشان می دهند. چیزی که قرار است با اپلیکیشن انجام دهید.
  • business logic خالص، کد ساده (بجز برخی از کتابخانه های کاربردی)
  • use case نیم داند چه کسی آن را راه انداخته است و چطور نتایج قرار است نمایش داده شوند( برای مثال، می تواند در یک صفحه وب باشد یا به عنوان JSONبرگشت داده شود)
  • استثناهای بیزینس را نمایان می کند.

Interface Adapters

نرم افزار در این لایه، مجموعه ای از آداپتورها است که داده را از قالب مناسب برای use caseها و entity ها به قالبی (فرمت) مناسب برای برخی از نمایندگی های خارجی مانند پایگاه داده یا وب تبدیل می کند. برای مثال، این لایه می تواند به صورت کامل شامل معماری MVC از یک GUI باشد. ارائه دهندگان، بازدیدها و کنترلرها (Presenters, Views, and Controllers)، همگی به اینجا تعلق دارند. این مدل ها احتمالا تنها ساختار داده هایی هستند که از کنترلر ها به سمت use case ها منتقل می شوند و سپس از use case ها به سمت presenterها و view ها بازگشت داده می شود.

به طور مشابه، داده در این لایه از مناسب ترین فرم برای entity و usecase ها به مناسب ترین فرم برای هر فریمورک ماندگاری که استفاده می شود (مانند پایگاه داده)، تبدیل می شود. هیچ کدی در داخل این دایره نباید چیزی درباره پایگاه داده بشناسد. اگر پایگاه داده یک پایگاه از نوع SQL است، سپس تمامی SQL باید به این لایه محدود شود، و به خصوص به قسمت هایی از لایه که مربوط به پایگاه داده است.

در این لایه همچنین آداپتور دیگری وجود دارد که ضروری است داده را از برخی فرم های خارجی، مانند یک سرویس خارجی، به فرم داخلی که توسط use caseو entity ها استفاده می شود، تبدیل کند.

  • واکشی و ذخیره داده از/به تعدادی از منابع (پایگاه داده، دستگاه های شبکه، فایل سیستم و غیره )
  • تعریف واسط/interface برای داده هایی که نیاز دارند تا مقداری لاجیک را اعمال کنند. یک یا چند ارائه دهنده داده (data provider)، واسط را پیاده سازی خواهند کرد، اما use case نمی داند که داده از کجا آمده است.
  • پیاده سازی واسط های تعریف شده توسط Use Case

چارچوب ها و درایور ها (Frameworks and Drivers)

بیرونی ترین لایه معمولا از چارچوب ها و ابزارهایی مانند web framework، دیتابیس و غیره تشکیل شده است. معمولا شما به غیر از glue code که این اتصالات با دایره داخلی را برقرار می کند، کد دیگری نمی بینید.

این لایه جایی است که همه جزییات در آن قرار دارد. web جزییات است. DB جزییات است. ما این چیزها را در قسمت بیرونی نگه می داریم که آسیب کمتری بتوانند برسانند.

فقط چهار دایره؟

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

عبور از مرزها

در پایین سمت راست نمودار (شکل ۱)، مثالی از نحوه عبور از مرزهای دایره ها وجود دارد. controller ها و presenter ها را در حال ارتباط با Use Case ها در لایه بعدی را نشان می دهد. به جریان کنترل توجه داشته باشید. این جریان از controller شروع می شود و از میان Use Case عبور می کند و سپس اجرا شدنش را در Presenter به پایان می رساند.

ما معمولا این مسايل تناقض ظاهری را با استفاده از اصول وارونگی وابستگی (Dependency Inversion Principle) حل می کنیم. در زبانی مانند Java، برای مثال، ما واسط ها (interfaces) و روابط وراثت (Inheritance relationships ) را به گونه ای ترتیب می دهیم که وابستگی های سورس کد دقیقا در نقاط درست در میان مرز، مخالف جریان کنترل باشد.

برای مثال، در نظر بگیرید که Use Case نیاز دارد تا Presenter را فراخوانی کند. با این حال این فراخوانی نباید به صورت مستقیم باشد چراکه این امر قانون وابستگی را نقض می کند: هیچ اسمی در یک دایره خارجی نمی تواند داخل یک دایره داخلی ذکر شود. بنابراین ما Use Case داریم که یک Interface را (اینجا به عنوان Use Case Output Port نشان داده شده است ) در دایره داخلی فراخوانی می کند و یک Presenter در دایره خارجی داریم که آن را پیاده سازی می کند.

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

چه داده هایی از مرزها عبور می کنند

به طور معمول داده هایی که از مرزها عبور می کنند دارای ساختار داده های ساده ای هستند. شما اگر بخواهید می توانید از ساختارهای پایه یا Object های ساده انتقال داده استفاده کنید. یا داده می تواند به سادگی آرگومانی در فراخوانی تابع باشد. یا می توانید آن را درون یک hashmapقرار دهید، یا آن را درون یک Objectبسازید. نکته مهم این است که ساختارهای داده ای ایزوله و ساده از مرزها عبور می کنند. ما نمیخواهیم تقلب کنیم و ردیف هایی از پایگاه داده یا entity را عبور دهیم.ما نمی خواهیم ساختار داده هر نوع وابستگی ای داشته باشد که قانون وابستگی را نقض کند.

برای مثال، بسیاری از فریمورک های پایگاه داده، فرمت داده مناسبی را در پاسخ یک کوئری برگشت می دهند. ما ممکن است این را RowStructure بنامیم. ما نمی خواهیم که ساختار ردیف را از میان یک مرز داخلی عبور دهیم که قانون وابستگی را نقض کند، زیرا یک دایره داخلی را مجبور می کند چیزی در مورد دایره خارجی بداند.

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

نتیجه گیری

مطابقت با این قوانین ساده کار سختی نیست و در آینده شما را از بسیاری از دردها رها می سازد. با تقسیم کردن نرم افزار به لایه ها، و مطابقت با قانون وابستگی، شما سیستمی را خواهید ساخت که ذاتا قابل تست است همراه با تمامی مزایایی که همراه دارد. زمانی که هر یک از قسمت های خارجی سیستم منسوخ شد، مانند پایگاه داده، یا web framework، شما می توانید اون اِلِمان های منسوخ شده را با کمترین سر و صدا جایگزین کنید.

مراجع:

https://www.freecodecamp.org/news/a-quick-introduction-to-clean-architecture-990c014448d2/

https://blog.cleancoder.com/uncle-bob/2012/08/13/the-clean-architecture.html

برای مطالعه بیشتر، کتاب « Clean Architecture , A C RAFTSMAN’S G UIDE TO S OFTWARE S TRUCTURE AND DESIGN » اثری از رابرت سی مارتین، پیشنهاد می شود. کتاب Clean Architecture را می توانید از لینک زیر دانلود نمایید.