使用IOC / DI改进设计

时间:2015-08-12 10:24:07

标签: c# oop dependency-injection inversion-of-control

我正在尝试使用DI / IOC为我的多模块解决方案找到更好的设计,但现在我不知何故丢失了。我有一个解决方案,可以通过不同的渠道将不同类型的实体分发给收件人。 这是我的课程的简化版本:

#region FTP Module
public interface IFtpService
{
    void Upload(FtpAccount account, byte[] data);
}

public class FtpService : IFtpService
{
    public void Upload(FtpAccount account, byte[] data)
    {
    }
}
#endregion

#region Email Module
public interface IEmailService :IDistributionService
{
    void Send(IEnumerable<string> recipients, byte[] data);
}

public class EmailService : IEmailService
{
    public void Send(IEnumerable<string> recipients, byte[] data)
    {
    }
}
#endregion

public interface IDistributionService { }
#region GenericDistributionModule

public interface IDistributionChannel
{
    void Distribute();
}

public interface IDistribution
{
    byte[] Data { get; }

    IDistributionChannel DistributionChannel { get; }

    void Distribute();
}

#endregion

#region EmailDistributionModule
public class EmailDistributionChannel : IDistributionChannel
{
    public void Distribute()
    {
        // Set some properties
        // Call EmailService???
    }

    public List<string> Recipients { get; set; } 
}

#endregion

#region FtpDistributionModule
public class FtpDistributionChannel : IDistributionChannel
{
    public void Distribute()
    {
        // Set some properties
        // Call FtpService???
    }

    public FtpAccount ftpAccount { get; set; }
}

#endregion

#region Program
public class Report
{
    public List<ReportDistribution> DistributionList { get; private set; }

    public byte[] reportData{get; set; }
}

public class ReportDistribution : IDistribution
{
    public Report Report { get; set; }

    public byte[] Data { get { return Report.reportData; } }

    public IDistributionChannel DistributionChannel { get; private set; }

    public void Distribute()
    {
        DistributionChannel.Distribute();
    }
}

class Program
{

    static void Main(string[] args)
    {
        EmailService emailService = new EmailService();
        FtpService ftpService = new FtpService();
        FtpAccount aAccount;
        Report report;

        ReportDistribution[] distributions =
        {
            new ReportDistribution(new EmailDistributionChannel(new List<string>("test@abc.xyz", "foo@bar.xyz"))),
            new ReportDistribution(new FtpDistributionChannel(aAccount))
        };
        report.DistributionList.AddRange(distributions);

        foreach (var distribution in distributions)
        {
            // Old code:
            // if (distribution.DistributionChannel is EmailDistributionChannel)
            // {
            //     emailService.Send(...);        
            // }else if (distribution.DistributionChannel is FtpDistributionChannel)
            // {
            //     ftpService.Upload(...);
            // }else{ throw new NotImplementedException();}

            // New code:
            distribution.Distribute();
        }
    }
}
#endregion

在我目前的解决方案中,可以创建和存储持久性IDistribution POCO(我在这里使用ReportDistribution)并将它们附加到可分发的实体(此处为Report例)。 例如。有人想通过电子邮件将现有Report分发给一组收件人。因此,他创建了一个新的ReportDistribution' with an EmailDistributionChannel'。后来他决定通过FTP将相同的Report分发到指定的FtpServer。因此,他创建了另一个ReportDistribution FtpDistributionChannel。 可以在相同或不同的频道上多次分发相同的Report

Azure Webjob选择存储的IDistribution个实例并分发它们。当前丑陋的实现使用if-else通过(低级别)DistributionsFtpDistributionChannelFtpService分发EmailDistributionChannels EmailService。< / p>

我现在正尝试在Distribute()FtpDistributionChannel上实现接口方法EmailDistributionChannel。但为了实现这一点,实体需要对服务的引用。通过ConstructorInjection将服务注入实体似乎被认为是不好的风格。

