使用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通过(低级)FtpService
和EmailDistributionChannels
与EmailService
来分发Distributions
和FtpDistributionChannel
。
我现在试图在FtpDistributionChannel
和EmailDistributionChannel
上实现接口方法Distribute()
。但是要做到这一点,实体需要一个对服务的引用。通过ConstructorInjection将服务注入实体似乎被认为是糟糕的风格。
Mike Hadlow提出了另外三个解决方案:
创建域服务。例如,我可以创建一个
FtpDistributionService
,注入一个FtpService
并编写一个Distribute(FtpDistributionChannel distribution)
方法(也是一个EmailDistributionService
)。除了Mike提到的缺点,我如何根据IDistribution
实例选择一个匹配的DistributionService
呢?用另一个if-else替换我的旧if-else感觉不对将
IFtpService/EMailService
注入Distribute()
方法。但是我应该如何在IDistribution
接口中定义Distribute()
方法呢?EmailDistributionChannel
需要IEmailService
,而FtpDistributionChannel
需要IFtpService
。域事件模式。我不知道这怎么能解决我的问题
让我试着解释一下为什么我想出了这个相当复杂的解决方案:它从一个简单的报表列表开始。不久,有人要求我将报告发送给一些收件人(并存储收件人列表)。简单!
后来,其他人添加了将报告发送到FtpAccount的需求。在应用程序中管理不同的FtpAccounts,因此也应该存储所选的帐户。这就是我添加IDistributionChannel抽象的地方。一切都还好。
然后有人需要通过电子邮件发送某种持久的日志文件的可能性。这导致了我使用IDistribution/IDistributionChannel的解决方案。如果现在有人需要分发其他类型的数据,我可以为这个数据实现另一个IDistribution。如果需要另一个DistributionChannel(例如Fax),我将实现它,并且它可用于所有可分发实体。
首先,为什么要为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是不一样的。例如,你不能用飞机运输一座大桥的一部分,要么需要货船,要么需要卡车。
我想说的是,创建过于通用的抽象/契约来促进代码重用似乎是个好主意,但它通常只会使你的应用程序更复杂或可读性更差。
在存在真正的复杂性问题时创建抽象,而不是事先创建。