У меня есть следующий сценарий:
Пользователь отправляет методу фабрики URI, который представляет полную информацию для соединения с классом MAILER. Например: «smtp: // пользователь: пароль @ сервер: порт». Я разбил эту строку подключения на части, которые представляют «протокол», «имя пользователя», «пароль», «почтовый сервер», «порт» и так далее. С протоколом я получаю экземпляр почтовой программы, который будет отвечать за отправку электронной почты. Весь экземпляр 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;
}
Но я не чувствую себя комфортно с этой реализацией, потому что:
Во всяком случае, я знаю, что это так неправильно! Но каков наилучший способ сделать этот фабричный метод без проблем, описанных выше (и других?)
В целом ваши решения лучше, чем вы думаете. Задача фабрики — перенести создание экземпляров классов на некоторый ресурс, который может быть изолирован от вашей кодовой базы.
Но есть большая проблема с вашим кодом: ваша фабрика статична. Это означает, что у вас нет способа фактически заменить или расширить его, не переписывая каждый второй фрагмент кода, где он использовался.
Что касается этого уродливого 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(jons_decode('/path/to/mailers.config.json'));
$mailer = $factory->create('smtp');
Таким образом, вы можете обойти эту фабрику как зависимость от всех экземпляров в вашем коде, которым это потребуется. И, при написании модульных тестов, вы можете заменить эту фабрику на какую-то ложную или двойную проверку.
У вас также могут быть другие варианты здесь. Вы можете переписать свой код, чтобы не зависеть от заводов, а вместо этого использовать DI-контейнер (например, Auryn или же Symfony DI).
Таким образом, код, которому нужны ваши почтовые программы, уже будет иметь пригодный для использования код, передаваемый в конструктор, когда он создается.
Причина, по которой я поднимаю эту альтернативу, заключается в том, что с того места, где я стою, похоже, что ваши упаковщики могут в конечном итоге потребовать другие параметры. И в этом случае использование фабрики становится несколько .. эмм … громоздким.
В таких ситуациях я иногда прибегаю к внешним файлам 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");
}
Во всяком случае, это просто идея. Я делаю это много для автоматизации моих веб-приложений.
Я предлагаю превратить фабрику в класс с помощью открытого статического метода getMailer (). Этот метод вызывает частные статические методы, названные в честь поддерживаемых протоколов, и возвращает их результат. Метод getMailer может проверить, существует ли метод в классе, названном для протокола input $, и, если нет, вызвать исключение. Для поддержки новых протоколов просто добавьте соответствующие частные статические методы в класс.