مفيش حاجة اسمها صدفة ولا حتي الدومين

أحمد رمضان

. 8 د قراءة

Advanced Web Application Architecture

Chapter: The Domain Model – Generating the identifier earlier (2.6)– Using a Value Object for the identifier (2.7)

في النقط اللي فاتت من الكتاب، اتكلمنا عن مفاهيم مهمة جدًا:Domainهو المجال اللي التطبيق شغال فيه (بيع كتب – حجز – توصيل… إلخ) Modelهو التمثيل المنطقي للحكاية اللي البزنس عايز يحكيهاGatewayطريقة نشيل بيها الـ SQL من الكود، وكل Class يمثل جدول كامل في الداتابيزEntityلما نقول Orderإحنا مش بنقول row في جدول إحنا بنقول: Order = طلب حقيقي في البزنس عنده:
Email
Quantity
Amount
ومحمي بقواعد (Assertions) مينفعش يبقى مش تمام

Repository Pattern

الـ Repository مجرد طبقة وسيطة بين عالمين:

👈 Domain → Objects، Rules، Business Logic
👈 Database → Tables، Rows، Arrays، SQL


و كمان عملنا ال implementation بتاع الRepo خش شوف 


-----------------------------------------------------------------------------------------------

قبل كدا السيناريو كده

Controller يعمل Order يبعته للـ Repository Repository يعمل save Database تولّد ID Repository يرجّع الـ ID Controller يحطه جوه الـ Order أو يستخدمه

الشكل ده معناه إيه؟

الـ Order اتخلق من غير ID
الـ Order استنى الداتابيز تقول له “إنت مين”
الدومين بقى معتمد على التخزين
وده غلط كبير في Domain Design

ليه غلط؟؟؟؟؟؟؟؟؟؟؟؟؟؟؟؟؟؟؟؟؟؟؟؟؟؟؟؟؟؟؟؟؟؟؟؟؟؟

1

Order من غير ID = Order ناقص و احنا قولنا ان الOrder في البزنس مش كائن وهمي
👈 Order حقيقي = لازم يكون له هوية
الهوية دي
بتستخدم في logging و في events و بتستخدم في tracking و بتستخدم في comparisons

-----------------------------------------------------------------------------------

يعني لو الـ Order لسه معندوش ID يبقى لسه مش كائن كامل وده ضد فكرة Entity = كائن له هوية ثابتة

-----------------------------------------------------------------------------------

طب بس كدا لأ لسه فيه غلط تاني 

2

خليت الداتابيز تقرر هوية الدومين أنت كده قلت ضمنيًا
“يا Database، قولي الـ Order ده اسمه إيه”

وده خطر لأن:

مش كل Database فيها auto increment و مش كل storage بيرجع ID فورًا في systems async مفيش ID أصلاً وقت الـ save و لو غيرت DB → المنطق كله يقع

-----------------------------------------------------------------------------------

يعني الدومين بقى ضعيف والداتابيز بقت صاحبة قرار وده عكس الصح

-----------------------------------------------------------------------------------

طب بس كدا لأ لسه فيه غلط تالت 

3

الsave بقى ليه side effect خطر

ليه ؟؟

علشان الsave المفروض يخزن وبس
لكن لما يرجّع ID و يكمّل الكائن و يغيّر حالته

يبقى كده

كسرنا Command / Query Separation

يعني إيه

يا تغيّر في السيستم او يا تسأل السيستم
بس ما تعملش الاتنين في نفس الوقت

-------------------------------------------------------------------

الCommand = أمر → يغيّر حالة السيستم

و الQuery = سؤال → يرجّع داتا بس، من غير أي تغيير

من الاخر 

👈 الsave بقى بيعمل أكتر من دور

------------------------------------------------------------------

الوقتي ه  Generating the identifier earlier


يعني ايه بقا

الـ Order لازم يبقى كامل من أول ثانية اتخلق فيها

وكامل يعني

معاه ID و معاه بياناته و معاه قواعده
من الاخر مش مستني حد يكمّله بعدين.

طب و الحل بقاااا؟؟؟؟

الحل مش في نوع الـ ID و لكن في توقيت إنشاء الـ ID

بدل ما:

الداتابيز تولّد الـ ID نخلي الدومين يولّد الـ ID أو على الأقل يطلبه قبل الإنشاء


طب بقولك م تيجي نفكها حتة حتة


 اولا الCommand يعني إيه؟

أي ميثود بتعمل insert او update اوdelete أو أي تغيير في الحالة
دي Command

 احنا قولنا القاعدة اي ؟؟؟؟؟؟

الـ Command مايرجعش داتا أقصى حاجة void أو Exception لو فشل

