【问题标题】:Improve design with IOC/DI使用 IOC/DI 改进设计
【发布时间】:2015-08-12 10:24:07
【问题描述】:

我目前正在尝试使用 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 anEmailDistributionChannel'。后来他决定通过 FTP 将相同的Report 分发到指定的 FtpServer。因此,他用FtpDistributionChannel 创建了另一个ReportDistribution。 可以在相同或不同的频道上多次分发相同的Report

Azure Webjob 获取存储的 IDistribution 实例并分发它们。当前,丑陋的实现使用 if-else 通过(低级)FtpServiceEmailDistributionChannelsEmailService 分发 DistributionsFtpDistributionChannel

我现在正在尝试在FtpDistributionChannelEmailDistributionChannel 上实现接口方法Distribute()。但要使其正常工作,实体需要对服务的引用。通过 ConstructorInjection 将服务注入实体似乎被认为是不好的风格。

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

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

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

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

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

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

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

非常感谢任何帮助/想法。

【问题讨论】:

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


    【解决方案1】:

    首先,你为什么要为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 的所有处理隔离到服务内,从而降低了调用代码的复杂性。

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

    当您开始编写这样的问题时,请尝试从上到下。否则很容易创建一个看似 SOLID 的架构,但它并不能真正解决实际的业务问题。

    更新

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

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

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

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

    为什么?你想隐藏什么复杂性?

    如果需要另一个 DistributionChannel(例如 Fax),我会实施它,它适用于所有可分发实体

    没有。分发东西 A 与分发东西 B 不同。例如,您不能在飞机上运输一座大型桥梁的一部分,需要货船或卡车。

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

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

    【讨论】:

    • 谢谢jgauffin!我认为我最初的示例代码过于简单。我将其更改为更接近我的实际实现,并添加了一些额外的 cmets。您的代码看起来很有趣,但我不确定它是否符合我的要求。也许你可以再看看我编辑的问题。
    • 我现在感觉很愚蠢,但我想我仍然不太了解您提出的解决方案。我的 Distribution/EmailDistribution/FtpDistribution-Stuff 是在单个项目/nuget 包中开发的。因此很容易将分发功能添加到另一个应用程序。我不想隐藏复杂性,但我想创建可重用的模块。你有什么建议?创建一个抽象的AbstractReportDistribution 类,继承ReportEmailDistributionReportFtpDistributionReportFaxDistribution 类以及AbstractLogDistribution 和后代?
    • ... 如果我需要添加另一个可分发,我必须复制和粘贴所有分发选项的代码?如果我需要另一个分发选项(例如 Dropbox),我必须为所有可分发文件实现XYZDropboxDistributionclasses?附言IDistributable 有一些额外的属性,如日期、分发者、分发状态......我非常感谢你的帮助,但今天我不明白你的意思......
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多