【问题标题】:Am I breaking the DI pattern rules我是否违反了 DI 模式规则
【发布时间】:2019-05-04 14:06:08
【问题描述】:

我对 DI 很陌生,所以我的知识并不是最丰富的。我目前正在开发一个 Asp.Net Core MVC 应用程序。

假设我有一个返回 DataSet 的 DLL(我可以完全控制这个 DLL 并且可以根据需要更改它)。

所以 DLL 中的代码如下所示:

public class DataStore : IDataStore
{       
   private readonly IConfiguration _configuration;       

   public DataStore(IConfiguration configuration)
   {
       _configuration = configuration;
   }

   public DataSet GetDataBySP(string spName, SqlParameter[] parameters = null)
   {
      DataSet ds = new DataSet();
      using(var con = new SqlConnection(_configuration.GetConnectionString("conStr")))
      {
         using(var cmd = new SqlCommand(spName, con))
         {
              cmd.CommandType = CommandType.StoredProcedure;

              if (parameters?.Length > 0)
                 cmd.Parameters.AddRange(parameters);

              con.Open();

              using (var da = new SqlDataAdapter(cmd))
              {
                 da.Fill(ds);
              }
         }
      }
      return ds;
   }
}

现在假设我有一个名为 Foo 的类,它只包含如下两个属性:

public interface IFoo
{
   Foo GetFooData();
}

public class Foo : IFoo
{
    private readonly IDataStore _dataStore;

    public int CurrentCount { get; set; }
    public int MaxCount { get; set; }

    public Foo(DataRow dr)
    {
        CurrentCount = dr.Field<int>("CurrentCount");
        MaxCount = dr.Field<int>("MaxCount");
    }

    public Foo(IDataStore dataStore)
    {
        _dataStore = dataStore;
    }
}

Foo我有一个叫GetFooData的方法如下:

public Foo GetFooData()
{
    var ds = _dataStore.GetDataBySP("sp name");

    //is the below code breaking the DI pattern rule?
    return ds.Tables[0].AsEnumberable().Select(f => new Foo(f)).FirstOrDefault();
}

Startup.cs 内的 ConfigureServices(IServiceCollection services) 方法中,我执行以下操作:

services.AddScoped<IFoo, Foo>();

所以我的问题是,通过在 Select() 中执行 new Foo(f) 是否打破了 DI 模式?如果是的话,请您建议如何克服这个问题。

【问题讨论】:

  • 你应该问问自己Foo-instance 是否真的应该创建其他实例。我怀疑它应该,而不是你可能有一个工厂来做这项工作。
  • @HimBromBeere 我在Startup.cs 中调用services.AddScoped&lt;IFoo, Foo&gt;()。你说的是工厂吗?
  • 这里没有 DI 开始。其次,由于某种原因,模式问题通常是题外话,最后,有很多教程和博客详细介绍了 DI 和 IOC,与 Q 和 A 格式相比,您将在那里获得更好的信息网站
  • 这可能只是问题的表达方式,但看起来Foo 正在破坏 SRP - 单一责任原则。这让我想知道这是不是XY problem
  • 只需在 FooFactory 中移动 GetFooData 方法。这样就干净了。

标签: c# dependency-injection .net-core asp.net-core-mvc


【解决方案1】:

我会通过使用 repositories数据传输对象 (DTO) 解决您的问题,如下所示:

您可以将其注入代码中的一些公共接口:

    public interface IFooRepository
    {
        FooDto GetFirst();
    }

    public interface IDataSetFactory
    {
        DataSet GetDataSet(string spName, SqlParameter[] = null);
    }

一些 DTO 将数据从一个类传递到另一个类(并且只有数据):

    public class FooDto
    {
        public int CurrentCount { get; set; }
        public int MaxCount { get; set; }
    }

一些内部实现,隐藏在界面用户的眼睛之外:

    internal sealed class FooRepository : IFooRepository
    {
        private readonly IDataSetFactory _dsFactory;
        public FooRepository(IDataSetFactory dsFactory)
        {
            _dsFactory = dsFactory;
        }

        public FooDto GetFirst()
        {
            return _dsFactory.GetDataSet("sp name")
                             .Tables[0]
                             .AsEnumberable()
                             .Select(Map)
                             .FirstOrDefault();
        }
        private FooDto Map(DataRow dr)
        {
            return new FooDto
                   {
                       CurrentCount = dr.Field<int>("CurrentCount"),
                       MaxCount = dr.Field<int>("MaxCount")
                   };
        }
    }

    internal sealed class DataSetFactory : IDataSetFactory
    {
        public DataSetFactory() { }
        private DataSet GetDataSet(string spName, SqlParameter[] = null)
        {
            //set up sql parameters, open connection
            return ds;
        }
    }

