Berhentilah Menggunakan Extend Di PHP - CRUDPRO

Berhentilah Menggunakan Extend Di PHP

Apakah Anda menggunakan class abstract atau ekstensi dalam kode domain Anda? Mudah-mudahan Anda akan berhenti melakukannya hari ini.

Sebelum menggali lebih dalam masalah ini, izinkan saya mengklarifikasi beberapa hal terlebih dahulu. Hari ini saya akan berbicara tentang kode yang tidak terkait dengan framework apa pun. Bergantung pada framework yang Anda pilih (Laravel, Symfony, dll.) atau library yang Anda gunakan.

Hari ini saya ingin membahas kode dari aplikasi. Anda dapat menyebutnya core code, core domain , code DDD, apa pun yang Anda inginkan. code yang menjadi tanggung jawab Anda atau tim Anda.

Karena itu, apakah Anda menggunakan kata kunci seperti 'extends' atau 'abstract' dalam kode Anda? Saya tidak menemukan alasan untuk extends class di dalamnya.

Artikel ini menguraikan mengapa menggunakan class final baik untuk code Anda. Beginilah tampilan template class saya di PHPStorm untuk semua proyek sejak saat itu.

//final-template.php
<?php

declare(strict_types);

namespace ${NAMESPACE};

final class ${NAME}
{
    
}

Saya sudah melihat dan mendengar ini berkali-kali. Apakah extends class memberikan fleksibilitas? Seberapa fleksibel inheritance untuk class seperti itu:

//final-bad-composition.php
<?php

abstract class BaseUser {}
abstract class WebUser extends BaseUser {}
class RegularUser extends WebUser {}
class AdminUser extends RegularUser {}
class PowerAdminUser extends RegularUser {}
class WipFunctionalityUser extends RegularUser {}

Mengubah class root seperti BaseUser di sini dapat menimbulkan konsekuensi yang menghancurkan seluruh aplikasi. Hanya kemungkinan masalah yang ditampilkan di sini, tetapi tidak ada manfaatnya. Inilah yang harus Anda lakukan:

1. Terima komposisi atas inheritance ❗

menggunakan interfaces;. Terapkan kata kunci final untuk mencegah inheritance

kodenya terlihat seperti ini:

//final-better-composition.php
<?php

interface UserInterface {}

final class RegularWebUser implements UserInterface {}
final class AdminUser implements UserInterface {}
final class PowerAdminUser implements UserInterface {}
final class WipFunctionalityUser implements UserInterface {}

Banyak orang kemudian mengatakan kepada saya: "kenapa, lihat berapa banyak kode yang akan diduplikasi, bisakah kita setidaknya menggunakan traits?" Tapi kemudian sifat ini akan menjadi bagian yang rapuh. Inilah tip kedua:

2. Jangan mencari pola❗

Tidak perlu takut duplikasi kode

Beginilah cara kerja otak kita. Paksa untuk mencari pola. Menghindari class abstract dan inheritance dapat mencegah keputusan pengkodean yang buruk.

Butuh fitur baru? Mulailah dengan interfaces. Gunakan blok dokumen untuk menjelaskan input, output, dan alasan di baliknya. Ini mungkin tampak seperti pelambatan, tetapi ini membantu Anda merencanakan apa yang benar-benar Anda butuhkan.

Saya ingin membagikan contoh yang menunjukkan betapa kuatnya teknik ini. Katakanlah aplikasi Anda perlu mengirimkan notifikasi. Tidak peduli bagaimana atau siapa. Kami hanya mementingkan fungsionalitas.

Mari kita mulai dengan menentukan notifikasi yang akan dikirim. Saya tidak peduli dengan detail. Ini hanyalah titik awal untuk jenis notifikasi (pesan SMS, pesan email, pesan whatsapp, dll.).

//final-notification.php
<?php

declare(strict_types=1);

namespace App\Notification;

/**
 * The base notification
 */
interface NotificationInterface
{
}

Sekarang mari buat manajer untuk melakukan pekerjaan. Tetap saja, saya tidak peduli dengan detail pesannya. Dijamin untuk mendapatkan pekerjaan yang dilakukan.

//final-notification-manager.php
<?php

declare(strict_types=1);

namespace App\Notification;

/**
 * The manager responsible for sending notifications.
 *
 * It is capable of sending many messages in batch.
 */
interface NotificationManager
{
    /**
     * @param iterable<NotificationInterface> $notifications
     *
     * @throws NotificationSendException In case of any errors
     */
    public function send(iterable $notifications): void;
}

Itulah yang saya sebut fleksibilitas sejati. Pemberitahuan seperti apa yang mungkin diperlukan?

Notifikasi email
//final-notification-email.php

<?php

declare(strict_types=1);

namespace App\Notification;

/**
 * E-mail notification
 */
