وابستگی حلقوی سرویسهای عمومی و راه فرار ازش
خلاصهٔ کاملتر
نویسنده میگه ماجرا با نیت خوب شروع میشه: یه سرویس عمومی مثل feature flag، analytics یا ایمیل تراکنشی میسازین که به هیچ دامنهای وابسته نباشه. بعد برای type safety همهچی رو strongly typed میکنین، مثلاً یه union از رویدادهای ORDER_PLACED، PRODUCT_ADDED و USER_REGISTERED. برای اینکه ساختار دادهها تکراری نشه، سرویس analytics شروع میکنه به import کردن تایپها از #orders، #products و #users.
اینجاست که مشکل پیدا میشه: سرویس عمومی حالا از همهٔ دامنهها خبر داره، در حالی که همون دامنهها خودشون صداش میزنن. یعنی یه وابستگی حلقوی (circular dependency) دارین. به گفتهٔ نویسنده این ذاتاً خراب نیست، ولی اول یه code smellه، و تو زبانی مثل Go اصلاً اجازه داده نمیشه.
نویسنده دو مدل از این coupling رو جدا میکنه. مدل تایپی سادهست: ماژول orders تابع trackOrder رو از analytics صدا میزنه و analytics تایپ Order رو برمیگردونه، در حالی که اصلاً id یا total رو نمیخونه و فقط payload رو serialize میکنه. import type تو runtime حذف میشه پس حلقهٔ واقعی نداریم، ولی وابستگی فقط برای تایپ ساخته شده.
مدل منطقی خیلی ترسناکتره. نویسنده یه handler تو سرویس notifications پیدا کرده با حدود ۲۵ شاخهٔ if/else روی اسم رویدادها مثل TaskReassignmentRequired و ProjectCommentPosted. این سرویس قرار بود فقط ایمیل بفرسته، ولی خودش تصمیم میگیره کی ایمیل بگیره، از سرویسهای دیگه داده میکشه و بعد ایمیل رو میفرسته. نویسنده میگه یه سرویس ایمیل باید فقط گیرنده، موضوع و متن رو بشناسه؛ handlerی که همهچی رو میدونه با گذشت زمان فقط بزرگتر و باز کردنش سختتر میشه.
اولین و کلاسیکترین راهحل اینه که سرویس عمومی رو مثل هر کتابخونهای wrap کنین، چه مرزش HTTP باشه چه یه ماژول داخل همون process. سرویس analytics دیگه تایپ دامنهها رو نمیشناسه و فقط یه payload عمومی میگیره:
export type AnalyticsClient = {
track: (args: { payload: Record<string, unknown>; userId: string }) => Promise<void>
}بعد هر دامنه wrapper خودش رو میسازه، مثلاً SubscriptionAnalyticsClient که تایپ Payload و SubscriptionPlan رو خودش نگه میداره و فقط track رو به کلاینت عمومی پاس میده. اینجوری جهت وابستگی یهطرفه میشه و اگه تبدیل یا منطق خاصی لازم شد، جاش تو همین wrapperه. اگه هر دو طرف دست خودتونه، میتونین AnalyticsClient رو generic کنین تا دامنه تایپ payload رو بهش بده.
نکات کلیدی:
- سرویس عمومی که تایپهای همهٔ دامنهها رو import کنه، وابستگی حلقوی میسازه.
- coupling دو نوع داره: تایپی (فقط برای type safety) و منطقی (منطق دامنه داخل سرویس عمومی).
- import type تو runtime حذف میشه ولی وابستگی طراحی هنوز سر جاشه.
- مثال مقاله: یه handler notifications با حدود ۲۵ شاخهٔ شرطی روی اسم رویدادها.
- راهحل اول: سرویس عمومی فقط Record بگیره و هر دامنه wrapper تایپدار خودش رو بسازه.




