مسیر تفکر در طراحی Model Driven (قسمت دوم)

در این مقاله سعی داریم که با انجام مثالی، کاربرد معیارهایی که در قسمت اول مقاله گفته شده را به تصویر بکشیم و در مسیری که مشخص شده است قدم برداریم. در حقیقت بخش عملی این مسیر، در این قسمت از مقاله است. مثالی که قرار است پیاده سازی شود، یک نیازمندی واقعی از محصولی است که در همین مثال کوچک، تقریبا تمام معیارهای طراحی را شامل میشود.قبل از انجام طراحی، گفتن این نکته الزامی است که، به چند دلیل پیاده سازی انجام شده صحیح نیست:1- طراحی و پیاده سازی انجام شده، بدون همفکری تیمی انجام شده است. تجارب کاری نشان میدهد که در یک تیم نرم افزاری، هر طراحی ایی حتی اگر در بهترین و بی نقص ترین شکل ممکن باشد، بدون مقبولیت جمعی، چندان دوام نخواهد آورد. چون اصولا در طراحی نرم افزار تداوم و تکامل یکی از مهمترین ویژگی ها باید باشد، این نکته بسیار حایز اهمیت است. یک طراحی با دوام در تیم نرم افزاری، همیشه در حد متوسط مهارت ها و قابلیت های آن تیم است. هرگز سطح مهارتی یک تیم نرم افزاری تغییر نمیکند، مگر اینکه متوسط آن تغییر کند و هر تغییری از تحصیل (تفکر و مطالعه) شروع میشود.2- پیاده سازی مد نظر تنها قرار است مسیر تفکر در طراحی Model Driven را بر ما آشکار کند. این پیاده سازی قصد برآورده کردن ارزش بیزینسی ندارد.3- طراحی بدون تست انجام شده و در مقاله بعدی، به منظور بررسی آثار رعایت معیار ها و نکات، بصورت Test After در میاید. یک طراحی خوب بصورت Test First است و از اصول TDD پیروی میکند.بعد از بیان و متوجه شدن مواردی که در بالا گفته شد، میتوانیم به سراغ پیاده سازی برویم. پیاده سازی انجام شده تنها به لایه Domain و Application مربوط میشود و وارد جزییات استفاده از Library های زیرساختی نمی شود. همچنین از پیاده سازی الگوریتم ها نیز اجتناب شده است.شرح نیازمندی: یک محصول E-Commerce را در نظر بگیرید که کاربران آن با توجه به خرید هایی که میکنند، امتیازاتی دریافت میکنند. داستان کاربری این نیازمندی از این قرار است: مالک محصول میخواهد امکان ثبت نام کاربر با کد معرف را داشته باشد. این کد معرف یک رشته 8 کارکتری است که توسط بازاریاب تولید میشود. همچنین بازاریاب میتواند مشخص کند هنگام ثبت نام کاربر، چه امتیاز اولیه ایی به کاربر اختصاص داده شود.قبل از اینکه به سراغ شرح بیشتر نیازمندی برویم، در مورد کد 8 رقمی معرف، تامل میکنیم. از Feature خواسته شده مشخص است که یک سیستم کدینک برای کد معرف، باید طراحی شود. در این مثال، این سیستم کدینگ را بصورت بسیار ساده یک رشته 8 کارکتری میبینیم که 3 کارکتر اول، کد ارجاع بازاریاب خواهد بود، 4 کارکتر بعدی کد ارجاع مشتری و کارکتر آخر یک شماره به عنوان کدینگ استفاده شده، برای ساخت کد معرف، قرار داده میشود. با توجه به اینکه در این سیستم کدینگ، هم از حروف هم از اعداد میتوان استفاده کرد، کد معرف یک عدد در مبنای 36 خواهد بود. بدین صورت سیستم کدینگ مد نظر، توانایی پوشش 46,656 بازاریاب را خواهد داشت. همچنین هر بازاریاب قابلیت تولید 1,679,616 کد یکتا را خواهد داشت که در مجموع 78,364,164,096 کد معرف یکتا وجود خواهد داشت.برای اینکه در سیستم کدینگ 0 جلو عدد نداشته باشیم اعداد را بصورت برعکس در کد میگذاریم. برای مثال در صورتی که شماره ارجاع بازاریاب 2 و شماره ارجاع مشتری 1 باشد، کد معرف 2001000C خواهد بود. حرف C در کارکتر آخر نشان دهنده سیستم کدینگ پیش فرض است که برای تولید کد معرف استفاده میشود. به عنوان یک مثال دیگر شماره ارجاع بازاریاب 457 و شماره ارجاع مشتری 7865 را در نظر بگیرید. کد معرف معادل آن PC0H260C خواهد بود.حال به منظور روشن ساختن نیازمندی، بعضی از سناریو های ممکن را بصورت Happy Path مورد بررسی قرار میدهیم.Scenario: Registering customer without referral codeGiven a customer with descriptions below:name: "mehdi," family: "falamarzi", referral code: ""When the customer registered in the systemThen the customer registered with "0" initial pointsScenario: Creating referral code for customers by marketerGiven a marketer with referral number: "457"And the marketer created "7864" referral code so farWhen the marketer created another referral code with "30" initial points for registering customersThen referral code "PC0H260C" created with "30" initial points for registering customersScenario: Registering customers with referral codeGiven a referral code "PC0H260C" with "30" initial points for registering customersAnd a customer with descriptions below:name: "mehdi," family: "falamarzi", referral code: "PC0H260C"When the customer registered in the systemThen the customer registered with "30" initial pointsحال پس از تفهیم و انتقال نیازمندی، اقدام به طراحی و پیاده سازی آن میکنیم. اول به سراغ marketer میرویم و آن را مدل میکنیم:مدل Marketerدر طراحی مدل Marketer، نکات زیر در نظر گرفته شده است:1- در طراحی Marketer از معیار اول استفاده شده است. به همین سبب کد های معرف دیگر بصورت Entity درون مدل Marketer قرار نخواهند گرفت بلکه بصورت AggregateRoot درآمده. این طراحی منطبق بر طراحی سیستم های OLTP باعث میشود که با ID به Model ها دسترسی پیدا کنیم. همچنین مدل Marketer با این طراحی، هر تعداد ReferralCode که درست شود، همچنان برای فراخوانی از دیتابیس سبک خواهد بود.(در اینجا گفتن این نکته شاید خالی از لطف نباشد، که در مدل REST بازاریاب، ممکن است برای سادگی کار Client، کد معرف، یک Entity از Marketer باشد. علی رغم اینکه در Domain، مدل Marketer چیز دیگری است، اما چون دغدغه سرویس های RESTful از مسایل Domain جدا هستند، آن لایه بخصوص میتواند مدل و نمایش Resource مستقل خودش را داشته باشد. شرح سرویس های RESTful خارج از قاموس این مقاله هست به این دلیل تا همین جا به مطالب مربوط به REST بسنده میکنیم) 2- بجای آن که درون ReferralCode برای چک شدن Unique بودن کد معرف از Domain Service استفاده شود، مدل Marketer را Factory برای Referral Code قرار میدهیم. این کار علاوه بر اینکه مدل ما را Rich میکند، از عریض و طویل شدن مدل ها نیز جلوگیری میکند. این نکته را در نظر داشته باشید که حضور ReferralCode در Marketer به هیچ عنوان ناقض قوانین طراحی Model Driven نیست بلکه به عنوان یک Trade Off در طراحی در نظر گرفته شده است. برای متوجه شدن این مورد میتوان به اولین جواب در سوال مطرح شده در Stackoverflow رجوع کرد: DDD: How to check invariant with multiple aggregate3- شماره ارجاع بازاریاب، تنها در زمان ایجاد کد معرف کاربرد دارد و در زمان ساخته شدن مدل Marketer کاربردی ندارد. به همین دلیل از Sequenceهای دیتابیس برای تخصیص شماره استفاده شده است. شماره ارجاع بازاریاب را همیشه میتوان در زمان ایجاد کد معرف در اختیار داشت و اطلاعات کامل را از طریق Event به اطلاع سیستم های دیگر از جمله سیستم Query رساند.4- بدلیل اینکه تعداد کدهای معرف تولید شده توسط Application افزایش میابد، استفاده از Timestamp الزامی است تا از مشکلات همزمانی ذخیره در دیتابیس جلوگیری به عمل آید. (البته Timestamp میتواند بصورت Shadow Property با استفاده از قابلیت های ORM پیاده سازی شود)پس از اینکه مدل Marketer توضیح داده شد، میتوانیم به سراغ طراحی و پیاده سازی مدل ReferralCode برویم:مدل ReferralCodeدر طراحی مدل نکات زیر در نظر گرفته شده است:1- سازنده این مدل بصورت internal قرار داده شده است تا تنها Factory آن که همان Marketer هست قادر به ساختن ReferralCode باشد. این عمل دقیقا نشان دهنده نیازمندی سیستم است که تنها بازاریاب قادر به ساختن کد معرف است. امروزه ORM های موجود نظیر EFCore قابلیت پشتیبانی از سازنده های internal همراه با پارامتر یا قابلیت استفاده از Factory برای مدل ها را دارا میباشند بنابراین مشکلی از لحاظ Mapping و بازخوانی مدل از دیتابیس پیش نخواهد آمد.2- در طراحی این مدل از چهارمین معیار طراحی استفاده شده است. بجای استفاده از Domain Service در مدل Customer برای تخصیص امتیاز اولیه، مدل های ReferralCode و Customer به هم ارتباط داده شده است. در این طراحی از Anemic شدن Customer اجتناب شده است. همچنین بیزینس مربوط به ReferralCode درون خودش قرار گرفته و این مدل را Rich کرده است.3- متد SetInitialRegisteringPoints بصورت internal قرار گرفته شده تا تنها مورد استفاده مدل های Domain قرار گیرد.پس از اینکه مدل ReferralCode توضیح داده شد، میتوانیم به سراغ طراحی و پیاده سازی مدل Customer برویم:مدل Customerدر طراحی مدل Customer این نکته در نظر گرفته شده است، که در زمان ساخته شدن مدل، امتیاز اولیه آن توسط ReferralCode تخصیص داده میشود. به همین دلیل ReferralCode در سازنده Customer آمده و همچنین Setter امتیاز مشتری نیز بصورت internal است.حال که مدل ها مشخص شدند، میتوانیم به سراغ Command و CommandHandlerی برویم که نیازمندی پروژه را پاسخ میدهند. طراحی آن ها به اینصورت خواهد بود:CreateCustomerCommandHandlerیک نکته در این طراحی وجود دارد. زمانی که مشتری کد معرف ندارد، ReferralCode آن Null است. اما این مورد در CommandHandler لحاظ نشده است. در حقیقت با استفاده از معیار شماره 3 طراحی سعی شده است که لایه های پایینی (درونی) بصورت هرچه Abstractتر طراحی شوند و Implemetation ها و احینا Cyclomatic های حاصله، به لایه های بالایی (بیرونی) انتقال داده شوند. برای اینکه این معیار را در طراحی در نظر بگیریم، با الهام گیری از الگوی طراحی Null Object، پیاده سازی ReferralCodeRepository را بدین صورت انجام میدهیم:(پیاده سازی ارتباط با دیتابیس انجام نشده است)ReferralCodeRepositoryNoReferralCodeبا استفاده از این طراحی، بجای آن که دو Command برای ثبت نام مشتری داشته باشیم یا برای پیاده سازی اینکار در لایه Application پیچیدگی Cyclomatic ایجاد کنیم، با رعایت معیار شماره 3 طراحی، یک دستی و سطح Abstraction لایه های پایینی (درونی) را حفظ خواهیم کرد.در طراحی های انجام شده، تمامی مدل ها Rich میباشند و حتی یک Domain Service استفاده نشده است. این در حالی است که تمامی مدل ها تنها فقط با ID آن ها قابل دستیابی هستند. همچنین از ایجاد Aggregate های سنگین نیز اجتناب شده است. این طراحی ساده، سبک و سریع مناسب سیستم های OLTP و متناظر با آن ها قسمت Write پروژه های DDD است.(نکته مهم حایز اهمیت آن است که، در قسمت Write، هیچ چیز اضافه ایی که برای انجام شدن Command ها مورد نیاز نیست، در State مدل ها نباید ذخیره شود. بسیاری از اطلاعات مورد نیاز در سیستم های مختلف، میتواند توسط Integration Eventها فراهم شود. همچنین سیستم Query که توسط Event ها دیتای آن فراهم میشود، یک سیستم خبره OLAP است که برای ذخیره و بازیابی اطلاعات تنها نیاز به Event های ذخیره شده دارد. همشکل کردن Write و Read از اشتباهات معمول برنامه نویسان در پیاده سازی CQRS است.)جمع بندی:برای اینکه بتوان معیارهای طراحی را رعایت کرد و یک پیاده سازی مطابق با آن ها انجام داد، نیاز است که در به کار بردن الگوهای طراحی شی گرایی تفکر کرد و از آن ها در مسیر درست کمک گرفت. به کارگیری از الگوهای طراحی برای تطابق پیاده سازی با معیار ها، همان مسیر تفکر در Model Driven است. تیم های نرم افزاری میتوانند در جلسات همفکری طراحی، پیشنهادات بیان شده را با معیارهای گفته شده تطابق دهند. هرکجا که طراحی مطابق با معیار ها نبود، مشکل را شناسایی و با استفاده از به کار گیری الگوهای طراحی شی گرایی، به یک طراحی مطابق با معیار های طراحی خوب Model Driven دست یابند.