interface EmailNotificationInterface extends NotificationInterface
{
    public function getTo(): EmailAddress;
    
    public function getSubject(): string;
    
    public function getBody(): string;
}
Notifikasi WhatsApp
//final-notification-whatsapp.php
<?php

declare(strict_types=1);

namespace App\Notification;

/**
 * WhatsApp notification
 */
interface WhatsAppNotificationInterface extends NotificationInterface
{
    public function getTo(): PhoneNumber;
    
    public function getMessage(): string;
}

Saya bisa melanjutkan daftar ini selamanya. Fitur dendensi ijection framework memungkinkan Anda menukar implementasi interfaces tanpa sepengetahuan aplikasi Anda. Dengan cara ini aplikasi dipisahkan dari detail sistem notifikasi. Pertimbangkan implementasi sederhana. Mekanisme transportasi diperlukan untuk mengirimkan notifikasi. Namun interfaces lain tersedia.

//final-notification-transport.php
<?php

declare(strict_types=1);

namespace App\Notification;

/**
 * Transport that physically delivers notifications.
 * 
 * @template T of NotificationInterface
 */
interface TransportInterface
{
    public function supports(NotificationInterface $notification): bool;
    
    /**
     * @param T $notification
     *
     * @throws NotificationSendException In case of any errors
     */
    public function deliver(NotificationInterface $notification): void;
}

Dan transportasi aktual yang mengirimkan pesan email:

//final-notification-transport-email.php
<?php

declare(strict_types=1);

namespace App\Notification\Implementation;

use App\Notification\EmailNotificationInterface;
use App\Notification\NotificationInterface;
use App\Notification\NotificationSendException;
use App\Notification\TransportInterface;
use Symfony\Component\Mailer\MailerInterface;
use Symfony\Component\Mime\Email;

/**
 * {@inheritDoc}
 *
 * @template-implements TransportInterface<EmailNotificationInterface>
 */
final class SymfonyMailerTransport implements TransportInterface
{
    private MailerInterface $mailer;
    
    public function __construct(MailerInterface $mailer)
    {
        $this->mailer = $mailer;
    }

    public function supports(NotificationInterface $notification): bool
    {
        return $notification instanceof EmailNotificationInterface;
    }
    
    /**
     * {@inheritDoc}
     */
    public function deliver(NotificationInterface $notification): void
    {
        $email = (new Email())
            ->to($notification->getTo()->getAddress())
            ->subject($notification->getSubject())
            ->html($notification->getBody());

        try {
            $this->mailer->send($email);
        } catch (\Exception $exception) {
            throw NotificationSendException::duringDelivery($exception);
        }
    }
}

Akhirnya manajer notifikasi sederhana:

//final-notification-manager-impl.php
<?php

declare(strict_types=1);

namespace App\Notification\Implementation;

use App\Notification\NotificationInterface;
use App\Notification\NotificationManagerInterface;
use App\Notification\NotificationSendException;
use App\Notification\TransportInterface;

/**
 * {@inheritDoc}
 */
final class DefaultNotificationManager implements NotificationManagerInterface
{
    /** @var iterable<TransportInterface> */
    private iterable $transports;
    
    /** @param iterable<TransportInterface> $transports */
    public function __construct(iterable $transports)
    {
        $this->transports = $transports;
    }
    
    /**
     * {@inheritDoc}
     */
    public function send(iterable $notifications): void
    {
        $notificationByTransport = $this->matchTransportsToNotifications($notifications);
        
        foreach ($notificationByTransport as [$transport, $notification]) {
            $transport->deliver($notification);
        }
    }
    
    /**
     * @param iterable<NotificationInterface> $notifications
     *
     * @return array<array-key, array{TransportInterface, NotificationInterface}>
     * 
     * @throws NotificationSendException If one of notifications cannt be sent
     */
    private function matchTransportsToNotifications(iterable $notifications): array
    {
        $matches = [];
    
        foreach ($notifications as $notification) {
            foreach ($this->transports as $transport) {
                if ($transport->supports($notification)) {
                    $matches[] = [$transport, $notification];

                    continue 2;
                }
            }
            
            throw NotificationSendException::unsupportedTransport($notification);
        }
        
        return $matches;
    }
}

Saya sudah menyukai setiap bagian dari kode ini:

  • Ikuti Prinsip Tanggung Jawab Tunggal: Setiap Bagian Hanya Mempedulikan Bagiannya
  • Ikuti prinsip buka-tutup. Fungsionalitas dapat diubah sesuai kebutuhan (menambahkan transport baru, menambahkan jenis notifikasi baru, dll.) tanpa merusak fungsionalitas saat ini.
  • Mengubah mekanisme transportasi tidak mempengaruhi bagian lain dari aplikasi
  • Mengubah bagian apa pun dari modul notifikasi tidak akan memengaruhi aplikasi Anda selama Anda tetap berpegang pada kontrak interfaces yang awalnya Anda rancang.
  • Sekarang setelah seluruh fungsi selesai, yang tersisa hanyalah membuat notifikasi baru. Katakanlah kita ingin mengirim email selamat datang setiap kali pengguna mendaftar. Membutuhkan implementasi interfaces notifikasi email.
