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

时间:2017-05-28 01:08:56

标签: php oop factory-pattern

我有以下情况:

用户向工厂方法发送URI,该URI表示连接到MAILER类的完整信息。例如:“smtp:// user:password @ 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;
}

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

  • 代码非常耦合
  • 如果不更改工厂方法,我就无法进行新的实施

无论如何,我知道这是错的!但是,没有上述问题(以及其他问题?)

,这种工厂方法的最佳方法是什么?

3 个答案:

答案 0 :(得分:3)

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

但是你的代码存在很大问题:你的工厂是静态的。这意味着,你没有办法实际替换它或扩展它而不重写其中已经使用过的所有其他代码。

对于那个丑陋的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容器(如AurynSymfony DI)。

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

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

答案 1 :(得分:1)

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

我会有一个可以由应用程序编辑或手动编辑的文件,类似于xml文件。它将存储所有设置,并且可以轻松编辑以添加或删除协议。这样,您不必通过类扩展或硬编码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");
}

无论如何,这只是一个想法。我做了很多工作来自动化我的网络应用程序。

答案 2 :(得分:0)

我建议使用公共静态方法getMailer()将工厂变成一个类。该方法将调用以支持的协议命名的私有静态方法并返回其结果。 getMailer方法可以测试在为输入$ protocol命名的类中是否存在方法,如果没有抛出异常。要支持新协议,只需在类中添加相应的私有静态方法。