Clean Architecture معماری توسعه نرم‌افزار

سطح آموزش: #حرفه‌ای

تا امروز به دفعات از اهمیت معماری صحبت کردم و MVP رو به عنوان یک پترن خوب برای توسعه اندروید معرفی کردم.

اما واقعیت اینه که MVP با تمام مزیت‌هایی که داره شاید خیلی نشه اسم معماری رو روش گذاشت. بیشتر یک پترن یا الگو هست تا یک معماری. البته باز اینکه معماری هست یا نه جای بحث داره و به تعریف ما از معماری برمیگرده اما در کل هر اسمی که روش بذاریم مدل ساده‌ایه و برای پروژه‌های بزرگ محدودیت‌هایی داره.

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

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

تمام این مسائل باعث میشه استفاده از یک معماری خوب توی پروژه خیلی مهم بشه. معماری فوق‌العاده‌ای که خودم هم ازش استفاده می‌کنم Clean Architecture ـه. این معماری پیشنهاد رابرت مارتین (uncle bob) هست که قبلا هم راجع بهش صحبت کرده بودم.

مقدمه

لینک مقاله‌ای که منبع اصلی پست من هم هست رو اینجا می‌تونید پیدا کنید.

سعی میکنم توی این پست به طور کامل با ذات clean architecture آشنا بشیم. به زودی متوجه میشید که clean architecture چیزی نیست که مختص به اندروید باشه و به طور کلی برای توسعه نرم‌افزار مطرح هست اما ازونجایی که احتمالا بیشتر دنبال‌کننده‌های این وبلاگ برنامه‌نویس اندروید هستن به مرور با ابزارهایی که به ما کمک می‌کنن تا بتونیم clean architecture رو به شکل اصولی پیاده کنیم آشنا میشیم. البته تا همین لحظه هم تا حد خوبی با design patternها و ابزارهایی مثل Dagger و Reactive Programming آشنا شدیم. ابزارهایی که برای پیاده‌سازی سریع و اصولی clean architecture خیلی میتونن به کمکمون بیان.

Clean Architecture

ما معماری‌های متفاوتی برای طراحی نرم‌افزار داریم:

Hexagonal Architecture

Onion Architecture

Screaming Architecture

DCI

BCE

نقطه مشترک تمام این معماری‌ها یک مسئله‌ست.

separation of concerns

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

و تمام این معماری‌ها با تقسیم نرم‌افزار به لایه‌های مختلف به این هدف میرسن. و تمام معماری‌ها حداقل دو لایه، یکی برای business rule و دیگری برای interfaceها دارن.

ویژگی مشترک تمام معماری‌ها این هست که:

  1. به framework وابسته نیستن. برای همین باعث میشن از فریمورک ها تنها به عنوان ابزار استفاده کنیم و درگیر محدودیت‌های هر فریمورک نشیم.
  2. قابل تست بودن. میتونیم business rulesمون رو بدون وجود ui، وب سرور، دیتابیس یا هر وابستگی خارجی دیگه‌ای تست کنیم.
  3. مستقل از UI هستن. میشه ui رو بدون اینکه نیاز باشه بخش‌های دیگه رو تغییر داد آپدیت کرد.
  4. مستقل از database هستن، بنابراین میشه بدون نیاز به تغییر بقیه بخش‌ها از oracle یا Sql server به Mongo ، CouchDB یا هرچیز دیگه‌ای تغییر دیتابیس داد.
  5. مستقل از واسطه خارجی هستن. به زبون ساده یعنی business rule هیچ اطلاعی از بخش‌های خارج از خودش نداره.

تلاش دیاگرامی که در ادامه می‌بینید اینه که تمام این معماری‌ها رو در یک ایده عملی ادغام کنه.

clean architecture diagram دیاگرام معماری کلین clean

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

دوایر هم مرکز نماینده بخش‌های مختلف نرم‌افزار هستن. به طور کلی هرچی بیشتر به مرکز حرکت کنیم کد ما high level تر میشه. دایره‌های بیرونی مکانیزم‌ها (پیاده‌سازی‌ها) هستن و دایره‌های درونی سیاست‌ها و خطی‌مشی‌ها هستن.

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

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

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

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