--------------------------------------------

ثانيا الQuery يعني إيه؟

أي ميثود بتسأل عن حاجة بترجع داتا من غير ما تلمس الحالة
دي Query

 احنا قولنا القاعدة اي ؟؟؟؟؟؟

الـ Query مايغيّرش أي حاجة 

تعالي نشوف الكلام دا معناه اي و اي الحل زي م قولنا الحل

مش في نوع الـ ID و لكن في توقيت إنشاء الـ ID


يعني اي  ندي الـ Order الـ ID من الأول يعني نعرفه ف ال constractor


معنى الكلام ده؟

Order مش بيتولد إلا وهو كامل
مفيش Order “نص مستوي”
الهوية جزء من تعريف الكائن


التغيير التاني: Repository بقى يجيب ID مش يولّده وقت save


ركز هنا حلو اوي 

👈 Repository مش بيرجع ID من save
👈 Repository بيطلع ID قبل الإنشاء

ليه Repository هو اللي بيطلع الـ ID؟

سؤال مهم جدا عشان
👈 هو الوحيد اللي عارف طريقة التخزين و هو اللي يعرف هل Integer ولا UUID ولا Sequence
👈 لكن الدومين مش مهتم يعرف

--------------------------------------------------------------------------------------

الدومين بس يقول “هاتلي هوية صالحة” و دا فرق فلسفي مش شكلي بس 

--------------------------------------------------------------------------------------

👈 مثال naive (بس تعليمي)


 الطريقة دي:

تنفع للفهم لكن خطره وده طبيعي… المهم الفكرة مش التنفيذ.

ايه هيه المشكلة الكبيرة اوي بقا شوفتها؟؟؟؟؟؟ 

الطريقة دي بسيطة، لكن لو فيه users كتير بيتعاملوا مع النظام في نفس الوقت، ممكن يحصل تعارض IDs.


الحل الصح عمليًا: UUID


ليه UUID ممتاز هنا؟

مش محتاج DB و مفيش collisions و كمان ينفع distributed systems
و ف النهاية بتحصل ع Order مستقل تمامًا


Controller بعد 2.6



-----------------------------------------------------------------------------------------

النتيجة: الأوردر جاهز بالـ ID من أول لحظة → ممتاز للـ Domain Model.

Order اتخلق كامل و save مجرد حفظ و مفيش تعديل بعد الإنشاء

------------------------------------------------------------------------------------------


حل متقدم: Sequence Table (لما تحب Integer من غير مشاكل)



Atomic و كمان Safe و Still domain-friendly

-------------------------------------------------------------------------------------------------

الخلاصة الحقيقية لـ 2.6

2.6 مش عن ID و لكن عن استقلال الدومين و اكتمال الكيان من لحظة ولادته و كمان فصل القرار عن التخزين و اخيرا احترام الـ Entity كObject مش Row

-------------------------------------------------------------------------------------------------

قبل الOrder ناقص DB بتتحكم save بيعمل أكتر من دوره

بعد الOrder كامل من البداية DB مجرد تنفيذ save() void نظيف Domain محترم نفسه

-------------------------------------------------------------------------------------------------





كدا شوفنا اي الغلط ف اني استخدم ال id  بعد م احفظ الorder و الحل اي بس تعالي نكمل 

--------------------------------------------------------------------------------------------------

2.7 Using a Value Object for the identifier

يعني اي؟؟؟؟؟

مش كفاية إننا ندي الـ Order ID من الأول
لا، إحنا كمان مش عايزين الـ ID يبقى primitive type زي int أو string
عايزينه يبقى كائن له معنى في الدومين → Value Object

ليييييه

نقدر نغيّر نوع الـ ID (int, UUID, ULID) بسهولة
نخفي التفاصيل الداخلية عن باقي النظام
نخلي الـ Order مستقل تمامًا عن طريقة التخزين

الهدف

نغلف نوع البيانات الداخلي → Entity تتحكم فيه بالكامل → Encapsulation محفوظ

يعني اي ؟؟؟

يعني بدل م اقول int $orderId اقول OrderId $orderId
وده فرق كبير جدًا في التصميم، مش تجميل كود وخلاص.


ليه أصلاً نعمل Value Object للـ ID؟

خلينا نربطها باللي فات قبل كدا

كنا عملنا حاجة حلوة الـ Order بياخد ID من الأول Repository عنده nextIdentity
بس لسه فيه مشكلة 👇

الـ Order عارف إن الـ ID UUID وعارف إنه String و عارف إنه متخزن إزاي وده معناه:

تفاصيل تخزين بدأت تتسرب جوه الـ Domain
Encapsulation على وشك يتكسر

