فصل ۱۱ — تکامل تصمیمهای طراحی
در دنیای مدرن و پرشتابی که در آن زندگی میکنیم، شرکتها نمیتوانند کند و بیتحرک باشند. برای همگام ماندن با رقابت، باید بهطور پیوسته تغییر کنند، تکامل یابند و حتی در گذر زمان خود را از نو بسازند. هنگام طراحی سیستمها نمیتوان این واقعیت را نادیده گرفت، بهویژه اگر قصد داریم نرمافزاری طراحی کنیم که بهخوبی با الزامات دامنهٔ کسبوکار خود سازگار باشد. وقتی تغییرها درست مدیریت نشوند، حتی پیچیدهترین و سنجیدهترین طراحی نیز سرانجام به کابوسی برای نگهداری و تکامل تبدیل میشود. این فصل بررسی میکند که تغییرهای محیط یک پروژهٔ نرمافزاری چگونه میتوانند تصمیمهای طراحی را تحت تأثیر قرار دهند و طراحی چگونه باید متناسب با آنها تکامل یابد. چهار بردار رایج تغییر را بررسی خواهیم کرد: دامنهٔ کسبوکار، ساختار سازمانی، دانش دامنه و رشد.
تغییر در دامنهها
در فصل ۲ با سه نوع دامنهٔ فرعی کسبوکار و تفاوتهای آنها آشنا شدید:
- هستهای (Core)
- فعالیتهایی که شرکت برای بهدست آوردن مزیت رقابتی، متفاوت از رقیبان خود انجام میدهد.
- پشتیبان (Supporting)
- کارهایی که شرکت متفاوت از رقیبان انجام میدهد، اما مزیت رقابتی مستقیم ایجاد نمیکنند.
- عمومی (Generic)
- کارهایی که همهٔ شرکتها به شیوهای یکسان انجام میدهند.
در فصلهای قبل دیدید که نوع دامنهٔ فرعی بر تصمیمهای طراحی راهبردی و تاکتیکی اثر میگذارد: چگونه مرزهای زمینههای محدود را طراحی کنیم، چگونه یکپارچهسازی میان زمینهها را سازمان دهیم و برای سازگار شدن با پیچیدگی منطق کسبوکار از چه الگوهایی استفاده کنیم.
برای طراحی نرمافزاری که واقعاً از نیازهای دامنهٔ کسبوکار هدایت شود، شناسایی دامنههای فرعی و انواع آنها حیاتی است. با این حال، این تمام ماجرا نیست. به همان اندازه مهم است که نسبت به تکامل دامنههای فرعی هوشیار باشیم. با رشد و تغییر یک سازمان، غیرعادی نیست که بعضی از دامنههای فرعی آن از یک نوع به نوع دیگری تبدیل شوند. چند نمونه از چنین تغییرهایی را بررسی کنیم.
هستهای به عمومی (Core to Generic)
فرض کنید شرکت فروش آنلاین BuyIT راهحل اختصاصی خود را برای تحویل سفارشها پیاده کرده است. این شرکت الگوریتم نوآورانهای برای بهینهسازی مسیر پیکهای خود توسعه داده و به همین دلیل میتواند هزینهٔ تحویل کمتری از رقیبان دریافت کند.
یک روز شرکت دیگری به نام DeliverIT صنعت تحویل را دگرگون میکند. این شرکت ادعا میکند مسئلهٔ «فروشندهٔ دورهگرد» را حل کرده و بهینهسازی مسیر را بهعنوان سرویس عرضه میکند. نهتنها بهینهسازی DeliverIT پیشرفتهتر است، بلکه سرویس آن با کسری از هزینهای ارائه میشود که BuyIT برای انجام همان کار میپردازد.
از دید BuyIT، به محض آنکه راهحل DeliverIT بهعنوان یک محصول آماده در دسترس قرار میگیرد، دامنهٔ هستهای بهینهسازی مسیر به یک دامنهٔ عمومی تبدیل میشود. در نتیجه، راهحل بهینه در دسترس همهٔ رقیبان BuyIT نیز قرار میگیرد. بدون سرمایهگذاری سنگین در تحقیق و توسعه، BuyIT دیگر نمیتواند از این دامنهٔ فرعی مزیت رقابتی به دست آورد. چیزی که پیشتر برای BuyIT یک مزیت رقابتی بود، اکنون به کالایی تبدیل شده است که همهٔ رقیبان میتوانند تهیه کنند.
عمومی به هستهای (Generic to Core)
از زمان تأسیس، BuyIT برای مدیریت موجودی خود از یک راهحل آماده استفاده کرده است. با این حال، گزارشهای هوش تجاری بهطور مستمر پیشبینیهای ناکافی از تقاضای مشتریان را نشان میدهند. در نتیجه، BuyIT در تجدید موجودی محبوبترین کالاها شکست میخورد و فضای انبار را برای محصولات کمطرفدار هدر میدهد.
پس از ارزیابی چند راهحل جایگزین مدیریت موجودی، تیم مدیریت BuyIT تصمیم راهبردی میگیرد روی طراحی و ساخت یک سیستم داخلی سرمایهگذاری کند. این راهحل داخلی ظرافتهای محصولات فروختهشده توسط BuyIT را در نظر میگیرد و پیشبینی بهتری از تقاضای مشتریان ارائه میدهد.
تصمیم BuyIT برای جایگزین کردن راهحل آماده با پیادهسازی اختصاصی، مدیریت موجودی را از یک دامنهٔ عمومی به یک دامنهٔ هستهای تبدیل میکند: اگر این قابلیت با موفقیت پیاده شود، BuyIT مزیت رقابتی بیشتری نسبت به رقبا خواهد داشت؛ رقیبان همچنان به راهحل عمومی وابسته میمانند و نمیتوانند از الگوریتمهای پیشرفتهٔ پیشبینی تقاضایی که BuyIT ابداع و توسعه داده است استفاده کنند.
یک مثال واقعی و کلاسیک از شرکتی که یک دامنهٔ عمومی را به دامنهٔ هستهای تبدیل کرده، Amazon است. مانند هر ارائهدهندهٔ سرویس دیگری، Amazon به زیرساختی برای اجرای سرویسهای خود نیاز داشت. شرکت توانست شیوهٔ مدیریت زیرساخت فیزیکی خود را «از نو اختراع» کند و بعدتر آن را به یک کسبوکار سودآور، یعنی Amazon Web Services، تبدیل کند.
پشتیبان به عمومی (Supporting to Generic)
دپارتمان بازاریابی BuyIT سیستمی را برای مدیریت فروشندگانی که با آنها کار میکند و قراردادهایشان پیادهسازی کرده است. چیز خاص یا پیچیدهای در این سیستم وجود ندارد؛ صرفاً چند رابط کاربری CRUD برای ورود داده است. به عبارت دیگر، نمونهای معمول از یک دامنهٔ فرعی پشتیبان است.
چند سال پس از آنکه BuyIT راهحل داخلی را ساخت، یک راهحل متنباز برای مدیریت قراردادها منتشر میشود. پروژهٔ متنباز همان قابلیتهای راهحل موجود را پیادهسازی میکند و حتی قابلیتهای پیشرفتهتری مانند OCR و جستوجوی تماممتن دارد. این قابلیتهای اضافه مدتها در Backlog شرکت بودهاند، اما به دلیل اثر تجاری اندک هرگز در اولویت قرار نگرفتهاند.
در نتیجه، شرکت تصمیم میگیرد راهحل داخلی را کنار بگذارد و محصول متنباز را یکپارچه کند. با این تصمیم، دامنهٔ مدیریت اسناد از یک دامنهٔ پشتیبان به یک دامنهٔ عمومی تبدیل میشود.
پشتیبان به هستهای (Supporting to Core)
یک دامنهٔ پشتیبان همچنین میتواند به دامنهٔ هستهای تبدیل شود؛ برای مثال وقتی شرکت راهی پیدا میکند که منطق پشتیبان را چنان بهینه کند که هزینهها کاهش یابند یا سود بیشتری ایجاد شود.
نشانهٔ معمول این تحول، افزایش پیچیدگی منطق کسبوکار دامنهٔ پشتیبان است. دامنههای پشتیبان بنا بر تعریف سادهاند و عمدتاً به رابطهای CRUD یا فرایندهای ETL شباهت دارند. اما اگر منطق کسبوکار با گذر زمان پیچیدهتر شود، باید دلیلی برای این پیچیدگی اضافه وجود داشته باشد. اگر این پیچیدگی بر سود شرکت اثر نمیگذارد، با «پیچیدگی تصادفی کسبوکار» روبهرو هستیم. از سوی دیگر، اگر پیچیدگی جدید سودآوری شرکت را بهبود میدهد، این نشانهای از تبدیل یک دامنهٔ پشتیبان به دامنهٔ هستهای است.
هستهای به پشتیبان (Core to Supporting)
یک دامنهٔ هستهای ممکن است با گذر زمان به دامنهٔ پشتیبان تبدیل شود. این اتفاق زمانی رخ میدهد که پیچیدگی دامنه دیگر توجیه اقتصادی نداشته باشد؛ به بیان دیگر، سودآور نباشد. در چنین حالتی سازمان ممکن است تصمیم بگیرد پیچیدگی اضافی را حذف کند و فقط حداقل منطق لازم برای پشتیبانی از پیادهسازی دامنههای دیگر را نگه دارد.
عمومی به پشتیبان (Generic to Supporting)
در نهایت، به همان دلیلی که یک دامنهٔ هستهای میتواند به پشتیبان تبدیل شود، دامنهٔ عمومی نیز میتواند به پشتیبان تبدیل گردد. دوباره به مثال سیستم مدیریت اسناد BuyIT برگردیم. فرض کنید شرکت به این نتیجه برسد که پیچیدگی یکپارچهسازی راهحل متنباز در برابر منافع آن توجیه ندارد و به سیستم داخلی بازگردد. در نتیجه، دامنهٔ عمومی به دامنهٔ پشتیبان تبدیل میشود.
این تغییرهای انواع دامنههای فرعی در شکل ۱۱-۱ منبع نمایش داده شدهاند.
ملاحظات طراحی راهبردی
تغییر در نوع یک دامنهٔ فرعی مستقیماً بر زمینهٔ محدود آن و، در نتیجه، بر تصمیمهای طراحی راهبردی متناظر اثر میگذارد. همانطور که در فصل ۴ آموختید، الگوهای متفاوت یکپارچهسازی زمینههای محدود برای انواع متفاوت دامنههای فرعی مناسباند. دامنههای هستهای باید مدل خود را با لایهٔ ضدفساد (Anticorruption Layer) محافظت کنند و مصرفکنندگان را در برابر تغییرهای مکرر مدل پیادهسازی، با استفاده از زبان منتشرشده در Open-Host Service، مصون نگه دارند.
الگوی دیگری که از این تغییرها اثر میپذیرد Separate Ways است. پیشتر دیدید که تیمها میتوانند این الگو را برای دامنههای پشتیبان و عمومی به کار ببرند. اگر دامنه به هستهای تبدیل شود، تکرار قابلیت آن توسط چند تیم دیگر پذیرفتنی نیست. در نتیجه تیمها چارهای جز یکپارچه کردن پیادهسازیهایشان ندارند. در این وضعیت، رابطهٔ Customer–Supplier منطقیتر است، زیرا دامنهٔ هستهای تنها توسط یک تیم پیادهسازی خواهد شد.
از دید راهبرد پیادهسازی، دامنههای هستهای و پشتیبان نیز با هم تفاوت دارند. دامنههای پشتیبان را میتوان برونسپاری کرد یا بهعنوان «چرخهای کمکی» برای نیروهای تازهاستخدامشده به کار گرفت. دامنههای هستهای باید درون سازمان و تا جای ممکن نزدیک به منابع دانش دامنه پیادهسازی شوند. بنابراین وقتی دامنهٔ پشتیبان به هستهای تبدیل میشود، پیادهسازی آن باید به داخل سازمان منتقل شود. منطق معکوس نیز برقرار است: اگر دامنهٔ هستهای به پشتیبان تبدیل شود، میتوان پیادهسازی آن را برونسپاری کرد تا تیمهای R&D داخلی روی دامنههای هستهای تمرکز کنند.
ملاحظات طراحی تاکتیکی
شاخص اصلی تغییر در نوع دامنهٔ فرعی، ناتوانی طراحی فنی موجود در پشتیبانی از نیازهای فعلی کسبوکار است.
به مثال تبدیل یک دامنهٔ پشتیبان به دامنهٔ هستهای برگردیم. دامنههای پشتیبان با الگوهای نسبتاً ساده برای مدلسازی منطق کسبوکار پیادهسازی میشوند؛ یعنی Transaction Script یا Active Record. همانطور که در فصل ۵ دیدید، این الگوها برای منطق کسبوکاری که شامل قواعد و ناورداییهای پیچیده است مناسب نیستند.
اگر قواعد و ناورداییهای پیچیده به مرور زمان به منطق کسبوکار اضافه شوند، Codebase نیز بهطور فزایندهای پیچیده میشود. افزودن قابلیت جدید دردناک خواهد شد، زیرا طراحی موجود برای سطح جدید پیچیدگی ساخته نشده است. این «درد» یک سیگنال مهم است: آن را نشانهای برای ارزیابی دوبارهٔ دامنهٔ کسبوکار و انتخابهای طراحی بدانید.
نیاز به تغییر در راهبرد پیادهسازی چیزی نیست که از آن بترسیم؛ طبیعی است. نمیتوانیم پیشبینی کنیم یک کسبوکار در آینده چگونه تکامل مییابد. همچنین نمیتوانیم پیچیدهترین الگوهای طراحی را برای تمام انواع دامنههای فرعی به کار ببریم؛ چنین کاری اتلاف منابع و ناکارآمد خواهد بود. باید مناسبترین طراحی را انتخاب کنیم و هر زمان نیاز بود آن را تکامل دهیم.
اگر تصمیم دربارهٔ مدلسازی منطق کسبوکار آگاهانه گرفته شده باشد و همهٔ گزینههای طراحی و تفاوتهای آنها را بشناسید، مهاجرت از یک الگو به الگوی دیگر چندان دشوار نیست. در ادامه چند نمونه را بررسی میکنیم.
Transaction Script به Active Record
در هستهٔ خود، Transaction Script و Active Record هر دو بر یک اصل متکیاند: منطق کسبوکار به شکل یک اسکریپت رویهای پیاده میشود. تفاوت آنها در مدلسازی ساختارهای داده است: Active Record ساختارهای داده را معرفی میکند تا پیچیدگی نگاشت آنها به مکانیزم ذخیرهسازی را کپسوله کند.
بنابراین وقتی کار با داده در Transaction Script دشوار میشود، آن را به Active Record بازآرایی کنید. به دنبال ساختارهای دادهٔ پیچیده بگردید و آنها را در اشیای Active Record کپسوله کنید. بهجای دسترسی مستقیم به پایگاه داده، از Active Recordها استفاده کنید تا مدل و ساختار ذخیرهساز را انتزاع کنند.
Active Record به Domain Model
اگر منطق کسبوکاری که Active Recordها را دستکاری میکند پیچیده شود و موارد ناسازگاری و تکرار بیشتر شوند، پیادهسازی را به الگوی Domain Model بازآرایی کنید.
با شناسایی Value Objectها شروع کنید. کدام ساختارهای داده را میتوان بهصورت اشیای تغییرناپذیر مدل کرد؟ منطق کسبوکار مرتبط با آنها را نیز پیدا کنید و آن را بخشی از Value Objectها قرار دهید.
سپس ساختارهای داده را تحلیل کنید و به دنبال مرزهای تراکنشی بگردید. برای آنکه همهٔ منطق تغییر وضعیت صریح شود، Setterهای تمام Active Recordها را private کنید تا فقط از داخل خود Active Record قابل تغییر باشند. طبیعی است که در این مرحله انتظار داشته باشید Compilation شکست بخورد؛ اما خطاهای کامپایل دقیقاً مشخص میکنند منطق تغییر وضعیت کجا قرار دارد. این منطق را به داخل مرزهای Active Record بازآرایی کنید.
برای مثال، پیادهسازی اولیه:
public class Player
{
public Guid Id { get; set; }
public int Points { get; set; }
}
public class ApplyBonus
{
...
public void Execute(Guid playerId, byte percentage)
{
var player = _repository.Load(playerId);
player.Points *= 1 + percentage/100.0;
_repository.Save(player);
}
}
در کد زیر نخستین گامهای تبدیل را میبینید. کد هنوز Compile نمیشود، اما خطاها آشکار میکنند کدام مؤلفههای بیرونی وضعیت شیء را کنترل میکنند:
public class Player
{
public Guid Id { get; private set; }
public int Points { get; private set; }
}
public class ApplyBonus
{
...
public void Execute(Guid playerId, byte percentage)
{
var player = _repository.Load(playerId);
player.Points *= 1 + percentage/100.0;
_repository.Save(player);
}
}
در تکرار بعدی میتوانیم منطق را به داخل مرز Active Record منتقل کنیم:
public class Player
{
public Guid Id { get; private set; }
public int Points { get; private set; }
public void ApplyBonus(int percentage)
{
this.Points *= 1 + percentage/100.0;
}
}
وقتی همهٔ منطق کسبوکاری که وضعیت را تغییر میدهد به داخل مرز اشیای متناظر منتقل شد، بررسی کنید برای تضمین سازگاری قوی قواعد و ناورداییهای کسبوکار چه سلسلهمراتبهایی لازماند. اینها نامزدهای خوبی برای Aggregate هستند. با در نظر گرفتن اصول طراحی Aggregate که در فصل ۶ مطرح شد، به دنبال کوچکترین مرزهای تراکنشی باشید؛ یعنی کمترین مقدار دادهای که باید توسط دامنهٔ کسبوکار بهصورت strongly consistent نگه داشته شود. سلسلهمراتب را در امتداد این مرزها تجزیه کنید. مطمئن شوید Aggregateهای بیرونی فقط با شناسهشان ارجاع داده میشوند.
در نهایت، برای هر Aggregate ریشهٔ آن یا نقطهٔ ورود به رابط عمومی را مشخص کنید. متدهای همهٔ اشیای داخلی دیگر Aggregate را private کنید تا فقط از داخل خود Aggregate قابل فراخوانی باشند.
Domain Model به Event-Sourced Domain Model
وقتی یک Domain Model با مرزهای Aggregate درست طراحی شده دارید، میتوانید آن را به مدل رویدادمنبع تبدیل کنید. بهجای تغییر مستقیم دادهٔ Aggregate، Domain Eventهایی را مدل کنید که برای نمایش چرخهٔ عمر Aggregate لازماند.
چالشبرانگیزترین بخش بازآرایی Domain Model به Event-Sourced Domain Model، تاریخچهٔ Aggregateهای موجود است: باید وضعیت «بیزمان» را به مدل مبتنی بر رویداد مهاجرت دهید. چون دادهٔ ریزدانهای که همهٔ تغییرهای گذشتهٔ وضعیت را نشان دهد وجود ندارد، یا باید رویدادهای گذشته را بهصورت best-effort تولید کنید، یا رویدادهای مهاجرت را مدل کنید.
تولید گذارهای گذشته (Generating Past Transitions)
این رویکرد شامل تولید یک جریان تقریبی از رویدادها برای هر Aggregate است، به شکلی که Projection آن جریان همان نمایش وضعیت پیادهسازی اصلی را ایجاد کند. مثال فصل ۷ را در نظر بگیرید که در جدول ۱۱-۱ نمایش داده شده است.
جدول ۱۱-۱ — نمایش حالتمحور دادهٔ Aggregate| lead-id | first-name | last-name | phone_number | status | last-contacted-on | order-placed-on | converted-on | followup-on |
|---|
| 12 | Shauna | Mercia | 555-4753 | converted | 2020-05-27T12:02:12.51Z | 2020-05-27T12:02:12.51Z | 2020-05-27T12:38:44.12Z | null |
از منظر منطق کسبوکار میتوانیم فرض کنیم نمونهٔ Aggregate ابتدا ایجاد شده، سپس با شخص تماس گرفته شده، سفارشی ثبت شده و در نهایت، چون وضعیت converted است، پرداخت سفارش تأیید شده است. مجموعهٔ رویدادهای زیر میتواند این فرضها را نمایش دهد:
{
"lead-id": 12,
"event-id": 0,
"event-type": "lead-initialized",
"first-name": "Shauna",
"last-name": "Mercia",
"phone-number": "555-4753"
},
{
"lead-id": 12,
"event-id": 1,
"event-type": "contacted",
"timestamp": "2020-05-27T12:02:12.51Z"
},
{
"lead-id": 12,
"event-id": 2,
"event-type": "order-submitted",
"payment-deadline": "2020-05-30T12:02:12.51Z",
"timestamp": "2020-05-27T12:02:12.51Z"
},
{
"lead-id": 12,
"event-id": 3,
"event-type": "payment-confirmed",
"status": "converted",
"timestamp": "2020-05-27T12:38:44.12Z"
}
اگر این رویدادها یکییکی اعمال شوند، میتوان آنها را به همان نمایش وضعیت دقیق سیستم اصلی Project کرد. رویدادهای «بازیابیشده» را نیز میتوان بهآسانی آزمود: وضعیت حاصل از Projection را با دادهٔ اصلی مقایسه کنید.
با این حال، باید عیب مهم این رویکرد را در نظر داشت. هدف Event Sourcing داشتن تاریخچهای قابل اعتماد و strongly consistent از Domain Eventهای Aggregate است. در این روش بازیابی کامل تاریخچهٔ گذارهای وضعیت ناممکن است. برای مثال، نمیدانیم Sales Agent چند بار با شخص تماس گرفته و در نتیجه چند رویداد contacted از دست رفتهاند.
مدلسازی رویدادهای مهاجرت (Modeling Migration Events)
رویکرد جایگزین این است که نبود دانش دربارهٔ رویدادهای گذشته را بپذیریم و آن را صریحاً بهصورت یک Event مدل کنیم. بهجای بازسازی رویدادهایی که احتمالاً به وضعیت فعلی منجر شدهاند، یک Migration Event تعریف کنید و از آن برای مقداردهی اولیهٔ Event Stream نمونههای موجود Aggregate استفاده کنید:
{
"lead-id": 12,
"event-id": 0,
"event-type": "migrated-from-legacy",
"first-name": "Shauna",
"last-name": "Mercia",
"phone-number": "555-4753",
"status": "converted",
"last-contacted-on": "2020-05-27T12:02:12.51Z",
"order-placed-on": "2020-05-27T12:02:12.51Z",
"converted-on": "2020-05-27T12:38:44.12Z",
"followup-on": null
}
مزیت این رویکرد آن است که فقدان دادهٔ گذشته را صریح میکند. در هیچ مرحلهای کسی نمیتواند اشتباهاً فرض کند Event Stream همهٔ Domain Eventهایی را که در چرخهٔ عمر Aggregate رخ دادهاند ثبت کرده است. عیب آن این است که ردپای سیستم Legacy برای همیشه در Event Store باقی میماند. برای مثال، اگر از CQRS استفاده میکنید—که در Event-Sourced Domain Model به احتمال زیاد چنین است—Projectionها همیشه باید Migration Eventها را در نظر بگیرند.
تغییرهای سازمانی
نوع دیگری از تغییر که میتواند بر طراحی سیستم اثر بگذارد، تغییر در خود سازمان است. فصل ۴ الگوهای مختلف یکپارچهسازی زمینههای محدود را بررسی کرد: Partnership، Shared Kernel، Conformist، Anticorruption Layer، Open-Host Service و Separate Ways. تغییر در ساختار سازمانی میتواند سطح ارتباط و همکاری تیمها را تغییر دهد و در نتیجه، شیوهٔ یکپارچهسازی زمینههای محدود نیز باید تغییر کند.
مثال ساده، رشد مراکز توسعه است که در شکل ۱۱-۲ نمایش داده شده است. چون یک Bounded Context فقط میتواند توسط یک تیم پیادهسازی شود، افزودن تیمهای توسعهٔ جدید ممکن است باعث شود مرزهای پهن زمینهٔ محدود به مرزهای کوچکتر شکسته شوند تا هر تیم روی Bounded Context خودش کار کند.
علاوه بر این، مراکز توسعهٔ سازمان اغلب در موقعیتهای جغرافیایی متفاوت قرار دارند. وقتی کار یک Bounded Context موجود به مکانی دیگر منتقل میشود، همکاری تیمها ممکن است آسیب ببیند. در نتیجه، الگوهای یکپارچهسازی Bounded Contextها باید متناسب با شرایط جدید تکامل یابند.
Partnership به Customer–Supplier
الگوی Partnership فرض میکند ارتباط و همکاری قوی میان تیمها وجود دارد. با گذشت زمان ممکن است این وضعیت از بین برود؛ برای مثال وقتی کار یکی از Bounded Contextها به یک مرکز توسعهٔ دوردست منتقل میشود. چنین تغییری ارتباط تیمها را تضعیف میکند و در این شرایط منطقی است از Partnership به رابطهٔ Customer–Supplier حرکت کنیم.
Customer–Supplier به Separate Ways
متأسفانه مشکلات شدید ارتباطی میان تیمها غیرعادی نیست. این مشکلات ممکن است از فاصلهٔ جغرافیایی یا سیاستهای سازمانی ناشی شوند. تیمها ممکن است با گذر زمان مشکلات یکپارچهسازی بیشتری تجربه کنند. در نقطهای، تکرار قابلیت ممکن است از ادامهٔ هزینهٔ هماهنگی و همکاری مقرونبهصرفهتر شود و تیمها به Separate Ways بروند.
دانش دامنه
همانطور که به یاد دارید، اصل محوری طراحی دامنهمحور این است که دانش دامنه برای طراحی یک سیستم نرمافزاری موفق ضروری است. کسب دانش دامنه یکی از دشوارترین جنبههای مهندسی نرمافزار است، بهویژه برای دامنههای هستهای. منطق یک دامنهٔ هستهای نهتنها پیچیده است، بلکه انتظار میرود مرتب تغییر کند. افزون بر این، مدلسازی فرایندی مستمر است؛ مدلها باید با افزایش دانش دربارهٔ دامنهٔ کسبوکار بهتر شوند.
در بسیاری از موارد، پیچیدگی دامنهٔ کسبوکار ضمنی است. در ابتدا همهچیز ساده و سرراست به نظر میرسد. این سادگی اولیه اغلب فریبدهنده است و بهسرعت به پیچیدگی تبدیل میشود. با افزوده شدن قابلیتهای بیشتر، Edge Caseها، ناورداییها و قواعد بیشتری کشف میشوند. چنین بینشهایی گاهی مخرباند و نیاز به بازسازی مدل از پایه دارند، از جمله بازنگری مرزهای Bounded Contextها، Aggregateها و سایر جزئیات پیادهسازی.
از دید طراحی راهبردی، یک قاعدهٔ اکتشافی مفید این است که مرزهای Bounded Context را بر اساس سطح دانش دامنه طراحی کنید. هزینهٔ تجزیهٔ یک سیستم به Bounded Contextهایی که بعداً نادرست از آب در میآیند میتواند بالا باشد. بنابراین وقتی منطق دامنه نامشخص است و زیاد تغییر میکند، منطقی است مرزهای زمینههای محدود را پهنتر طراحی کنید. سپس با کشف دانش بیشتر و پایدار شدن تغییرهای منطق کسبوکار، آن زمینههای پهن را میتوان به زمینههای باریکتر یا Microserviceها تجزیه کرد. تعامل میان Bounded Context و Microservice در فصل ۱۴ بررسی میشود.
وقتی دانش دامنهٔ جدید کشف میشود، باید از آن برای تکامل طراحی و مقاومتر کردن آن استفاده کرد. متأسفانه تغییر در دانش دامنه همیشه مثبت نیست؛ دانش دامنه میتواند از دست برود. با گذشت زمان، مستندات کهنه میشوند، افرادی که روی طراحی اصلی کار کردهاند شرکت را ترک میکنند و قابلیتهای جدید بهصورت موردی اضافه میشوند تا در نهایت Codebase به جایگاه نامطلوب «سیستم Legacy» برسد. جلوگیری فعالانه از این فرسایش دانش حیاتی است. ابزار مؤثر برای بازیابی دانش دامنه، کارگاه EventStorming است که موضوع فصل بعد خواهد بود.
رشد
رشد نشانهٔ یک سیستم سالم است. وقتی قابلیتهای جدید بهطور پیوسته اضافه میشوند، یعنی سیستم موفق است: برای کاربرانش ارزش ایجاد میکند و برای پاسخ بهتر به نیازهای آنان و رقابت با محصولات دیگر گسترش مییابد. اما رشد روی تاریکی هم دارد. با رشد پروژهٔ نرمافزاری، Codebase ممکن است به «گلولهٔ بزرگ گل» (Big Ball of Mud) تبدیل شود:
یک Big Ball of Mud سامانهای است با ساختار درهموبرهم، گسترده، شلخته، وصلهپینهای و شبیه جنگلی از Spaghetti Code. این سیستمها نشانههای آشکار رشد بیقاعده و تعمیرهای مکرر و مصلحتی را نشان میدهند.
— Brian Foote و Joseph Yoder
رشد کنترلنشدهای که به Big Ball of Mud منجر میشود نتیجهٔ گسترش قابلیت سیستم بدون ارزیابی دوبارهٔ تصمیمهای طراحی است. رشد، مرزهای مؤلفهها را باد میکند و بهتدریج دامنهٔ مسئولیت آنها را افزایش میدهد. بررسی اثر رشد بر تصمیمهای طراحی حیاتی است، بهویژه چون بسیاری از ابزارهای DDD اساساً دربارهٔ تعیین مرزند: اجزای کسبوکار (Subdomain)، مدل (Bounded Context)، تغییرناپذیری (Value Object) و سازگاری (Aggregate).
اصل راهنما برای مقابله با پیچیدگی ناشی از رشد این است که پیچیدگی تصادفی—یعنی پیچیدگی ناشی از تصمیمهای طراحی منسوخشده—شناسایی و حذف شود. پیچیدگی ذاتی یا ضروری دامنهٔ کسبوکار باید با ابزارها و شیوههای DDD مدیریت شود.
در فصلهای قبل روندی را دنبال کردیم: نخست دامنهٔ کسبوکار و اجزای راهبردی آن را تحلیل کردیم، سپس مدلهای مناسب را طراحی کردیم و در نهایت آن مدلها را در کد پیادهسازی کردیم. برای پیچیدگی ناشی از رشد نیز همین الگو را دنبال میکنیم.
Subdomainها
همانطور که در فصل ۱ گفته شد، شناسایی مرزهای Subdomain میتواند دشوار باشد. بنابراین بهجای تلاش برای مرزهای «کامل»، باید به دنبال مرزهای «مفید» باشیم؛ یعنی مرزهایی که اجازه دهند مؤلفههای دارای ارزش تجاری متفاوت را شناسایی کنیم و برای هرکدام ابزار مناسب طراحی و پیادهسازی را به کار بگیریم.
با رشد دامنهٔ کسبوکار، مرزهای Subdomain ممکن است مبهمتر شوند و تشخیص اینکه یک Subdomain در واقع چند Subdomain ریزتر را در خود دارد دشوارتر گردد. به همین دلیل باید Subdomainهای شناساییشده را بازبینی کنید و با قاعدهٔ اکتشافی Use Caseهای همبسته—مجموعه Use Caseهایی که روی مجموعه دادهٔ یکسان کار میکنند—محل مناسب برای شکستن یک Subdomain را بیابید. این موضوع در شکل ۱۱-۳ نشان داده شده است.
اگر بتوانید Subdomainهای ریزتری از انواع متفاوت شناسایی کنید، بینش مهمی برای مدیریت پیچیدگی ذاتی دامنه به دست آوردهاید. هرچه اطلاعات دربارهٔ Subdomainها و نوع آنها دقیقتر باشد، در انتخاب راهحل فنی مناسب برای هرکدام موفقتر خواهید بود.
استخراج Subdomainهای داخلی و صریح کردن آنها بهویژه برای Core Subdomainها مهم است. باید تا جای ممکن Core Subdomain را از بقیه Distill کنیم تا تلاش خود را جایی سرمایهگذاری کنیم که از منظر راهبرد کسبوکار بیشترین اهمیت را دارد.
Bounded Contextها
در فصل ۳ آموختید که Bounded Context اجازه میدهد از چند مدل متفاوت از دامنهٔ کسبوکار استفاده کنیم. بهجای ساخت یک مدل «همهکاره و در هیچچیز استاد نیست»، میتوانیم چند مدل بسازیم که هرکدام بر حل یک مسئلهٔ مشخص متمرکز باشند.
با تکامل و رشد پروژه، غیرعادی نیست که Bounded Contextها تمرکز خود را از دست بدهند و منطق مربوط به چند مسئلهٔ مختلف را انباشته کنند. این همان پیچیدگی تصادفی است. مانند Subdomainها، ضروری است مرزهای Bounded Context را هر از گاهی بازبینی کنیم. همیشه به دنبال فرصتهایی برای سادهتر کردن مدلها از طریق استخراج Bounded Contextهای کاملاً متمرکز بر یک مسئله باشید.
رشد میتواند مشکلات طراحی پنهان را نیز آشکار کند. ممکن است متوجه شوید چند Bounded Context بهمرور «پرحرف» شدهاند و برای تکمیل تقریباً هر عملیات ناچارند Context دیگری را صدا بزنند. این میتواند نشانهٔ قوی یک مدل نامناسب باشد و باید با بازطراحی مرزهای Bounded Context برای افزایش خودمختاری آنها اصلاح شود.
Aggregateها
در بحث Domain Model فصل ۶، از اصل زیر برای طراحی مرزهای Aggregate استفاده کردیم:
قاعدهٔ کلی این است که Aggregateها را تا جای ممکن کوچک نگه دارید و فقط اشیایی را در آن قرار دهید که دامنهٔ کسبوکار واقعاً نیاز دارد در وضعیتی strongly consistent باشند.
با افزایش الزامات کسبوکار، ممکن است «راحت» به نظر برسد که قابلیتهای جدید را میان Aggregateهای موجود پخش کنیم، بدون آنکه اصل کوچک نگه داشتن Aggregate را دوباره بررسی کنیم. اگر Aggregate رشد کند و دادهای را شامل شود که لازم نیست همهٔ منطق کسبوکار آن بهصورت strongly consistent باشد، دوباره با پیچیدگی تصادفی روبهرو هستیم که باید حذف شود.
استخراج قابلیت کسبوکار به یک Aggregate مستقل نهتنها Aggregate اصلی را ساده میکند، بلکه میتواند Bounded Context میزبان آن را نیز سادهتر سازد. چنین بازآراییای اغلب یک مدل پنهان دیگر را آشکار میکند که پس از صریح شدن، باید در Bounded Context دیگری استخراج شود.
جمعبندی
همانطور که به هراکلیتوس نسبت داده میشود، تنها ثابت زندگی تغییر است. کسبوکارها نیز استثنا نیستند. شرکتها برای رقابتی ماندن پیوسته میکوشند تکامل یابند و خود را از نو بسازند. این تغییرها باید عناصر درجهیک فرایند طراحی باشند.
با تکامل دامنهٔ کسبوکار، تغییرهای Subdomainهای آن باید شناسایی و در طراحی سیستم منعکس شوند. مطمئن شوید تصمیمهای طراحی گذشته هنوز با وضعیت فعلی دامنه و Subdomainهای آن همراستا هستند. هر زمان لازم بود، طراحی را بهگونهای تکامل دهید که بهتر با راهبرد و نیازهای فعلی کسبوکار هماهنگ شود.
همچنین باید درک کرد که تغییر در ساختار سازمانی میتواند بر ارتباط و همکاری تیمها و نحوهٔ یکپارچهسازی Bounded Contextهایشان اثر بگذارد. یادگیری دربارهٔ دامنهٔ کسبوکار فرایندی مداوم است. هرچه دانش دامنهٔ بیشتری کشف میشود، باید از آن برای تکامل تصمیمهای راهبردی و تاکتیکی استفاده کرد.
در نهایت، رشد نرمافزار نوعی تغییر مطلوب است، اما اگر درست مدیریت نشود میتواند اثرهای فاجعهباری بر طراحی و معماری سیستم بگذارد. بنابراین:
- وقتی قابلیت یک Subdomain گسترش مییابد، به دنبال مرزهای ریزدانهتر Subdomain باشید تا تصمیمهای طراحی بهتری بگیرید.
- اجازه ندهید Bounded Context به «همهکارهٔ بیتمرکز» تبدیل شود. مطمئن شوید مدلهای هر Context بر حل مسائل مشخص تمرکز دارند.
- مرزهای Aggregate را تا جای ممکن کوچک نگه دارید. از معیار دادهٔ strongly consistent برای کشف فرصتهای استخراج منطق کسبوکار به Aggregateهای جدید استفاده کنید.
سخن پایانی نویسنده دربارهٔ این موضوع این است که مرزهای مختلف را دائماً برای نشانههای پیچیدگی ناشی از رشد بررسی کنید. برای حذف پیچیدگی تصادفی اقدام کنید و از ابزارهای DDD برای مدیریت پیچیدگی ذاتی دامنهٔ کسبوکار بهره ببرید.
تمرینها
- چه نوع تغییرهایی در یکپارچهسازی Bounded Contextها اغلب بر اثر رشد سازمانی رخ میدهند؟
- Partnership به Customer–Supplier (Conformist، Anticorruption Layer یا Open-Host Service)
- Anticorruption Layer به Open-Host Service
- Conformist به Shared Kernel
- Open-Host Service به Shared Kernel
- فرض کنید یکپارچهسازی Bounded Contextها از رابطهٔ Conformist به Separate Ways تغییر کرده است. بر اساس این تغییر چه اطلاعاتی میتوان استنباط کرد؟
- تیمهای توسعه برای همکاری با مشکل مواجه بودهاند.
- قابلیت تکرارشده یا Supporting Subdomain است یا Generic Subdomain.
- قابلیت تکرارشده یک Core Subdomain است.
- A و B.
- A و C.
- نشانههای تبدیل Supporting Subdomain به Core Subdomain چیست؟
- تکامل مدل موجود و پیادهسازی Requirementهای جدید آسانتر میشود.
- تکامل مدل موجود دردناک میشود.
- Subdomain با فرکانس بیشتری تغییر میکند.
- B و C.
- هیچکدام.
- کشف یک فرصت کسبوکار جدید به کدام تغییر منجر میشود؟
- Supporting به Core.
- Supporting به Generic.
- Generic به Core.
- Generic به Supporting.
- A و B.
- A و C.
- چه تغییری در راهبرد کسبوکار میتواند یکی از Generic Subdomainهای WolfDesk—شرکت فرضی معرفیشده در پیشگفتار—را به Core Subdomain تبدیل کند؟