Entity ها business ruleهای محصول رو encapsulate میکنن! میدونم جمله عجیب و غریبی شد. معمولا اینطوریه که محصول ما صرفا یک اپلیکیشن نیست. به عنوان تلگرام رو در نظر بگیرید که روی انواع سیستم عامل از اندروید و ios گرفته تا ویندوز و لینوکس اپلیکیشن داره. قطعا ما نمیخوایم برای هر پلتفرم یکسری برنامه‌نویس جدا داشته باشیم که صرفا روی اون پلتفرم از اول نرم‌افزار رو توسعه بدن. هربار که تغییری نیاز داریم مجددا سراغ تک‌تک کدها بریم و تغییرات رو اعمال کنیم. بهتر اینه که ما یک هسته مرکزی واحد داشته باشیم و بسته به پلتفرم UI و Database و موارد دیگه رو تغییر بدیم. البته به دلایلی تلگرام برای پلتفرم‌های مختلف از سورس‌کدهای مختلف استفاده می‌کنه ولی به طور کلی محصولات enterprise از این قانون تبعیت می‌کنن

بنابراین میخوایم business rule ثابت بمونه. حالا entity ها یک shell طوری هستن که business rule داخل اون‌ها قرار میگیره.

در مواقعی که چند محصولی نیستید هم Entityها business object هایی هستن که کلی‌ترین و high-level ترین قواعد نرم‌افزار رو شامل میشن. قواعدی که به ندرت قرار هست تغییر کنن. برای مثال ما توقع نداریم مسائل امنیت نرم‌افزار entityها رو تحت تاثیر قرار بدن.

Use Cases

توی این لایه business ruleهای مخصوص هر محصول رو داریم. نرم‌افزار توی این لایه تمام use caseهای مورد نیاز سیستم رو encapsulate  و پیاده‌سازی میکنه.

یکمی ترجمه بعضی از واژه‌ها مثل encapsulate  و usecase سخته. Encapsulate  رو کلاسی در نظر بگیرید که یکسری component رو درون خودش داره و به ما اجازه استفاده ازون‌ها رو بدون اینکه بدونیم داخل چه اتفاقی میفته میده. مثلا وقتی ما از یک تابع استفاده می‌کنیم کاری نداریم متد چی کار میکنه. ما فقط ورودی مورد نیاز رو میدیم و انتظار خروجی مطلوب داریم. بنابراین تابع هرچیزی که داخلش داره رو encapsulate  کرده.

در مورد use case هم اگر نمیدونید چیه اینطوری تصور کنید که کلاس‌هایی هستن که یک کار بخصوص رو انجام میدن. مثلا ما میخوایم دیتایی رو از سرور بخونیم. یک use case میتونیم داشته باشیم که با صدا کردنش بره دیتا رو بخونه و به ما برگردونه. نوعی عملگرِ واحد هستن. عملگر هستن چون برامون کاری انجام میدن و واحد هستن چون طبق قانون SRP هر کلاس باید فقط یک دلیل برای تغییر کردن داشته باشه.

کارِ use case ها هماهنگی حرکت داده‌ها به entity و از entity ها و همچنین هدایت entityها برای استفاده از business ruleهاشون برای رسیدن use case به هدفش است.

ما توقع نداریم که تغییر روی این لایه تاثیری روی entityها داشته باشه، همونجوری که توقع نداریم تغییرات روی لایه‌های بالاتر مثل دیتابیس و UI تغییری روی این لایه داشته باشه.

البته ما توقع داریم تغییرات روی عملکرد اپلیکیشن این لایه رو تحت تاثیر قرار بده. مثلا اگه از سمت سرور تغییراتی اعمال بشه نباید Entityها تغییر کنن ولی Use case ها قطعا از این تغییرات تاثیر میگیرن.

Interface Adapters