طب و اي الحل الي بقاااااا؟؟؟

👈 نغلف الـ ID جوه Value Object

يعني نعمل كلاس اسمه OrderId

👈 هو الوحيد اللي عارف نوع الـ ID
👈 عارف بيتحوّل لإيه
👈 عارف جاي من UUID ولا int ولا ULID

والباقي كله يتعامل معاه كـ OrderId وخلاص


تعالي نعمله سواااااا


تعالي بقا علشان الكود دا محتاج واقفة 


اول معلومة ليه final

👈 final → مفيش inheritance لأن ده قيمة مش سلوك
👈 يعني محدش من بره يعرف الـ ID شكله إيه
👈 يعني حطيت اول قفل ع Encapsulation

تاني و اهم معلومة ليه الـ constructor private؟


👈 علشان محدش يعمل (new OrderId($anyThing
👈 إنت اللي بتتحكم إزاي الكائن ده يتخلق وده جوهر الـ Value Object
ده اسمه Named Constructor و معناه طريقة واضحة لبناء الـ OrderId
لو بكرة غيرت UUID بـ ULID هتغير هنا وبس والـ Domain كله مش هيحس

تالت معلومة خاصة ب asString 

اللي عايز يطلع الـ ID بره الدومين
ياخده string وبس

يعني

👈 Session او Database او API او Logs كلهم ياخدوا string لكن محدش يلمس الـ UUID نفسه
👈 Encapsulation اتقفل بالقفل التاني



نعدي على الـ Entity


--------------------------------------------------------------------------------------------------

لما كتبت كدا private OrderId $id


هنا حصل التحول الحقيقي الـ Order مبقاش عارف UUID و مبقاش عارف string و مبقاش عارف int

هو عارف حاجة واحدة 👈 أنا عندي OrderId

--------------------------------------------------------------------------------------------------

لما كتبت كدا OrderId $id جوه ال Constractor


هنا Order كامل من أول لحظة الـ ID جزء من هويته مش مستني DB ولا Repository

--------------------------------------------------------------------------------------------------

لما كتبت كدا public function id(): OrderId


إنت ما رجعتش string رجعت OrderId

يعني

اللي جوه الدومين يتعامل مع Value Object
التحويل يحصل عند الحدود بس (Boundaries)

--------------------------------------------------------------------------------------------------

لما كتبت كدا    this->id->asString جوه الmappedData


Entity هي اللي قررت إمتى تطلع الـ ID بره وبأي شكل
مش Repository مش Controller ده Encapsulation صح 




نعدي على الـ Repo


--------------------------------------------------------------------------------------------------

الـ Repository بينفّذ و مش بيفكر و مش بيقرر هو بس بيقول اتفضل OrderId جاهز

--------------------------------------------------------------------------------------------------


لما كتبت كدا  public function save(Order $order): void

save() → void
يعني مفيش return و مفيش IDs طالعةو مفيش leaks و اخيرا CQRS محترم 
بمعني تاني المشكلة الي كتبناها فوق 
CQRS = Command Query Responsibility Segregation
يعني: نفصل بين الحاجات اللي بتغير حالة النظام (Commands) والحاجات اللي بتقرا البيانات (Queries).
Command: أي حاجة بتعمل تغيير (insert, update, delete) → مفيش return غير نجاح/فشل، مش data.
Query: أي حاجة عايز تعرف بيها حاجة عن النظام → مفيش تغيير، بس بترجع البيانات.

--------------------------------------------------------------------------------------------------


نعدي على الـ Controller بقا 




--------------------------------------------------------------------------------------------------

لما كتبت كدا  orderId = $orderRepository->nextIdentity

الController ما يعرفش UUID وميعرفش string من الاخر خد OrderId وخلاص

--------------------------------------------------------------------------------------------------

لما كتبت كدا  new Order و اعطيته كل الداتا حتي الId

الـ Order اتخلق: كامل و متماسك و مستقل عن التخزين

--------------------------------------------------------------------------------------------------

لما كتبت كدا  orderRepository->save($order)

الـ Order اتحفظ وبس من غير رجوع و كمان من غير side effects

--------------------------------------------------------------------------------------------------

لما خليت الContainer يعمل set للcurrentOrder

عن طريق order->asstring 


هنا بس خرجنا بره الدومين فحوّلنا الـ Value Object لـ string وده مقصود وده صح

--------------------------------------------------------------------------------------------------

--------------------------------------------------------------------------------------------------

الخلاصة بقاااااااااااا


2.7 مش عن UUID و لكن عن السيطرة

السيطرة على النوع و السيطرة على الإنشاء و السيطرة على الخروج بره الدومين

النتيجة:

Order كائن محترم
Encapsulation متقفل
Repository عبد مطيع
Database تفصيلة تنفيذية

--------------------------------------------------------------------------------------------------


بعد ما خلّينا الـ constructor private



طلع سؤال ذكي جدًا:

هو كده إحنا طبقنا Singleton Design Pattern؟

الإجابة المختصرة:

لا لا لا لا لا لا لا لا لا لا لا لا لا لا لا 



ليه OrderId مش Singleton؟

خلّينا الأول نوضح الفرق الأساسي، لأن هنا معظم الناس بتتلخبط.


أولًا: يعني إيه Singleton؟

Singleton هدفه حاجة واحدة بس يبقى فيه نسخة واحدة فقط من الـ Object في السيستم كله

خصائصه المشهورة:

Instance واحدة بس
static getInstance
نفس الـ Object بيتعاد كل مرة
مفيش new خالص من برّه



طيب Value Object (زي OrderId) وضعه إيه؟

Value Object هدفه مختلف تمامًا التحكم في إزاي الكائن يتخلق مش عدد النسخ اللي تتخلق

يعني عادي جدًا:

100 OrderId او حتي 1,000 OrderId او مليون OrderId ولا أي مشكلة خالص كل واحد مستقل المهم كلهم متخلقين صح

يبقى ليه الـ constructor معمول private؟

خلّيك فاكر الجملة دي كويس إنت اللي بتتحكم إزاي الكائن يتخلق ودي مش رفاهية… دي حماية للدومين.



المشكلة لو الـ constructor كان public

كان يحصل إيه؟

أي حد ممكن يعمل string او int او UUID مش valid او null او garbage
الObject يتخلق بس مكسور من جوه القواعد اتكسرت قبل ما الدومين يبدأ أصلاً

يعني:

Encapsulation راحت
Value Object فقد قيمته
الدومين بقى مكشوف

يبقي الحل اي بقا ؟؟؟؟؟؟؟؟؟؟؟؟

private constractor 


--------------------------------------------------------------------------------------------------

طب إزاي الكائن يتخلق أصلًا؟

هنا بقى يدخل Factory Method

Factory Method = طريقة رسمية لإنشاء Object

يعني اي ؟؟

هي دالة شغلتها الأساسية إنها تخلق Object بدل ما الناس تستخدم new مباشرة.


ده Factory Method كامل

ليه السطر ده Factory Method؟

خلّينا نمشي عملي، مش نظري.
Factory Method هوه Method مسؤول عن إنشاء كائن بدل ما الإنشاء يحصل بـ new من برّه وده اللي حصل حرفيًا.



--------------------------------------------------------------------------------------------------الصورة الكاملة بقى

اولا

قفلنا الإنشاء المباشر عن طريق ال private constractor

ثانيا

فتحنا باب واحد رسمي عن طريق static function fromUuid

static → من غير instance بيرجع OrderId جاهز

يعني 

عايز OrderId؟
ادخل من الباب ده بس تعالي من عند الstatic function 

----------------------------------------------------------------------------------------------------------------------------------------------------------

طيب ليه مش constructor وخلاص؟

عشان الـ constructor

بيجبرك تستخدم new
صعب تمنع إساءة الاستخدام
مبيشرحش الكائن اتخلق إزاي وليه

لكن OrderId::fromUuid($uuid) بتقول معنى واضح:

OrderId جاي من UUID مش new وخلاص

طيب ليه استخدمنا self؟

معناها: رجّع نفس الكلاس لو اسم الكلاس اتغير → الكود سليم و لو حصل inheritance → still safe

والمهم

الـ Factory Method بيرجع نفس نوعه

هل كل Factory Method لازم يكون static؟

لا لا لا 
بس في Value Objects
static هو الطبيعي
لأنك معندكش instance أصلًا
الهدف: خلق كائن من قيمة
وده اللي هنا بالظبط.

ربط كل ده بالـ Encapsulation

الـ Encapsulation اتحفظ إزاي؟
الإنشاء اتقفل → private constructor
القواعد اتحطت في مكان واحد
مفيش خلق عشوائي

يعني

أي OrderId في السيستم
عدّى من fromUuid
وده ضمان قوي جدًا.

الخلاصة النهائية (مهمة جدًا)

OrderId مش Singleton
private constructor ≠ Singleton
الهدف مش نسخة واحدة
الهدف خلق مضبوط
private constructor = منع
Factory Method = بديل منظم
Factory Method = باب رسمي للإنشاء
fromUuid = المصنع
self = ضمان النوع
Encapsulation محفوظ
الدومين متحكم في نفسه