継承の上の構成:「テンプレート法」に取り組む


この記事では、私たちは継承についての構成について話をするつもりですが、理論的な観点からこのトピックに近づいていません(トピックを把握することによって、何百もの良い記事をオンラインで見つけることができます).

テンプレートメソッド
テンプレートメソッドは、いくつかのメソッド(少なくとも1つ)が宣言されているアルゴリズムスケルトンをカプセル化するために考案された行動設計パターンですabstract 具体的なクラスに“テンプレート”を拡張し、具体的な実装を提供する.このパターンはInversion Of Control, which I’ve already written about . その性質に基づいて,テンプレート法は継承原理に強く依存しているが,これにも関わらず組成を用いることによってかなりの結果が得られる.

免責事項
この記事で、私は1つのアプローチが他方よりよいと主張していません;実際、私は両方の長所と短所を強調しようとします.この記事は、「規範的な」デザインパターンの代わりの「共感的な」実装を捜していたので、私はテンプレートメソッドのために一つを見つけませんでした.


上でリンクされた例に基づいて、少し修正されたコードがあります.
abstract class UserRegister 
{
   private $em;

   public function __construct(EntityManagerInterface $em) 
   {
       $this->em = $em;
   }

   final public function register(User $user): void 
   {
       $user->enable();
       if (!$this->sendVerification($user)) {
         return;
       }

       $this->em->persist($user);
       $this->em->flush();
   }

   protected abstract function sendVerification(User $user): bool;
}

class EmailVerificationUserRegister extends UserRegister 
{
   protected function sendVerification(User $user): bool
   {
       $email = $user->getEmail();

       return $this->sendEmail($email);
   }

   private function sendEmail(string $email): bool
   {
      // omitted: sending email code
   }
}

class PhoneVerificationUserRegister extends UserRegister 
{
   protected function sendVerification(User $user): bool
   {
       $phone = $user->getPhoneNr();

       return $this->sendSms($phone);
   }

   private function sendSms(string $phone): bool
   {
     // omitted: sending sms code
   }
}

class NoVerificationUserRegister extends UserRegister 
{
   protected function sendVerification(User $user): bool
   {
       $user->verified();

       return true;
   }
}
テンプレートメソッドのコードが表示されます.以下の「クライアント」
class Client
{
  private $register;

  public function __construct(UserRegister $register)
  {
    $this->register = $register;
  }

  public function register(User $user)
  {
    // do something before registration
    $this->register->register($user);
    // do something after registration
  }
}

$client = new Client(new EmailVerificationUserRegister());
$client->register($new User());
EmailVerificationUserRegister ランダムに選択し、両方の別の具体的な実装を選択するか、依存性注入メカニズム(それは可能な限り複雑さを低く保つためにここでは報告されていない)でそれを構成することができます.

組成
私たちには2つの選択肢があります(多分、より多くがあるかもしれませんが、私が見つけたものです).後者から始まります- 2つの最悪のものですが、私はまだ共有を望みました-私は、以下のコードで終わりました:
class UserRegister 
{
   private $em;

   public function __construct(EntityManagerInterface $em) 
   {
       $this->em = $em;
   }

   final public function register(User $user, $sendVerification): void 
   {
       $user->enable();
       if (!is_callable($sendVerification) {
         return;
       }

       $verificationHasBeenSent = call_user_func_array($sendVerification, [$user]);
       if (is_bool($verificationHasBeenSent) && $verificationHasBeenSent) {
         return;
       }

       $this->em->persist($user);
       $this->em->flush();
   }
}
話すclosure コールバックとして、クロージャanonymous function ( lambdaとも呼ばれます) PHP 5.3で導入され、基本的にはコードの一部(関数、実際)です.宣言する必要はありませんが、単に"inline "を使い、定義されている場所の範囲を継承しますexplicitly binded to other context ). 継承例と同じ結果を得るために、以下のように具体的な実装の一つを変更すべきです
interface UserRegisterInterface
{
  public function register(User $user): void;
}