نرم‌افزار توی این لایه مجموعه‌ای از adapterهاست که اطلاعات قابل درک برای use case ها و entityها رو به اطلاعاتی قابل درک برای منابع مختلف مثل database تبدیل میکنه. جای پترن‌هایی مثل MVC هم توی این لایه‌ست. Controller و presenter و view و این‌ها متعلق به این لایه هستن. Modelها نیز data structureهایی هستن که از کنترلرها به use caseها و سپس از use caseها به presenterها و viewها فرستاده میشن.

به همین صورت هم تو این لایه داده‌ها از مدل قابل درک برای entityها و use caseها به مدل قابل درک برای لایه‌های بیرونی‌تر مثل دیتابیس تبدیل میشن. قرار نیست که لایه‌های داخلی خبری از اتفاقات این لایه داشته باشن برای همین مثلا اگه SQL داریم، تمام داده‌های مربوطه باید محدود به این لایه‌ باشن و مدل‌ها هم وقتی میخوان از لایه‌های درونی‌تر وارد شن باید map شن به مدل‌هایی جدید و قابل درک برای این لایه.

همچنین تمام adapterهایی که برای تبدیل داده‌هایی که از سرویس‌های خارجی میان به داده‌های قابل درک برای entity و use caseها توی این لایه قرار می‌گیرن.

Frameworks and Drivers

به طور کلی خارجی‌ترین لایه از فریم‌ورک‌ها و ابزارهایی مثل دیتابیس و وب فریم‌ورک و… تشکیل شده. قرار نیست که حجم کد زیادی توی این لایه نوشته شه و بیشتر در نقش رابط ارتباطی‌ای با دایره‌های داخلی‌تره.

این لایه جاییه که تمام جزئیات قرار می‌گیرن. وب و دیتابیس جزئیاتی هستن که ما توی لایه بیرونی قرار میدیم جایی که کمترین خطر رو برامون دارن.

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

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

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

خارجی‌ترین دایره جزئی‌ترینِ پیاده‌سازی‌هاست و هرچه بیشتر به داخل بریم abstraction بیشتر میشه و ما مسائل کلی‌تری رو encapsulate می‌کنیم.

ارتباط مرز‌ها

پایین سمت راستِ دیاگرام مثالی هست که نشون میده ما چطوری بین مرزها حرکت می‌کنیم. تصویر نشون میده که controllerها و presenterها چطوری با use caseها در لایه بعدی ارتباط برقرار میکنن.

 

clean architecture clean architecture

به جریان حرکتمون دقت کنید. از controllerها شروع میشه از use caseها استفاده می‌کنن و در نهایت چیزی رو داخل presenter اجرا میکنن. به جهت فلش‌ها هم دقت کنید. داخل به سمت use caseهاست که نشون میده presenter و controller از use case اطلاع دارن و ازش استفاده می‌کنن و نه برعکس.

اینجا یک تناقضی هست که چطور قراره controller با استفاده از use case می‌خواد با presenter صحبت کنه در حالتی که ما گفتیم use caseها اطلاعی از presenter ندارن پس نمیتونن متدی رو داخل پرزنتز اجرا کنن.

این مشکل رو ما با استفاده از Dependency Inversion Principle یا DIP حل می‌کنیم. توی dip ما با استفاده از یکسری interface اجازه نمیدیم کلاس سطح بالا (دایره داخلی) به طور مستقیم با کلاس سطح پایین صحبت کنه. کلاس high-level باید یکسری interfaceهایی داشته باشه که برای انجام کارهای مختلف اون‌ها رو صدا کنه و بعد کلاس low-level این interfaceها رو implement کنه.

برای مثال تصور کنید use case میخواد presenter رو صدا کنه. میدونیم که نباید این کار رو به طور مستقیم انجام بده چون Dependency Inversion Principle رو نقض می‌کنه. کاری که می‌کنیم اینه که use case برای انجام کارهای مختلف interfaceهایی رو call می‌کنه که پیاده‌سازی این اینترفیس‌ها داخل presenter انجام میشه. تو این حالت وقتی بخوایم در آینده پرزنتر رو تغییر بدیم نیازی نداریم دست به یوز کیس‌ها بزنیم و فقط باید مطمئن شیم اینترفیس‌ها به درستی داخل پرزنتر پیاده‌سازی شدن.