//final-notification-email-impl.php
<?php

declare(strict_types=1);

namespace App\Notification\Impl;

use App\Notification\EmailAddress;
use App\Notification\EmailNotificationInterface;

final class EmailNotification implements EmailNotificationInterface
{
    private EmailAddress $emailAddress;
    private string $subject;
    private string $body;
    
    public function __construct(EmailAddress $emailAddress, string $subject, string $body)
    {
        $this->emailAddress = $emailAddress;
        $this->subject = $subject;
        $this->body = $body;
    }
    
    public function getTo(): EmailAddress
    {
        return $this->emailAddress;
    }
    
    public function getSubject(): string
    {
        return $this->subject;   
    }
    
    public function getBody(): string
    {
        return $this->body;   
    }
}

Layanan yang membuat notifikasi dari object user:

//final-notification-email-service.php
<?php

declare(strict_types=1);

namespace App\User\Notification;

use App\Notification\EmailAdddress;
use App\Notification\EmailNotificationInterface;
use App\Notification\Impl\EmailNotification;
use App\Template\TemplateEngine;

final class WelcomeGreetingEmailNotificationFactory
{
    private TemplateEngine $templates;
    
    public function __construct(TemplateEngine $templates)
    {
        $this->templates = $templates;
    }
    
    public function __invoke(UserInterface $user): EmailNotificationInterface
    {
        return new EmailNotification(
             new EmailAddress($user->getEmail()),
             'Welcome to the site!',
             $this->templates->render('Notification/Email/welcome.html', ['user' => $user]),
        );
    }
}

Kirim pemberitahuan dengan terhubung ke event post-pendaftaran aplikasi

//final-notification-subscriber.php
    <?php

declare(strict_types=1);

namespace App\User\Notification;

use App\Notification\EmailNotificationInterface;
use App\Notification\NotificationManagerInterface;
use App\Events\PostRegistrationEvent;
use App\User\Notification\WelcomeGreetingEmailNotificationFactory;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

final class EmailNotification implements EventSubscriberInterface
{
    private NotificationManagerInterface $notificationManager;
    private WelcomeGreetingEmailNotificationFactory $notificationFactory;
    
    public function __construct(
        NotificationManagerInterface $notificationManager,
        WelcomeGreetingEmailNotificationFactory $notificationFactory
    ) {
        $this->notificationManager = $notificationManager;
        $this->notificationFactory = $notificationFactory;
    }

    /** {@inheritDoc} */
    public static function getSubscribedEvents(): array
    {
        return [PostRegistrationEvent::NAME => 'sendNotification'];
    }
    
    public function sendNotification(PostRegistrationEvent $event): void
    {
        $notification = ($this->notificationFactory($event->getUser());
        
        $this->notificationManager->send([$notification]);
    }
}

Dekorasi service untuk ekstensi fleksibel

Teknik ampuh terakhir yang ingin saya perkenalkan adalah pola Dekorator. Ini sangat berguna karena Anda dapat extends fungsionalitas class dengan aman tanpa masuk ke bagian dalamnya. Misalnya, server SMTP Anda mengalami masalah dari waktu ke waktu, mencegah pengiriman email. Berkat pola dekorator, transmisi ulang dapat dilakukan pada transportasi apa pun.

//final-notification-transport-email-decorator.php
<?php

declare(strict_types=1);

namespace App\Notification\Implementation;

use App\Notification\NotificationInterface;
use App\Notification\TransportInterface;

/**
 * {@inheritDoc}
 *
 * @template-implements TransportInterface<TransportInterface>
 */
final class RepeatableTransportDecorator implements TransportInterface
{
    private TransportInterface $transport;
    private int $retryCount;
    
    public function __construct(TransportInterface $transport, int $retryCount)
    {
        $this->transport = $transport;
        $this->retryCount = $retryCount;
    }

    public function supports(NotificationInterface $notification): bool
    {
        return $this->transport->supports($notification);
    }
    
    /**
     * {@inheritDoc}
     */
    public function deliver(NotificationInterface $notification): void
    {
        $tryCounter = 0;
        
        do {
            try {
                $this->transport->deliver($notification);

                return;
            } catch (\Exception $exception) {
                ++$tryCounter;
            }
        } while ($tryCounter < $this->retryCount);
        
        throw NotificationSendException::duringDelivery($exception);
    }
}

Semoga bermanfaat, Terima kasih.