class EmailVerificationUserRegister implements UserRegisterInterface
{
   private $register;

   public function __construct(UserRegister $register)
   {
     $this->register = $register;
   }

   public function register(User $user)
   {
     $this->register->register($user, [$this, 'sendVerification']);
   }

   private function sendVerification(User $user): bool
   {
       $email = $user->getEmail();

       return $this->sendEmail($email);
   }

   private function sendEmail(string $email): bool
   {
      // omitted: sending email code
   }
}
注意を払うcall_user_func_array そしてその事実にUserRegister::register コールバックにはタイプミスがない.私は、何かをすることができました
class UserRegister 
{
   [...]
   final public function register(User $user, \Closure $sendVerification): void 
   {
       $user->enable();
       $verificationHasBeenSent = $sendVerification($user);
       if (is_bool($verificationHasBeenSent) && $verificationHasBeenSent) {
         return;
       }
       [...]
   }
}

class EmailVerificationUserRegister implements UserRegisterInterface 
{ 
  [...]
  public function register(User $user) 
  { 
    $this->register->register($user, function (User $user) { return $this->sendVerification($user); });
  } 
  [...]
}
でもcall_user_func_array is much safer . また、導入する必要がありましたUserRegisterInterface で使用するにはClient クラス(その変更がそのコンストラクターのTypeHinpであるだけであるので、私は二度と現れません).
第2の方法は、aに似たものを使うことですstrategy pattern 次のようになります.
interface VerificationStrategy
{
  public function sendVerification(User $user): bool;
}

class UserRegister 
{
   private $em;

   public function __construct(EntityManagerInterface $em) 
   {
       $this->em = $em;
   }

   final public function register(User $user, VerificationStrategy $strategy): void 
   {
       $user->enable();
       if (!$strategy->sendVerification($user)) {
         return;
       }

       $this->em->persist($user);
       $this->em->flush();
   }
}

class EmailVerificationUserRegister implements VerificationStrategy 
{ 
  public function sendVerification(User $user): bool 
  { 
    $email = $user->getEmail(); 

    return $this->sendEmail($email); 
  } 

  private function sendEmail(string $email): bool 
  { 
    // omitted: sending email code 
  } 
}

class Client 
{
  private $register; 

  public function __construct(UserRegister $register) 
  { 
    $this->register = $register; 
  } 

  public function register(User $user, VerificationStrategy $strategy) 
  { 
    // do something before registration 
    $this->register->register($user, $strategy); 
    // do something after registration 
  } 
} 

$client = new Client(); 
$client->register(new User(), new EmailVerificationUserRegister());

結論
要約すると、ウェブ上のどこにでも見つけることができる遺産の上の構成の利点の横で、私はあなたの注意に従うことができます.
  • 継承により、少なくとも1つのクラスから継承できます(少なくともPHPで).上の例はすばらしいですが、動作に何か変化があったら変更することは困難です.
  • 継承を使用すると、抽象クラスとすべての具体的なクラスをテストする必要があります.私の観点から、クラスのテストをスキップするだけでは、拡張モジュールが実装の詳細にすぎないので間違っています.これらのクラスに対するテストは全くない.
  • クロージャでは、呼び出し元から渡されたパラメーターが関数定義と一致することを保証することはできません.レジスタクラスがコールバックを呼び出すか、その結果を管理する方法の変更を想像してください.戻り値の型も問題です.実際、私は一貫性をチェックするためにいくつかの余分な制御コードを必要としました.
  • クロージャでは、最初の例では、それが文字列であるとしてメソッド名をリファクタリングするのも面倒です.
  • 戦略を使用すると、動作が変更された場合、コードを簡単に適応できます.MoreEeverは、コピー/貼り付けの抽象クラスのテスト(継承の場合)だけではない単位テストを持つことができます.もちろん、コードがどのように機能するかを理解するために、もう少し文脈のスイッチを使う必要がありますが、私の観点からは良いトレードオフです.
  • 以上です.
    ハッピーコーディング!🙂