برای حل هر مسئلهای به صورت مهندسی، شناخت حوزهای که برای آن راهحل ارائه میشود الزامی است. بدیهی است که بدون شناخت فرایندهای بانکی، مسائل مربوط به آموزش، بیمه و ...، طراحی سامانهای در این حوزهها در عمل غیرممکن خواهد بود. تعامل میان مهندسان نرمافزار و افراد متخصص در این حوزهها برای دستیابی به این هدف الزامی میباشد.
طراحی دامنهمحور یا حوزهمحور (Domain Driven Design) برای پاسخ به نیازمندی بالا، تکنیکها و روشهایی را ارائه میدهد و ارتباطات میان افراد متخصص در یک حوزه و توسعهدهندگان سامانه را تسهیل میکند.
حوزه (Domain) در معنای عام یک مجموعهی دانشی محسوب میشود. وقتی ما در مورد یک حوزه صحبت میکنیم، در واقعیت درباره مجموعهی علوم و اصطلاحات مربوط به یک دامنهی خاص صحبت میکنیم مثلا حوزه کارهای بانکی، حوزه بیمه و موارد دیگر. در زمینهی نرمافزار، حوزه در واقعیت همان کسب و کار یا ایدهای هست که قصد مدلسازی آن را در قالب یک سامانه داریم.
متخصصان در یک حوزه کسانی هستند که تمامی اصطاحات و دانش مورد نیاز در مورد آن حوزه را دارند در حالی که شاید برنامهنویس نباشند. یکی از اهداف طراحی دامنهمحور ساخت یک مدلی است که افراد مختصص حوزه آن را درک کنند. دقت شود این مدل به این معنا نیست که من یک تکه کد را ارائه دهم و از آن به عنوان مدل یاد کنم. بلکه مدل مفهومی انتزاعی و بستری مشترک برای صحبت با متخصصان در آن حوزه است. پیادهسازی نرمافزاری یا نموداری از رفتار مدل، همگی پیادهسازیهای از مدل هستند که باید تا حد امکان نماینده مدل اصلی باشند اما معیار صحبت با متخصصان حوزه نیستند.
ارتباط میان مختصصان یک حوزه و توسعه دهندگان، نیاز به زبان مشترک و فراگیر (Ubiquitous) دارد. دقت شود که با صحبت به متخصصان حوزه به یک زبان مشترک میرسیم و از اصطلاحات آنها در حوزهی تیم توسعه نیز استفاده میکنیم. نباید این مسیر را به صورت برعکس انجام دهیم مثلا بیان مفهوم جدول، سطر و شی و توصیف آنها برای متخصصان و صحبت کردن روی این موارد کار کاملا اشتباهی است. یک مشتری رستوران تنها سفارش، رزرو و غذا را درک میکند!
بررسی هر حوزه دانشی اگر چه در نگاه اول ساده به نظر میآید، اما زمانی که دقیق مورد بررسی قرار میگیرد، حجم زیادی از روابط و موضوعات مشاهده میشود. اگر قرار باشد تمامی این موارد را تحت یک مدل بیان کنیم قطعا خروجی بسیار پیچیده میشود. به همین دلیل ما معمولا دامنه مورد بررسی را به زیردامنههای کوچکتری تقسیم میکنیم. این زیردامنهها از کنار هم قرارگرفتن ایدهها و قواعد مرتبط ایجاد میشوند. هر کدام از این زیر بخشها میتوانند زبان فراگیر خود را داشته باشند.
ممکن است در این تقسیم به زیردامنه، مفاهیم مشترکی را مشاهده کنیم که در چندین زیردامنه وجود داشته باشند، مثل مشتری یا سفارش در رستوران. حتی گاهی این موارد مشترک در نگاه اول کاملا یکسان هستند اگر چه ممکن است در زمان تغییر کنند. در این تجزیه این نکته مهم را باید در نظر گرفت که هرگز تلاش نکنیم این موارد را به صورت مشترک استفاده کنیم. استفاده مشترک تنها توسعههای آینده را محدود میسازد. نکات زیر در مورد این موجودیتهای مهم باید در نظر گرفته شود:
معمولا به این زیردامنهها که میتوانند زبان مشترک خود را داشته باشند، محدوده مفاهیم (bounded context) میگویند. مثلا معنای سفارش در انبار برای دریافت مواد غذایی و سفارش یک مشتری برای خرید غذا کاملا متفاوت هستند. (زیردامنهی سفارش غذا و مدیریت انبار) معمولا این زیردامنههای محدود نقطهی شروع خوبی برای معماری میکروسرویسها هستند.
متاسفانه رویکرد کلی برای تشخیص این محدوده مفاهیم وجود ندارد. اما میتوان با در نظر گرفتن نکات زیر تا حد خوبی زیردامنهها را تشخیص دهیم:
بعد از این که لایههای مختف برنامه تشخیص داده شدند و در قالب محدوده مفاهیم جدا شدند، ما نیاز داریم که ارتباط بین این بخشها را به گونهای فراهیم کنیم که این استقلال برای هر محدوده حفظ شود. به عبارتی گاهی مفاهیم مختلفی در هر محدوده وجود دارند که خاص آن محدوده هستند یا در صورت یک مفهومبودن، یکسری ویژگیها را نداشته یا نیاز به ترجمهشدن برای یک محدودهی دیگر داشته باشند. ایجاد یک ساختار داده جامع (شامل ویژگیهای هر دو محدوده) یا به اشتراکگذاری مفاهیم بین هر دو محدوده، تنها وابستگی محدودهها را افزایش میدهد که خوب نیست.
لایههای جلوگیری از انحراف (Anti-Corruption Layer)، مسئولیت این تبدیلات را بین محدودهی مفاهیم دارند به صورتی که استقلال بین محدودهها حفظ شوند. این لایه به خصوص در سامانههایی دارای بخشهای قدیمی (legacy) بسیار مفید میباشد زیرا جداسازی بخش legacy از دیگر محدودههای سیستم و تنها ارتباطدادن با دیگر بخشها از طریق لایه جلوگیری از انحراف سبب میشود که خلوص و تمیزی دیگر محدودهها حفظ گردد.
در هر حوزهی دانشی، انواع مختلفی از فعالیتها (activities) رخ میدهد که بررسی آنها برای تولید سامانه الزامی میباشد.
دستور (Command) درخواستی به منظور انجام یک هدف میباشد. یک دستور در زمان تعریف هنوز انجام نشده است و به همین دلیل ممکن است پیش از انجام، لغو شود. معمولا دستورات به یک محدوده مشخص ارسال میشوند و سبب تغییر در وضعیت دامنه میشوند مانند اضافهکردن یک مورد به سفارش، پرداخت قبض، آمادهسازی غذا و ...
رخداد (Event) نشاندهندهی عملی هستند که در گذشته انجام شده است. از آنجایی که عمل در گذشته انجام شده است، دیگر قابل انکار یا لغوکردن نیستند. معمولا رخدادها به تعداد زیادی محدوده ارسال میشوند و تغییرات رخداده در درون دامنه را ثبت میکنند مانند موردی که به سفارش اضافه شده است، قبضی که پرداخت شده است و ...
پرس و جو (Query) نشاندهندهی درخواست برای یک اطلاعاتی در دامنه هستند. جواب یک پرس و جو همواره وجود خواهد داشت و پس از اجرای آن، تغییری در وضعیت دامنه رخ نمیدهد. پرس و جو نیز معمولا برای یک محدوده خاص ارسال میشود مثلا گرفتن لیست موارد یک سفارش، بررسی پرداخت یا عدم پرداخت یک قبض.
انواع فعالیتهای ذکرشدن در بالا، همگی در طراحیهای مختلف نشاندهندهی مفهومهای مختلف هستند. مثلا تمامی موارد بالا همان پیامهای سیستمهای واکنشپذیر (reactive) هستند. یا در طراحی محدوده مفاهیم (bounded context) یا میکروسرویس (micro-service) بخش رابط برنامه (API) هستند.
بعد از تشخیص محدودههای مفاهیم و تشخیص فعالیتهای مختلفی که در دامنه وجود دارند، ما نیاز داریم که همین مفاهیم را در سمت کد نیز داشته باشیم. زمانی که ما به این سمت میرویم، دوست داریم به گونهای مفاهیم را در کد مشاهده کنیم که بتوانیم تناظر یک به یک را به سادگی در هنگام استفاده از زبان فراگیر در صحبت با افراد متخصص داشته باشیم. به همین دلیل معمولا تلاش میشود که اگر مثلا دستوری به نام «باز کردن در» داریم، آن را به صورت OpenDoor
یا OpenDoorCommand
در کد قرار دهیم. به این وسیله، به راحتی بتوانیم بین توسعهدهندگان و افراد متخصص زبان مشترک را حفظ کنیم.
در درون هر کدام از این محدوده مفاهیم، یکسری اشیا (objects) وجود دارند که خاص آن محدوده هستند. این اشیا انواع مختلفی دارند که در ادامه با هر کدام آشنا میشویم.
شی از نوع مقدار (Value Object) بر اساس مقدار ویژگیهایش تعریف میشود. به عنوان مثال در بالا «آدرس» یک شی مقدار هست. از آنجایی که هر دو جدول اول و دوم دارای ویژگیهای کاملا یکسانی هستند، هر دو با هم برابر بوده و یک شی مقدار را نشان میدهند. یکی از خاصیتهای مهم شی مقدارها، تغییرناپذیری (immutable) میباشد به صورتی که اگر هر کدام از ویژگیها تغییر کنند، این شی دیگر با شی قبلی برابر نخواهد بود و به شی جدیدی تبدیل میشود. این شیها میتوانند دارای منطقهای کسب و کاری باشند مثلا آدرس دارای یک منطق (یا تابعی) باشد که بر اساس آدرس، مقدار تقریبی طول و عرض جغرافیایی را روی نقشه برگرداند.
هر موجودیت (Entity)، توسط یک ویژگی یکتا (شناسه یا کلید) مشخص میشود. به عنوان مثال در بالا «امین» به عنوان یک موجودیت واحد میباشد. اگر چه ممکن است که «قد» یا «وزن» امین تغییر کند (به دلیل مثلا ورزشکردن!)، ولی موجودیت «امین» ثابت خواهند ماند و این همان فرد است. با این حال موجودیت «امین» با «علیرضا» متفاوت است حتی اگر دارای ویژگیهای کاملا یکسانی باشند. به صورت مشابه هر موجودیت نیز میتواند دارای یکسری منطقهای کسب و کاری برای استخراج یا تغییر ویژگیها باشد.
شی تجمیعی (Aggregate)، نوعی خاص از موجودیت میباشد که در آن مجموعهای از اشیای دامنه تحت یک موجودیت ریشه (root) قرار میگیرند. تمامی اشیا در یک شی تجمیعی تحت یک موجودیت مورد بررسی قرار میگیرند و هر دسترسی به زیربخشها باید از طریق موجودیت ریشه صورت بگیرد.
تشخیص موجودیتهای تجمیعی کار سادهای نیست. گاهی یک موجودیت در یک محدوده، نیاز به چندین موجودیت ریشه دارد. موجودیتهای ریشه الزاما بین محدودهها یکسان نیستند. تعدادی سوال که در تشخیص موجودیتهای تجمیعی به ما کمک میکنند:
در طراحی دامنهمحور، در کنار اشیا و فعالیتهایی که استخراج و طراحی میشوند، معمولا یکسری انتزاعها (abstractions) نیز مشاهده میشود که در بیشتر فعالیتها به ما کمک میکنند. در ادامه با انواع مختلفی از این انتزاعها آشنا میشویم.
اگر چه موجودیتها و اشیا میتوانند دارای منطقهای کسب و کار (business logic) باشند اما گاهی یک منطق خاصی وجود دارد که این منطق به یک موجودیت یا شی خاص مربوط نمیشود. (به عبارتی به هر دلیلی نمیتواند موجودیتی را انتخاب کنید که برای این فرآیند منطقی به نظر بیاید) در چنین مواردی این منطق میتواند در قالب یک سرویس پنهان (encapsulate) شود. سرویسها باید بدون وضعیت (stateless) باشند. اگر نیاز به یک وضعیت دارید، احتمالا منطق مربوطه یک موجودیت یا مقدار میباشد.
معمولا سرویسها خود در قالب یک انتزاع (مشخصنمودن رفتار) بیان میشوند و سپس به شکلهای مختلفی پیادهسازی (concrete implementation) میشوند. این سبب میشود که تغییر پیادهسازی راحت باشد و از طرفی استفادهکننده تنها رفتارهای موردنیاز را استفاده میکند در حالی که نگران نحوهی پیادهسازی نیست.
معمولا تعداد سرویسهای درون یک سامانه نباید بسیار باشند. اگر به چنین وضعیتی برسیم، احتمالا در تعریف برخی موارد زیادهروی کردهایم. همچنین یک سرویس باید یک لایهی سبک روی یک فرایند یا منطق باشد و نباید به گونهای باشد که انگار یک آچار فرانسه همه کاره است!
گاهی وقتی در هنگام ایجاد موجودیتها (مخصوصا موجودیتهای تجمیعی) منطق لازم برای ساخت این موجودیتها بدیهی نیست و باید نکات مختلفی را در نظر گرفت. مثلا ممکن است برای ایجاد موجودیتی لازم باشد اطلاعات آن ابتدا توسط سرویسی خارجی بررسی شود و سپس شناسهی یکتا برای آن تولید شود و در انتها در یک مخزن داده خارجی ذخیره شود. به همین دلیل ایجاد یک شی میتواند چالشهای زیادی داشته باشد و یک کارخانه (Factory) میتواند این پیچیدگیهای فراوان را پنهان نموده و استفاده از موجودیت را برای ما ساده کند. معمولا در پیادهسازی کارخانه مشابه سرویس از روش انتزاع استفاده میشود و پیادهسازی مختلف برای آن ارائه میشوند.
در مثال بالا مثلا ایجاد یک رزرو و پیچیدگیهای آن در قالب یک انتزاع سادهسازی شده است و مفاهیمی مثل پایگاهداده Cassandra یا نحوهی ایجاد شناسهی یکتا پنهان شده است. اگر با مفهوم CRUD آشنا باشید، یک کارخانه معادل بخش C یا Create میباشد.
مخزنها (Repository) خیلی مشابه کارخانهها هستند با این تفاوت که بخش دریافت اطلاعات یا ویرایش موجودیتهای پیچیده را برای ما به صورت انتزاعی فراهم میکنند. به عبارتی اگر با مفهوم CRUD آشنا باشید، در این صورت مخزنها کارهای RUD (یعنی Read و Update و Delete) را انجام میدهند. معمولا به صورت مشابه مخزنها به صورت انتزاعی نوشته میشوند و پیادهسازیهای مختلف برای آنها فراهم میشود.
همانطور که مشخص است مفاهیم مخزن و کارخانه بسیار به هم نزدیک هستند، به همین دلیل در مواردی این دو مفهوم با یکدیگر ترکیب میشوند و تحت عنوان مخزن کلیهی ویژگیهای CRUD را فراهم میکنند.
مفاهیم طراحی دامنهمحور که در این قسمت آنها را بیان کردیم، صرفا برای آشنایی اولیه با این نوع رویکرد طراحی و معماری بود. به عبارتی همانطور که مشاهده کردید، تلاش این نوع طراحی و معماری این بود که تا جای ممکن زبان مشترکی را بین توسعهدهندگان و افراد متخصص ایجاد کند و از طرفی تلاش میکرد که نحوهی پیادهسازی برنامه را به گونهای انجام دهد که مفاهیم و محدودهها به خوبی از همدیگر تفکیکشده و برای توسعه سامانه مشکلی نداشته باشیم. یکی از مزیتهای این روش طراحی این است که مفاهیم آن و نحوهی قسمتبندی بخشهای مختلف آن، در رویکرهایی مثل معماری واکنشگرا (Reactive Architecture) و میکروسرویس (Micro-services) مفید خواهد بود.
این مطلب، بخشی از تمرینهای درس معماری نرمافزار در دانشگاه شهیدبهشتی است.