Выбор типа внедрения зависимости
Недавно я столкнулся с проблемой, когда мне нужно выбрать тип на основе параметра. Например: класс, используемый для отправки уведомлений, который должен выбрать правильный канал (электронная почта, смс, ...) на основе входного параметра.
Я выгляжу примерно так:
public class NotificationManager
{
IEmail _email;
ISms _sms;
public NotificationManager (IEmail email, ISMS sms)
{
_email = email;
_sms = sms;
}
public void Send(string type)
{
switch(type)
{
case "email":
_email.send;
break;
case "sms":
_sms.send;
break;
}
}
}
Проблема здесь в том, что, когда я использую этот вид конструкции, конструктор быстро становится очень большим со всеми различными способами отправки уведомлений.
Мне действительно не нравится это, и это делает юнит-тестирование этого блока выбора неуправляемым.
Я не могу просто сказать new email();
потому что электронная почта типа уведомления будет полагаться на IEmailManager, и это только решит проблему.
Есть ли какая-то модель, которая будет делать то же самое, но лучше и чище?
1 ответ
Я хотел бы предложить вам объединить ваши IEmail
а также ISms
интерфейсы в IMessageService
(при условии, что это не нарушает принципа подстановки Лискова) и использовать шаблон стратегии, чтобы ваша служба уведомлений могла выбирать тип (или типы) IMessageService
которые используются.
Рефакторинг IMessageService
public interface IMessageService
{
void Send(string subject, string body);
bool AppliesTo(IEnumerable<string> providers);
}
public class EmailMessageService : IMessageService
{
public EmailMessageService(/* inject dependencies (and configuration) here */)
{
// Set dependencies to private (class level) variables
}
public void Send(string subject, string body)
{
// Implementation - use dependencies as appropriate
}
public bool AppliesTo(IEnumerable<string> providers)
{
return providers.Contains("email");
}
}
public class SmsMessageService : IMessageService
{
public SmsMessageService(/* inject dependencies (and configuration) here */)
{
// Set dependencies to private (class level) variables
}
public void Send(string subject, string body)
{
// Implementation - use dependencies as appropriate
}
public bool AppliesTo(IEnumerable<string> providers)
{
return providers.Contains("sms");
}
}
Реализуйте шаблон стратегии
public interface IMessageStrategy
{
void Send(string message, string body, string provider);
void Send(string message, string body, IEnumerable<string> providers);
}
public class MessageStrategy : IMessageStrategy
{
private readonly IMessageService[] messageServices;
public MessageStrategy(IMessageService[] messageServices)
{
if (messageServices == null)
throw new ArgumentNullException("messageServices");
this.messageServices = messageServices;
}
public void Send(string message, string body, string provider)
{
string[] providers = provider.Split(';').Select(p => p.ToLower().Trim()).ToArray();
this.Send(message, body, providers);
}
public void Send(string message, string body, IEnumerable<string> providers)
{
foreach (IMessageService messageService in messageServices)
{
if (messageService.AppliesTo(providers))
{
messageService.Send(message, body);
}
}
}
}
использование
В вашем контейнере DI зарегистрируйте все типы, которые соответствуют IMessageService
быть решенным как массив. Например, в StructureMap:
container.For<IMessageService>().Use<EmailMessageService>();
container.For<IMessageService>().Use<SmsService>();
Или же вы можете использовать Scan для автоматического выбора новых типов, которые добавляются после факта.
var container = new Container(x => x.Scan(scan =>
{
scan.TheCallingAssembly();
scan.WithDefaultConventions();
scan.AddAllTypesOf<IMessageService>();
}));
В любом случае, регистрация типов в контейнере - это все, что вам нужно для IMessageService[]
зависимость.
Тогда это просто вопрос инъекций IMessageStrategy
в класс, который требует обмена сообщениями и передачи волшебной строки, чтобы выбрать, какие типы служб сообщений использовать.
public class SomeService : ISomeService
{
private readonly IMessageStrategy messageStrategy;
public SomeService(IMessageStrategy messageStrategy)
{
if (messageStrategy == null)
throw new ArgumentNullException("messageStrategy");
this.messageStrategy = messageStrategy;
}
public void DoSomething()
{
// Send a message via email
this.messageStrategy.Send("This is a test", "Hello", "email");
// Send a message via SMS
this.messageStrategy.Send("This is a test", "Hello", "sms");
// Send a message via email and SMS
this.messageStrategy.Send("This is a test", "Hello", "email;sms");
}
}
Обратите внимание, что если вы выберете такой подход, ваш EmailStrategy
класс не нужно будет менять, если вы решите позже добавить или удалить IMessageService
- вам нужно только изменить конфигурацию DI.