القيم الخالية ، عند استخدامها بدون تفكير ، يمكن أن تجعل حياتك لا تطاق وقد لا تفهم بالضبط ما الذي يسبب لهم مثل هذا الألم. اسمحوا لي أن أشرح.
القيم الافتراضية
لقد رأينا جميعًا طريقة تتطلب العديد من الوسائط ، ولكن أكثر من نصفها اختياري. والنتيجة هي شيء مثل هذا:
public function insertDiscount( string $name, int $amountInCents, bool $isActive = true, string $description = '', int $productIdConstraint = null, DateTimeImmutable $startDateConstraint = null, DateTimeImmutable $endDateConstraint = null, int $paymentMethodConstraint = null ): int
في المثال أعلاه ، نريد إنشاء خصم يتم تطبيقه في كل مكان افتراضيًا ، ولكن يمكن أن يكون غير نشط عند الإنشاء ، أو ينطبق فقط على منتجات محددة ، أو يتصرف إلا في أوقات معينة أو يطبق عندما يختار المستخدم طريقة دفع محددة.
إذا كنت ترغب في إنشاء خصم لطريقة دفع معينة ، فستحتاج إلى الاتصال بالطريقة كما يلي:
insertDiscount('Discount name', 100, true, '', null, null, null, 5);
ستعمل هذه الشفرة ، لكنها غير مفهومة تمامًا للشخص الذي يقرأها. يصبح من الصعب للغاية تحليله ، لذلك لا يمكننا دعم التطبيق بسهولة.
دعنا نأخذ هذا المثال حجة بواسطة حجة.
ما هو خصم صالح؟
لقد اكتشفنا بالفعل أن الخصم غير المحدود ينطبق في كل مكان. وبالتالي ، يحتوي خصم صالح على كل شيء ما عدا القيود التي يمكننا إضافتها لاحقًا. الوسيطة isActive لها قيمة افتراضية من true. لذلك ، يمكن استدعاء الطريقة كما يلي:
insertDiscount('Discount name', 100);
فقط من خلال قراءة الكود ، لا أعرف أن الخصم سيكون نشطًا على الفور. لمعرفة ذلك ، يجب أن أتحقق مما إذا كان توقيع الطريقة له قيم افتراضية.
الآن تخيل أنك تحتاج إلى قراءة 200 سطر من التعليمات البرمجية. هل أنت متأكد من أنك تريد التحقق من كل توقيع للطريقة التي تم استدعاؤها للحصول على معلومات مخفية؟ أفضل قراءة الكود دون الحاجة إلى البحث عن أي شيء.
الشيء نفسه ينطبق على الحجة المسؤولة عن الوصف. بشكل افتراضي ، إنها سلسلة فارغة - يمكن أن يسبب ذلك العديد من المشكلات في المكان الذي تتوقع أن ترى فيه وصفًا. على سبيل المثال ، قد تتم طباعته على الشيك ، ولكن نظرًا لأنه فارغ ، يرى المستخدم ببساطة سطرًا فارغًا بجوار السطر مع المبلغ. يجب ألا يسمح النظام بحدوث ذلك.
أود إعادة كتابة هذه الطريقة مثل هذا:
public function insertDiscount( string $name, string $description, int $amountInCents, bool $isActive ): int
لقد أزلت القيود تمامًا لأننا قررنا إضافتها لاحقًا باستخدام طرق منفصلة. نظرًا لأن جميع المعلمات مطلوبة الآن ، فيمكن ترتيبها بأي ترتيب. لقد وضعت الوصف مباشرة بعد الاسم ، لأن الرمز يقرأ بشكل أفضل عندما يكون قريبًا.
insertDiscount( 'Discount name', 'Discount description', 100, Discount::STATUS_ACTIVE );
أنا أيضا استخدام الثوابت لحالة نشاط الخصم. الآن لا تحتاج إلى إلقاء نظرة على توقيع الطريقة لمعرفة ما هو المعنى الحقيقي لهذه الحجة: يصبح من الواضح أننا نقوم بإنشاء الخصم النشط. في المستقبل ، يمكننا تحسين هذه الطريقة (المفسد: استخدام كائنات القيمة).
مضيفا القيود
الآن يمكنك إضافة العديد من القيود. لتجنب الصفر ، صفر ، صفر الجحيم ، سنقوم بإنشاء طرق منفصلة.
public function addProductConstraint( Discount $discount, int $productId ): Discount; public function addDateConstraint( Discount $discount, DateTimeImmutable $startDate, DateTimeImmutable $endDate ): Discount; public function addPaymentMethodConstraint( Discount $discount, int $paymentMethod ): Discount;
وبالتالي ، إذا أردنا إنشاء خصم جديد مع قيود معينة ، فسنفعل ذلك بالشكل التالي:
$discountId = insertDiscount( 'Discount name', 'Discount description', 100, Discount::STATUS_ACTIVE ); addPaymentMethodConstraint( $discountId, PaymentMethod::CREDIT_CARD );
الآن قارن هذا بالمكالمة الأصلية. سترى مدى ملاءمة القراءة.
فارغة في خصائص الكائن
يؤدي حل الأصفار في خصائص الكائن أيضًا إلى حدوث مشكلات. لا يمكنني نقل عدد مرات رؤية مثل هذه الأشياء:
$currencyCode = strtolower( $record->currencyCode );
Buuum! "لا يمكن تمرير فارغة إلى strtolower." حدث هذا لأن المطور نسي أن كود العملة قد يكون لاغيا. نظرًا لأن العديد من المطورين ما زالوا لا يستخدمون IDE أو يقمعوا التحذيرات في نفوسهم ، فإن هذا قد لا يلاحظه أحد لسنوات عديدة. سيستمر ظهور الخطأ في بعض السجلات غير المقروءة ، وسيقوم العملاء بالإبلاغ عن المشكلات التي تحدث بشكل دوري والتي ، على ما يبدو ، ليست ذات صلة بهذا ، لذلك لن يزعج أحد أن ينظر إلى سطر التعليمات البرمجية هذا.
يمكننا بالطبع إضافة شيكات فارغة أينما وصلنا إلى CurrencyCode. ولكن بعد ذلك سننتهي بنوع مختلف من الجحيم:
if ($record->currencyCode === null) { throw new \RuntimeException('Currency code cannot be null'); } if ($record->amount === null) { throw new \RuntimeException('Amount cannot be null'); } if ($record->amount > 0) { throw new \RuntimeException('Amount must be a positive value'); }
ولكن ، كما فهمت بالفعل ، ليس هذا هو الحل الأفضل. إلى جانب فوضى طريقتك ، يجب عليك الآن تكرار هذا الاختبار في كل مكان. وفي كل مرة تقوم فيها بإضافة خاصية فارغة أخرى ، لا تنس القيام بشيك آخر! لحسن الحظ ، هناك حل بسيط: الأشياء القيمة.
كائنات القيمة
الكائنات القيمة هي أشياء قوية ولكنها بسيطة. المشكلة التي كنا نحاول حلها هي أنه من الضروري التحقق من صحة جميع ممتلكاتنا باستمرار. لكننا نفعل ذلك لأننا لا نعرف ما إذا كان من الممكن الوثوق بخصائص الكائن ، سواء كانت صالحة. ماذا لو استطعنا؟
للثقة بالقيم ، فإنها تحتاج إلى سمتين: يجب التحقق من صحتها ويجب ألا تتغير منذ التحقق من الصحة. ألق نظرة على هذا الفصل:
final class Amount { private $amountInCents; private $currencyCode; public function __construct(int $amountInCents, string $currencyCode): self { Assert::that($amountInCents)->greaterThan(0); $this->amountInCents = $amountInCents; $this->currencyCode = $currencyCode; } public function getAmountInCents(): int { return $this->amountInCents; } public function getCurrencyCode(): string { return $this->currencyCode; } }
أنا أستخدم حزمة beberlei / assert. يلقي استثناء كلما فشل التحقق. هذا هو نفس استثناء الخطأ في الكود المصدري ، إلا إذا نقلنا الشيك إلى هذا المُنشئ.
بما أننا نستخدم تعريفات الكتابة ، فإننا نضمن أن النوع صحيح أيضًا. وبالتالي ، لا يمكننا تمرير كثافة العمليات إلى strtolower. إذا كنت تستخدم إصدارًا أقدم من PHP لا يدعم تعريفات النوع ، فيمكنك استخدام هذه الحزمة للتحقق من الأنواع مع -> integer () و -> string ().
بعد إنشاء الكائن ، لا يمكن تغيير القيم ، لأن لدينا أحرفًا فقط ، ولكن ليس هناك محددات. وهذا ما يسمى الحصانة. لا تسمح إضافة النهاية بتوسيع هذه الفئة لإضافة أدوات تسوية أو طرق سحرية. إذا رأيت مبلغ المبلغ في معاملات الطريقة ، فيمكنك التأكد بنسبة 100٪ من أن جميع خصائصه قد تم التحقق من صحتها وأن الكائن آمن للاستخدام. إذا كانت القيم غير صالحة ، فلن نتمكن من إنشاء كائن.
الآن بمساعدة كائنات القيمة ، يمكننا تحسين مثالنا:
$discount = new Discount( 'Discount name', 'Discount description', new Amount(100, 'CAD'), Discount::STATUS_ACTIVE ) insertDiscount($discount);
يرجى ملاحظة أننا نقوم أولاً بإنشاء خصم ، وداخله نستخدم المبلغ كوسيطة. هذا يضمن أن الأسلوب insertDiscount يستقبل كائن خصم صالحًا بالإضافة إلى جعل فهم كتلة التعليمات البرمجية هذه أسهل بكثير.
قصة رعب الصفر
دعونا نلقي نظرة على حالة مثيرة للاهتمام والتي يمكن أن تكون خالية ضارة في التطبيق. والفكرة هي استخراج المجموعة من قاعدة البيانات وتصفيتها.
$collection = $this->findBy(['key' => 'value']); $result = $this->filter($collection, $someFilterMethod); if ($result === null) { $result = $collection; }
إذا كانت النتيجة خالية ، فاستخدم المجموعة الأصلية كنتيجة؟ هذا يمثل مشكلة ، لأن طريقة التصفية تُرجع فارغة إذا لم تجد القيم المناسبة. وبالتالي ، إذا تم تصفية كل شيء ، فسنتجاهل المرشح ونرجع كل القيم. هذا يكسر تماما المنطق.
لماذا تستخدم المجموعة الأصلية كنتيجة؟ لن نعرف ابدا. أظن أن المطور لديه بعض الافتراضات حول معنى "لاغية" في هذا السياق ، لكن اتضح أنه خطأ.
هذه هي المشكلة مع القيم الخالية. في معظم الحالات ، ليس من الواضح ما يعنيه ، وبالتالي يمكننا فقط تخمين كيفية الرد عليها. من السهل جدًا ارتكاب خطأ. الاستثناء ، من ناحية أخرى ، واضح للغاية:
try { $result = $this->filter($collection, $someFilterMethod); } catch (CollectionCannotBeEmpty $e) {
هذا الرمز فريد من نوعه. المطور من غير المرجح أن يساء تفسيره.
هل يستحق كل هذا الجهد؟
كل هذا يبدو وكأنه جهد إضافي لكتابة التعليمات البرمجية التي تفعل الشيء نفسه. نعم هو كذلك. ولكن في الوقت نفسه ، ستقضي وقتًا أقل بكثير في قراءة الكود وفهمه ، وبالتالي ستتم مكافأة هذه الجهود إلى حد ما. كل ساعة إضافية أقضيها في كتابة التعليمات البرمجية تنقذني بشكلٍ صحيح في الأيام التي لا أحتاج فيها إلى تغيير الرمز أو إضافة ميزات جديدة. فكر في الأمر على أنه استثمار مضمون عالي العائد.
لقد حان نهاية بلدي tirade فارغة. آمل أن يكون هذا يساعدك على كتابة رمز أكثر قابلية للفهم وصيانتها.