使用IOC/DI改进设计

本文关键字:DI IOC 使用 | 更新日期: 2023-09-27 18:07:32

我目前正在尝试使用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。因此,他用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替换我的旧if-else感觉不对

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

  3. 域事件模式。我不知道这怎么能解决我的问题

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

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

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

使用IOC/DI改进设计

首先,为什么要为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帐户的责任转移到实际服务中。在现实中,您可能有一个管理web或类似的网站,可以将帐户映射到特定的用户。

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

电子邮件分发器将以同样的方式工作。

当你开始编写这样的问题时,试着从上到下。否则很容易创建一个看起来很坚固的架构,但它并不能真正解决实际的业务问题。

我读了你的更新,我不明白为什么你必须使用相同的类来满足新的要求?

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

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

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

为什么?你想要隐藏的复杂性是什么?

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

。分配物品A和分配物品b是不一样的。例如,你不能用飞机运输一座大桥的一部分,要么需要货船,要么需要卡车。

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

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