给定一些特定的 DI 框架(Ninject、DryIoc 等),您可以将这些接口注入代码中的任何位置。使用 DI 的巨大好处 - 它迫使您正确设计应用程序(接口与实现分离),因此您可以在某个时间点实现您的 IFooRepository 以从天空中的星星读取数据,或者从文件中返回,或者在测试中返回空结果,或者任何你想做的事情。

如果您设计不正确 - 它可能无法编译,或者会过于复杂。正确使用 DI 是个难题,但估计 99% 的 *.cs 文件不会引用任何 DI 框架引用:

using System.IO;
using Ninject; //<-- I mean, this one. If it is everywhere, something is overcomplexed and you are not using framework correctly.
using System.Media;

【讨论】:

  • 谢谢你。我会把所有的 crud 操作放在DataSetFactory 类中吗?还是我必须为每个方法创建一个类,即CreateRecord(), UpdateRecord(), DeleteRecord(),并使GetDataSet private 不会引发错误?
  • 为每个表创建存储库,甚至更好地为每个对象(业务对象)创建存储库。它将包含创建/读取/更新/删除方法以及特定的方法(如 CountItems、SumPrices、GetValidInvoices 等)。工厂应该保持原样,除了提供特定数据集之外没有任何其他逻辑。
  • DataSetFactory 中创建/读取/更新/删除以及每个存储库类的特定方法是否没有意义?或者这是否变得过于笼统而违背了整个目的?
  • 这取决于你想要多少抽象。如果它们对于所有逻辑都相同(我非常怀疑),您可以将其放入名为 CommonRepository 的类中并改用它。大多数情况下,这些操作与具体的业务对象(发票、学校、用户等)相关。比如根本不可能有Update或者Delete操作,只有Create和Read,或者组合,比如GetOrCreate、CreateOrUpdate、DeleteIfExist等。所以,不,即使有抽象,DataSetFactory也只负责建立连接,不适用于状态变化。
【解决方案2】:

是的,你违反了 DI 的精神,但可能不是你想的那样。

正如其中一位 cmets 指出的那样,您可能违反了 Foo 的 SRP。您曾经说过它“仅包含两个属性”,但它包含逻辑并获取并存储DataStore 引用。如果这两个属性真的是 Foo 包含的全部,那么它本质上就是一个 DTO ... 一个没有行为或业务规则的愚蠢的属性包,在 那种 的情况下,我想说你并没有仅仅通过调用,本质上是new Foo(properties) 来违反 DI 的精神......因为匿名对象或元组可以服务于相同的目的......它本质上会将 Foo 减少为 content。 DI 不是关于“从不调用new”......它是关于逻辑代码不解决其自身的依赖关系以执行其逻辑。

由于在您的示例中,您还让 Foo 负责查询 DataSource 以查找具有这些属性的记录,因此您确实在 Foo 内部拥有该数据检索逻辑,并且你肯定会进入一些灰色地带。如前所述,您的班级现在有两个不同且不相交的职责。您开始接近 DDD / 富域概念,Foo 将负责传输其自己的数据处理有关操作该数据的业务问题。但是DDD is not the most DI-friendly of architectures 通常被认为更适合大型和复杂的域而不是小型域。

DI 的整体理念是代码单元(类)不应该解决它们自己的依赖关系,以帮助管理/组织/降低复杂性。如果您将Foo 上的GetFooData() 方法分解为DI 提供的 的其他类,并将Foo 简化为一个简单的DTO/属性包,那么我认为您至少会有一个论证您没有违反 DI 的精神,因为 Foo 不是将 DataRow 转换为 Foo 的代码的行为依赖项。

【讨论】:

  • 这是一个非常好的答案,显示了主题的正确观点。
猜你喜欢
  • 2018-07-03
  • 2016-12-29
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-04-11
  • 1970-01-01
  • 2020-07-11
相关资源
最近更新 更多