Nilai-nilai Null, ketika digunakan tanpa berpikir, dapat membuat hidup Anda tak tertahankan dan Anda bahkan mungkin tidak mengerti apa yang sebenarnya menyebabkan mereka begitu sakit. Biarkan saya jelaskan.
Nilai default
Kita semua telah melihat metode yang membutuhkan banyak argumen, tetapi lebih dari setengahnya adalah opsional. Hasilnya kira-kira seperti ini:
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
Dalam contoh di atas, kami ingin membuat diskon yang berlaku di mana-mana secara default, tetapi dapat tidak aktif saat membuat, berlaku hanya untuk produk tertentu, hanya bertindak pada waktu tertentu atau berlaku ketika pengguna memilih metode pembayaran tertentu.
Jika Anda ingin membuat diskon untuk metode pembayaran tertentu, Anda harus memanggil metode sebagai berikut:
insertDiscount('Discount name', 100, true, '', null, null, null, 5);
Kode ini akan berfungsi, tetapi benar-benar tidak dapat dipahami oleh orang yang membacanya. Menjadi sangat sulit untuk menganalisisnya, jadi kami tidak dapat dengan mudah mendukung aplikasi tersebut.
Mari kita ambil contoh argumen ini dengan argumen.
Apa itu diskon yang valid?
Kami telah menemukan bahwa diskon tak terbatas berlaku di mana-mana. Dengan demikian, diskon yang valid berisi semuanya kecuali batasan yang dapat kita tambahkan nanti. Argumen isActive memiliki nilai default true. Oleh karena itu, metode ini dapat disebut sebagai berikut:
insertDiscount('Discount name', 100);
Hanya dengan membaca kode, saya tidak tahu bahwa diskon akan segera aktif. Untuk mengetahuinya, saya harus memeriksa apakah tanda tangan metode memiliki nilai default.
Sekarang bayangkan Anda perlu membaca 200 baris kode. Apakah Anda yakin ingin memeriksa setiap tanda tangan dari metode yang dipanggil untuk informasi tersembunyi? Saya lebih suka membaca kode tanpa harus mencari apa pun.
Hal yang sama berlaku untuk argumen yang bertanggung jawab untuk deskripsi. Secara default, ini adalah string kosong - ini dapat menyebabkan banyak masalah di tempat Anda mengharapkan untuk melihat deskripsi. Misalnya, mungkin dicetak pada cek, tetapi karena kosong, pengguna cukup melihat garis kosong di sebelah garis dengan jumlahnya. Sistem seharusnya tidak membiarkan ini terjadi.
Saya akan menulis ulang metode ini seperti ini:
public function insertDiscount( string $name, string $description, int $amountInCents, bool $isActive ): int
Saya sepenuhnya menghapus batasan karena kami memutuskan untuk menambahkannya nanti menggunakan metode terpisah. Karena semua parameter sekarang diperlukan, mereka dapat diatur dalam urutan apa pun. Saya menempatkan deskripsi tepat setelah nama, karena kode membaca lebih baik ketika mereka sudah dekat.
insertDiscount( 'Discount name', 'Discount description', 100, Discount::STATUS_ACTIVE );
Saya juga menggunakan konstanta untuk status aktivitas diskon. Sekarang Anda tidak perlu melihat tanda tangan metode untuk mencari tahu apa arti sebenarnya dari argumen ini: menjadi jelas bahwa kami membuat diskon aktif. Di masa mendatang, kita dapat lebih meningkatkan metode ini (spoiler: using object values).
Menambahkan Kendala
Sekarang Anda dapat menambahkan berbagai batasan. Untuk menghindari nol, nol, nol neraka, kami akan membuat metode terpisah.
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;
Dengan demikian, jika kami ingin membuat diskon baru dengan batasan tertentu, kami akan melakukannya seperti ini:
$discountId = insertDiscount( 'Discount name', 'Discount description', 100, Discount::STATUS_ACTIVE ); addPaymentMethodConstraint( $discountId, PaymentMethod::CREDIT_CARD );
Sekarang bandingkan dengan panggilan asli. Anda akan melihat betapa lebih nyamannya membaca.
Properti objek tidak ada
Menyelesaikan nol pada properti objek juga menyebabkan masalah. Saya tidak bisa menyampaikan seberapa sering saya melihat hal-hal seperti itu:
$currencyCode = strtolower( $record->currencyCode );
Buum! "Tidak bisa meneruskan nol ke strtolower." Ini terjadi karena pengembang lupa bahwa currencyCode mungkin nol. Karena banyak pengembang masih tidak menggunakan IDE atau menekan peringatan di dalamnya, ini bisa tidak diperhatikan selama bertahun-tahun. Kesalahan akan terus bergulir di beberapa log yang belum dibaca, dan klien akan melaporkan masalah berkala yang tampaknya tidak terkait dengan ini, jadi tidak ada yang akan repot untuk melihat baris kode ini.
Kami tentu saja dapat menambahkan cek kosong di mana pun kami mengakses currencyCode. Tetapi kemudian kita akan berakhir di neraka yang berbeda:
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'); }
Tapi, seperti yang sudah Anda pahami, ini bukan solusi terbaik. Selain mengacaukan metode Anda, sekarang Anda harus mengulangi tes ini di mana-mana. Dan setiap kali Anda menambahkan properti nol lain, jangan lupa untuk melakukan pemeriksaan lain seperti itu! Untungnya, ada solusi sederhana: nilai objek.
Nilai Objek
Objek nilai adalah hal yang kuat tetapi sederhana. Masalah yang kami coba selesaikan adalah bahwa perlu untuk terus memvalidasi semua properti kami. Tapi kami melakukan ini karena kami tidak tahu apakah mungkin untuk mempercayai sifat-sifat objek, apakah mereka valid. Bagaimana jika kita bisa?
Untuk mempercayai nilai-nilai, mereka membutuhkan dua atribut: mereka harus divalidasi dan seharusnya tidak berubah sejak validasi. Lihatlah kelas ini:
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; } }
Saya menggunakan paket beberlei / assert. Itu melempar pengecualian setiap kali cek gagal. Ini sama dengan pengecualian untuk null dalam kode sumber, kecuali kami telah memindahkan centang ke konstruktor ini.
Karena kami menggunakan deklarasi tipe, kami menjamin bahwa tipenya juga benar. Dengan demikian, kita tidak dapat beralih ke strtolower. Jika Anda menggunakan versi PHP yang lebih lama yang tidak mendukung deklarasi tipe, maka Anda dapat menggunakan paket ini untuk memeriksa tipe dengan -> integer () dan -> string ().
Setelah membuat objek, nilai tidak dapat diubah, karena kita hanya memiliki getter, tetapi tidak ada setter. Ini disebut kekebalan. Menambahkan final tidak memungkinkan memperluas kelas ini untuk menambahkan setter atau metode ajaib. Jika Anda melihat Jumlah $ jumlah dalam parameter metode, Anda dapat 100% yakin bahwa semua propertinya telah divalidasi dan objek tersebut aman untuk digunakan. Jika nilai tidak valid, kami tidak akan dapat membuat objek.
Sekarang dengan bantuan objek nilai, kita dapat lebih meningkatkan contoh kita:
$discount = new Discount( 'Discount name', 'Discount description', new Amount(100, 'CAD'), Discount::STATUS_ACTIVE ) insertDiscount($discount);
Harap perhatikan bahwa kami pertama kali membuat Diskon, dan di dalamnya kami menggunakan Jumlah sebagai argumen. Ini memastikan bahwa metode insertDiscount menerima objek diskon yang valid selain membuat seluruh blok kode ini lebih mudah dipahami.
Nol Horor Story
Mari kita lihat kasus menarik di mana null bisa berbahaya dalam aplikasi. Idenya adalah untuk mengekstrak koleksi dari database dan memfilternya.
$collection = $this->findBy(['key' => 'value']); $result = $this->filter($collection, $someFilterMethod); if ($result === null) { $result = $collection; }
Jika hasilnya nol, lalu gunakan koleksi asli sebagai hasilnya? Ini bermasalah, karena metode penyaringan mengembalikan nol jika tidak menemukan nilai yang sesuai. Jadi, jika semuanya disaring, kami akan mengabaikan filter dan mengembalikan semua nilai. Ini sepenuhnya mematahkan logika.
Mengapa koleksi asli digunakan sebagai hasilnya? Kami tidak akan pernah tahu. Saya menduga bahwa pengembang memiliki asumsi tertentu tentang apa arti null dalam konteks ini, tetapi ternyata salah.
Ini adalah masalah dengan nilai null. Dalam kebanyakan kasus, tidak jelas apa artinya, dan oleh karena itu kita hanya bisa menebak bagaimana meresponsnya. Sangat mudah untuk melakukan kesalahan. Pengecualian, di sisi lain, sangat jelas:
try { $result = $this->filter($collection, $someFilterMethod); } catch (CollectionCannotBeEmpty $e) {
Kode ini unik. Pengembang tidak mungkin salah menafsirkannya.
Apakah ini sepadan dengan usaha?
Semua ini terlihat seperti usaha ekstra untuk menulis kode yang melakukan hal yang sama. Ya, benar. Tetapi pada saat yang sama Anda akan menghabiskan lebih sedikit waktu untuk membaca dan memahami kode, sehingga upaya akan dihargai. Setiap jam tambahan yang saya habiskan menulis kode dengan benar menyelamatkan saya berhari-hari ketika saya tidak perlu mengubah kode atau menambahkan fitur baru. Anggap saja sebagai investasi hasil tinggi yang dijamin.
Jadi akhir dari omelan nol saya telah tiba. Saya harap ini membantu Anda menulis kode yang lebih mudah dipahami dan dipelihara.