PHP 中基于字符串的工厂模式 (OOP)

Factory pattern in PHP based on a string (OOP)

我有以下场景:

用户向工厂方法发送表示连接到 MAILER 的完整信息的 URI class。例如:"smtp://user:password@server:port"。我将此连接字符串拆分为表示 "protocol"、"username"、"password"、"mail server"、"port" 等部分。通过该协议,我获得了负责发送电子邮件的邮件程序实例。所有 Mailer 实例都实现了 MailWrapperInterface。

今天我做了以下事情:

/**
 * @return MailWrapperInterface
 */
public static function mailerFactory($protocol): MailWrapperInterface
{
    if (in_array($protocol, ['smtp', 'ssl', 'tls'])) {
        $mail = new PHPMailerWrapper($connection);
    } elseif ($protocol === "ses") {
        $mail = new AmazonSesWrapper($connection);
    } elseif ($protocol === "mandrill") {
        $mail = new MandrillApiWrapper($connection);
    } elseif ($protocol === "sendmail") {
        $mail = new SendMailWrapper($connection);
    } elseif ($protocol === "mailgun") {
        $mail = new MailgunApiWrapper($connection);
    } else {
        throw new InvalidArgumentException("The $protocol is not valid");
    }

    return $mail;
}

但是我对这个实现感到不舒服,因为:

不管怎样,我知道这是错的!但是在没有上述问题(和其他问题)的情况下执行此工厂方法的最佳方法是什么?

我建议使用 public 静态方法 getMailer() 将工厂变成 class。该方法将调用以支持的协议和 return 结果命名的私有静态方法。 getMailer 方法可以测试 class 中是否存在以输入 $protocol 命名的方法,如果不存在则抛出异常。要支持新的协议,只需在 class.

中添加相应的私有静态方法即可

在这种情况下,我有时会求助于外部 pref 文件或数据库解决方案。取决于需要调用多少次数据。

我会有一个可以通过应用程序或手动编辑的文件,类似于 xml 文件。它将存储所有设置,并且可以轻松编辑以添加或删除协议。这样,您不必通过 class 扩展或硬编码 if/elseif 更改方法,您只需更新设置文件即可。

/core/prefs/protocols.xml

<?xml version="1.0" encoding="UTF-8"?>
<config>
    <protocol classname="PHPMailerWrapper">
        <arg>smtp</arg>
        <arg>ssl</arg>
        <arg>tls</arg>
    </protocol>
    <protocol classname="AmazonSesWrapper">
        <arg>ses</arg>
    </protocol>
    <protocol classname="MandrillApiWrapper">
        <arg>mandril</arg>
    </protocol>
</config>

改变您目前使用的方法

# I would add this function to fetch that file and convert it to object
private static function getEngines()
    {
        return simplexml_load_file('core/prefs/protocols.xml');
    }
# I would alter this method
public static function mailerFactory($protocol)
{
    # Get xml prefs
    $XMLEngine  =   self::getEngines();
    # Loop through protocols
    foreach($XMLEngine as $obj) {
        # Check if protocol in current object
        if(!in_array($protocol,json_decode(json_encode($obj->arg),true)))
            continue;
        # Get class name arra
        $classArr   =   (array) $obj->attributes()->classname;
        # Set name
        $class      =   $classArr[0];
        # Create variabled class
        return new $class($connection);
    }
    # Throw if all else fails...
    throw new InvalidArgumentException("The $protocol is not valid");
}

无论如何,这只是一个想法。我经常这样做来自动化我的网络应用程序。

总的来说,您的解决方案比您想象的要好。工厂的要点是将 类 的实例化转移到可以与您的代码库隔离的某些资源。

但是你的代码有一个大问题:你的工厂是静态的。这意味着,如果不重写使用过它的所有其他代码,您就无法实际替换或扩展它。

至于那个丑陋的 if-else 块,正如 Rasclatt 已经提到的那样,解决方案只是使用配置文件。

class MailerFactory {

    private $config = [];

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

    public function create($uri) {
        $parts = $this->splitProtocolFunctionSomething($uri);
        $protocol = $parts['protocol'];
        $connection = $this->makeTheConnectionSomehow();

        if (!array_key_exists($protocol, $this->config)) {
            throw new InvalidArgumentException("The $protocol is not valid");
        }

        $class = $this->config[$protocol]['classname'];
        return new $class($connection)
    }
}

然后你就可以通过写来使用它:

$factory = new MailerFactory(json_decode('/path/to/mailers.config.json'));
$mailer = $factory->create('smtp');

这样您就可以将此工厂作为依赖项传递给代码中所有需要它的实例。并且,在编写单元测试时,您可以将此工厂替换为一些模拟或测试替身。

替代方法

您在这里也可能有不同的选择。你可以重写你的代码,这样你就不再依赖工厂,而是使用 DI 容器(比如 Auryn or Symfony DI)。

这样,需要您的邮件程序的代码在实例化时就已经在构造函数中传递了一个可用的代码。

我提出这个替代方案的原因是,从我的立场来看,您的包装器可能最终需要不同的参数。在那种情况下,使用工厂变得有点.. emm .. 笨拙。