Mike Hadlow提出了另外三个解决方案:

  1. 创建域名服务。我可以,例如创建FtpDistributionService,注入FtpService并编写Distribute(FtpDistributionChannel distribution)方法(以及EmailDistributionService)。除了Mike提到的缺点之外,如何根据DistributionService实例选择匹配的IDistribution?用另一个替换旧的if-else感觉不对

  2. IFtpService/EMailService注入Distribute()方法。但是,我应该如何在Distribute()界面中定义IDistribution方法? EmailDistributionChannel需要IEmailServiceFtpDistributionChannel需要IFtpService

  3. 域事件模式。我不确定这如何解决我的问题。

  4. 让我试着解释为什么我想出了这个相当复杂的解决方案: 它从一个简单的报告列表开始。很快有人让我向一些收件人发送报告(并存储收件人列表)。简单!

    后来,其他人添加了向FtpAccount发送报告的要求。在应用程序中管理不同的FtpAccounts,因此也应存储所选的帐户。 这就是我添加了IDistributionChannel抽象的地步。一切都很好。

    然后有人需要通过电子邮件发送某种持久性日志文件的可能性。这导致了我的IDistribution / IDistributionChannel解决方案。 如果现在有人需要分发其他类型的数据,我可以为这些数据实现另一个IDistribution。如果需要另一个DistributionChannel(例如传真),我实现它并且它可用于所有可分发的实体。

    我真的很感激任何帮助/想法。

1 个答案:

答案 0 :(得分:0)

首先,为什么要为FtpAccount创建接口?该类是孤立的,不提供任何需要抽象的行为。

让我们从你原来的问题开始,然后从那里开始构建。问题是因为我将其解释为您希望使用不同的介质集向客户端发送内容。

通过在代码中表达它,可以这样做:

public void SendFileToUser(string userName, byte[] file)
{
    var distributions = new []{new EmailDistribution(), new FtpDistribution() };        
    foreach (var distribution in distributions)
    {
        distribution.Distribute(userName, file);
    }
}

看看我做了什么?我添加了一些上下文。因为您的原始用例是通用的。您通常不希望将任意数据分发到任意分发服务。

我所做的更改引入了一个域和一个真正的问题。

通过这种改变,我们也可以对其余的类进行建模。

public class FtpDistributor : IDistributor
{
    private FtpAccountRepository _repository = new FtpAccountRepository();
    private FtpClient _client = new FtpClient();

    public void Distribute(string userName, byte[] file)
    {
        var ftpAccount = _repository.GetAccount(userName);
        _client.Connect(ftpAccount.Host);
        _client.Authenticate(ftpAccount.userName, ftpAccount.Password);
        _Client.Send(file);
    }
}

看看我做了什么?我将跟踪FTP帐户的责任转移到实际服务上。实际上,您可能有一个管理网络或类似的帐户可以映射到特定用户。

通过这样做,我还在服务中隔离了有关FTP的所有处理,从而降低了调用代码的复杂性。

电子邮件分发商的工作方式相同。

当你开始编写这样的问题时,请尝试从top-&gt;向下。否则很容易创建一个似乎是SOLID的架构,而它并没有真正解决实际的业务问题。

<强>更新

我已经阅读了您的更新,但我不明白为什么您必须为新要求使用相同的类?

  

然后有人需要通过电子邮件发送某种持久性日志文件的可能性

这是一个完全不同的用例,应该与原始用例分开。为它创建新代码。 .NET中的SmtpClient对我们来说非常容易,不需要抽象出来。

  

如果现在有人需要分发其他类型的数据,我可以为这些数据实现另一个IDistribution。

为什么呢?你试图隐藏什么复杂性?

  

如果需要另一个DistributionChannel(例如传真),我实现它并且它可用于所有可分发的实体

没有。分配物品A与分配物品B不同。例如,您不能在空中运输工具上运输大型桥梁的部件,无论是货运船还是卡车。

我想说的是,创建过于通用的抽象/合同以促进代码重用似乎是一个好主意,但它通常只会使您的应用程序更复杂或更不易读。

当存在真正的复杂性问题时创建抽象而不是事先。