降低圈复杂度的设计模式

Design pattern to reduce cyclomatic complexity


我必须通过相关客户的 API.
向客户发送发票 每个客户都可以选择联系方式(电子邮件、传真、短信...)。
我的问题是每次添加新的 "contact type" 都会增加脚本的圈复杂度。
这是我的代码:
<?php

interface ExternalApi
{
    public function sendByEmail();
    public function sendByFax();
    public function sendBySMS();
    // more sending modes ...
}

class Api1 implements ExternalApi
{   
    public function sendByEmail()
    {
        echo __CLASS__." sending email<br>\n";
    }
    
    public function sendByFax()
    {
        echo __CLASS__." sending fax<br>\n";
    }
    
    public function sendBySMS()
    {
        echo __CLASS__." sending SMS<br>\n";
    }
}

class Customer
{
    public const EMAIL = 'email';
    public const FAX = 'fax';
    public const SMS = 'sms';
    public const EMAIL_AND_FAX = 'email_and_fax';
    public const PHONE = 'phone';
    public const PLANE = 'plane';
    public const BOAT = 'boat';
    public const SATELITE = 'satelite';
    public const CAB = 'cab';
    // more contact types...
    
    public ?string $contactType;
}

class Invoice
{
    public Customer $customer;
    
    public function __construct(Customer $customer)
    {
        $this->customer = $customer;
    }
}

class InvoiceSender
{
    private ExternalApi $api;

    public function __construct(ExternalApi $api)
    {
        $this->api = $api;
    }
    
    public function send(Invoice $invoice)
    {
        switch($invoice->customer->contactType) {
            case Customer::EMAIL :
                $this->api->sendByEmail();
                break;
            case Customer::FAX :
                $this->api->sendByFax();
                break;
            case Customer::SMS :
                $this->api->sendBySMS();
                break;
            case Customer::EMAIL_AND_FAX:
                $this->api->sendByEmail();
                $this->api->sendByFax();
                break;
            // more cases ...
        }
    }
}

$customer = new Customer();
$customer->contactType = Customer::EMAIL_AND_FAX;
$invoice = new Invoice($customer);
$api = new Api1();
$invoiceSender = new InvoiceSender($api);
$invoiceSender->send($invoice);

正如你所看到的,每次我添加一个新的“联系人类型”时,InvoiceSender::send 的 switch 语句都会增加。
你知道哪种设计模式可以解决这个问题吗?

使用 sendshouldSend 方法创建类似于 SendInvoiceInterface 的界面。然后创建3个实现:

  • EmailInvoiceSender
  • FaxInvoiceSender
  • SMSInvoiceSender

接下来将这些作为数组全部注入到您的 InvoiceSender 中。然后,您可以循环遍历所有发件人,如果他们 shouldSend,则 'ask' 他们可以代替 switch-case。如果计算结果为真,您可以对它们调用 send

这样逻辑就保留在每个发件人内部,而不是不断增长的开关盒。

谢谢大家的指点。
你关于使用包含所有服务的数组的建议让我想到了另一个解决方案:

<?php

interface ExternalApi
{
    public function getSendingModes();
}

class Api1 implements ExternalApi
{   
    public function getSendingModes()
    {
        return [
            Customer::EMAIL => fn() => $this->sendByEmail(),
            Customer::FAX => fn() => $this->sendByFax(),
            Customer::SMS => fn() => $this->sendBySMS(),
            Customer::EMAIL_AND_FAX => function() {
                $this->sendByEmail();
                $this->sendByFax();
            },
        ];
    }
    
    private function sendByEmail()
    {
        echo __CLASS__." sending email<br>\n";
    }
    
    private function sendByFax()
    {
        echo __CLASS__." sending fax<br>\n";
    }
    
    private function sendBySMS()
    {
        echo __CLASS__." sending SMS<br>\n";
    }
}

class Customer
{
    public const EMAIL = 'email';
    public const FAX = 'fax';
    public const SMS = 'sms';
    public const EMAIL_AND_FAX = 'email_and_fax';
    public const PHONE = 'phone';
    public const PLANE = 'plane';
    public const BOAT = 'boat';
    public const SATELITE = 'satelite';
    public const CAB = 'cab';
    // more contact types...
    
    public ?string $contactType;
}

class Invoice
{
    public Customer $customer;
    
    public function __construct(Customer $customer)
    {
        $this->customer = $customer;
    }
}

class InvoiceSender
{
    private ExternalApi $api;
    private array $sendingModes;

    public function __construct(ExternalApi $api)
    {
        $this->api = $api;
        $this->sendingModes = $api->getSendingModes();
    }
    
    public function send(Invoice $invoice)
    {
        $sendingMode = $this->sendingModes[$invoice->customer->contactType] ?? null;
        if (null === $sendingMode) {
            throw new RuntimeException("API ".get_class($this->api)." doesn't have sending mode ".$invoice->customer->contactType);
        }
        $sendingMode();
    }
}

$customer = new Customer();
$customer->contactType = Customer::EMAIL_AND_FAX;
//$customer->contactType = Customer::EMAIL;
//$customer->contactType = Customer::BOAT;
$invoice = new Invoice($customer);
$api = new Api1();
try {
    $invoiceSender = new InvoiceSender($api);
    $invoiceSender->send($invoice);
} catch (Exception $ex) {
    echo $ex->getMessage()."\n";
}

我更改了 ExternalApi 接口以检索一组发送模式。
这样做,每个 API 都知道自己的发送模式,因此每个 API 不需要实现它们不处理的发送模式。
数组的每个值都是一个匿名函数,它触发 API class.
的私有方法 此外,我不必为每种发送模式编写额外的 classes。