همین تکنیک رو برای تمام مرزها استفاده می‌کنیم و به اینصورت با بهره جستن از dynamic polymorphism  می‌‌تونیم وابستگی‌هامون رو برخلاف flow نرم‌افزار بسازیم و فارغ از اینکه flow of control چه جهتیه قانون وابستگی رو رعایت می‌کنیم.

چه داده‌هایی مرزها رو رد می‌کنن؟

به طور کلی داده‌هایی که می‌تونن بین مرزها جابجا شن ساختمان داده‌های ساده هستن. شما هم میتونید از ساختارهای پایه‌ای برنامه استفاده کنید و هم از Data Transfer Objects (که به اختصار DTO میگن). من خودم از dto استفاده می‌کنم. DTOها ساختارهایی هستن که حین جابجایی بین مرزها مورد استفاده قرار می‌گیرن و بعد از ارسال با استفاده از mapper به ساختارهای قابل استفاده در بخش‌های مختلف تبدیل می‌شن.

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

حواستون باشه که نمیتونیم entityها یا ساختارهایی که مثلا برای database استفاده می‌کنیم رو بین مرزها جابجا کنیم چون این کار قانون وابستگی رو نقض می‌کنه.

برای مثال خروجی دیتابیس برای یک query یک ساختمان داده قابل قبول و خوب میتونه باشه. ما نباید یک instance از این داده رو به متدی در یک دایره داخلی پاس بدیم. این کار قانون وابستگی‌مون رو نقض می‌کنه.

پس هروقت ما یک داده رو بین مرزها جابجا می‌کنیم فرمتِ اون متناسب با دایره داخلی خواهد بود.

نتیجه (چرا حتما باید از clean architecture استفاده کنیم)

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

10 دیدگاه برای “Clean Architecture معماری توسعه نرم‌افزار

  1. برای کسایی که میخوان تازه از معماری های توسعه نرم افزار استفاده کنند کدوم معماری رو پیشنهاد می کنید ؟
    CA به ظاهر جذاب میاد ولی فکر میکنم خیلی دشوار باشه واسه تازه وارد ها.
    و اینکه اگه سورسی سراغ دارید که این از CA استفاده کرده باشه، معرفی کنید، خیلی بهتر میشه درکش کرد.

    تشکر بابت آموزش های حرفه ای تون.

    1. آره اصلا clean برای پروژه‌های کوچیک مناسب هم نیست. بیشتر به درد پروژه‌های enterprise یا حداقل متوسط و بزرگ میخوره. بخصوص وقتی دنبال ساختار modular هستیم. برای شروع بهترین کار استفاده از mvpـه به نظرم. گوگل یکسری چیزای جدید برای mvvm هم معرفی کرده واقعیت خیلی فرصت نکردم برم سراغشون خودم ولی مقالاتی که خوندم خیلی بازخوردهای مثبتی نداشته و هنوز فکر کنم ترکیب mvp و RxJava و dagger بهترین گزینه برای پروژه‌های ساده اندروید باشه.

  2. سلام
    میخواستم بدونم که میشه از فریم ورک spring هم توی اندروید استفاده کرد؟
    به نظر شما خوبه یا نه ؟ منظورم اینه که این معماری Clean Architecture توی اندروید بهتر جواب میده یا Spring ?
    ممنون

  3. سلام. ممنون‌بخاطر مطالب خوبتون. بنظر میرسه با معرفی architecture component توسط گوگل باید رفته رفته با mvp خداحافظی کنیم‌. و mvvm رو‌جایگزین اون بکنیم.

  4. سلام
    ممنون میشم صحبت های تخصصی در مورد ۴ مبحث زیر هم داشته باشید
    clean architecture
    modular patterns
    flux architecture
    rx-Java & rx-Android with clean architecture
    با توجه به این که این ۴ مورد در اپلیکیشن تپسی کاملا استفاده شدند کمک بزرگی میکنید توضیح بدید
    چطوری کنار هم گذاشتن این ۴ مورد میتونه یه پروژه خوب رو تولید کنه

پاسخ دهید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *