目录
介绍
要求
第一次尝试
第二次尝试
最终尝试
IoC容器
依赖注入和IoC乍一看似乎很复杂,但是它们很容易学习和理解。
在本文中,我们将通过在C#中重构一个非常简单的代码示例来说明依赖注入和IoC容器。
构建一个允许用户查看可用产品并按名称搜索产品的应用程序。
我们将从创建分层架构开始。使用分层体系结构有多种好处,但是由于我们专注于依赖注入,因此我们将不在本文中列出它们。
下面是该应用程序的类图:
首先,我们将从创建一个Product类开始:
public class Product
{
public Guid Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
然后,我们将创建数据访问层:
public class ProductDAL
{
private readonly List _products;
public ProductDAL()
{
_products = new List
{
new Product { Id = Guid.NewGuid(), Name= "iPhone 9",
Description = "iPhone 9 mobile phone" },
new Product { Id = Guid.NewGuid(), Name= "iPhone X",
Description = "iPhone X mobile phone" }
};
}
public IEnumerable GetProducts()
{
return _products;
}
public IEnumerable GetProducts(string name)
{
return _products
.Where(p => p.Name.Contains(name))
.ToList();
}
}
然后,我们将创建业务层:
public class ProductBL
{
private readonly ProductDAL _productDAL;
public ProductBL()
{
_productDAL = new ProductDAL();
}
public IEnumerable GetProducts()
{
return _productDAL.GetProducts();
}
public IEnumerable GetProducts(string name)
{
return _productDAL.GetProducts(name);
}
}
最后,我们将创建UI:
class Program
{
static void Main(string[] args)
{
ProductBL productBL = new ProductBL();
var products = productBL.GetProducts();
foreach (var product in products)
{
Console.WriteLine(product.Name);
}
Console.ReadKey();
}
}
我们在第一次尝试中编写的代码可以正常工作,但是有一些问题:
较高级别的对象不应依赖于较低级别的对象。两者都必须依赖抽象。那么,抽象是什么?
抽象是功能的定义。在我们的例子中,业务层依赖于数据访问层来检索书籍。在C#中,为了实现抽象,我们使用接口。接口表示功能的抽象。
因此,让我们创建抽象。
下面是数据访问层的抽象:
public interface IProductDAL
{
IEnumerable GetProducts();
IEnumerable GetProducts(string name);
}
我们还需要更新数据访问层:
public class ProductDAL : IProductDAL
我们还需要更新业务层。实际上,我们将不依赖于数据访问层的实现,而是将业务层更新为依赖于数据访问层的抽象:
public class ProductBL
{
private readonly IProductDAL _productDAL;
public ProductBL()
{
_productDAL = new ProductDAL();
}
public IEnumerable GetProducts()
{
return _productDAL.GetProducts();
}
public IEnumerable GetProducts(string name)
{
return _productDAL.GetProducts(name);
}
}
我们还必须创建业务层的抽象:
public interface IProductBL
{
IEnumerable GetProducts();
IEnumerable GetProducts(string name);
}
我们也需要更新业务层:
public class ProductBL : IProductBL
最后,我们必须更新UI:
class Program
{
static void Main(string[] args)
{
IProductBL productBL = new ProductBL();
var products = productBL.GetProducts();
foreach (var product in products)
{
Console.WriteLine(product.Name);
}
Console.ReadKey();
}
}
我们在第二次尝试中完成的代码可以工作,但是我们仍然依赖于数据访问层的具体实现:
public ProductBL()
{
_productDAL = new ProductDAL();
}
那么,如何解决呢?这是依赖注入模式起作用的地方。
到目前为止,我们所做的所有工作都不是依赖注入。
为了在没有具体实现的情况下使作为较高层对象的业务层依赖于较低层对象的功能,其他人必须创建该类。其他人必须提供下层对象的具体实现,这就是我们所说的依赖注入。从字面上看,这意味着我们正在将依赖对象注入到更高级别的对象中。实现依赖注入的一种方法是使用构造函数依赖注入。
因此,让我们更新业务层:
public class ProductBL : IProductBL
{
private readonly IProductDAL _productDAL;
public ProductBL(IProductDAL productDAL)
{
_productDAL = productDAL;
}
public IEnumerable GetProducts()
{
return _productDAL.GetProducts();
}
public IEnumerable GetProducts(string name)
{
return _productDAL.GetProducts(name);
}
}
基础结构必须提供对实现的依赖:
class Program
{
static void Main(string[] args)
{
IProductBL productBL = new ProductBL(new ProductDAL());
var products = productBL.GetProducts();
foreach (var product in products)
{
Console.WriteLine(product.Name);
}
Console.ReadKey();
}
}
创建数据访问层的控件已集成到基础架构中。这也称为控制反转。我们不是在业务层中创建数据访问层的实例,而是在基础架构内部(即Main方法)创建它。Main方法会将实例注入业务逻辑层。因此,我们将低层对象的实例注入到高层对象的实例中。因此,这称为依赖注入。
现在,如果我们看一下代码,我们仅依赖于业务访问层中数据访问层的抽象,而业务访问层是数据访问层实现的接口。因此,我们遵循上层对象和下层对象都依赖于抽象的原理,抽象是上层对象和下层对象之间的契约。
现在,我们可以有不同的团队在不同的层次上工作。我们可以有一个团队在数据访问层上工作,一个团队在业务层上工作,一个团队在UI上工作。
然后是可维护性和可扩展性的好处。例如,如果我们要为SQL Server创建一个新的数据访问层,则只需破坏数据访问层的抽象并将实例注入基础结构中即可。
最后,源代码现在可以测试了。由于我们在各处使用接口,因此我们可以轻松地在较低的单元测试中提供另一种实现。这意味着较低的测试将更容易设置。
现在,让我们测试业务层。我们将使用xUnit进行单元测试,并使用Moq模拟数据访问层。
以下是业务层的单元测试:
public class ProductBLTest
{
private readonly List _products = new List
{
new Product { Id = Guid.NewGuid(), Name= "iPhone 9",
Description = "iPhone 9 mobile phone" },
new Product { Id = Guid.NewGuid(), Name= "iPhone X",
Description = "iPhone X mobile phone" }
};
private readonly ProductBL _productBL;
public ProductBLTest()
{
var mockProductDAL = new Mock();
mockProductDAL
.Setup(dal => dal.GetProducts())
.Returns(_products);
mockProductDAL
.Setup(dal => dal.GetProducts(It.IsAny()))
.Returns(name => _products.Where(p => p.Name.Contains(name)).ToList());
_productBL = new ProductBL(mockProductDAL.Object);
}
[Fact]
public void GetProductsTest()
{
var products = _productBL.GetProducts();
Assert.Equal(2, products.Count());
}
[Fact]
public void SearchProductsTest()
{
var products = _productBL.GetProducts("X");
Assert.Single(products);
}
}
您可以看到,使用依赖注入很容易设置单元测试。
容器仅仅是有助于实现依赖注入的东西。容器通常实现三种不同的功能:
让我们实现一个简单的容器来注册映射以及创建对象。
首先,我们需要一个存储映射的数据结构。我们将选择Hashtable。该数据结构将存储映射。
首先,我们将在Container的构造函数中初始化Hashtable。然后,我们将创建一个注册映射的RegisterTransient方法。最后,我们将创建一个将创建对象的Create方法:
public class Container
{
private readonly Hashtable _registrations;
public Container()
{
_registrations = new Hashtable();
}
public void RegisterTransient()
{
_registrations.Add(typeof(TInterface), typeof(TImplementation));
}
public TInterface Create()
{
var typeOfImpl = (Type)_registrations[typeof(TInterface)];
if (typeOfImpl == null)
{
throw new ApplicationException($"Failed to resolve {typeof(TInterface).Name}");
}
return (TInterface)Activator.CreateInstance(typeOfImpl);
}
}
最后,我们必须更新UI:
class Program
{
static void Main(string[] args)
{
var container = new Container();
container.RegisterTransient();
IProductBL productBL = new ProductBL(container.Create());
var products = productBL.GetProducts();
foreach (var product in products)
{
Console.WriteLine(product.Name);
}
Console.ReadKey();
}
}
现在,让我们在容器中实现Resolve方法。此方法将解决依赖关系。
下面是Resolve方法:
public T Resolve()
{
var ctor = ((Type)_registrations[typeof(T)]).GetConstructors()[0];
var dep = ctor.GetParameters()[0].ParameterType;
var mi = typeof(Container).GetMethod("Create");
var gm = mi.MakeGenericMethod(dep);
return (T)ctor.Invoke(new object[] { gm.Invoke(this, null) });
}
然后,我们可以在UI中使用如下Resolve方法:
class Program
{
static void Main(string[] args)
{
var container = new Container();
container.RegisterTransient();
container.RegisterTransient();
var productBL = container.Resolve();
var products = productBL.GetProducts();
foreach (var product in products)
{
Console.WriteLine(product.Name);
}
Console.ReadKey();
}
}
在上面的源代码中,容器使用container.Resolve
这是一个非常简单且基本的IoC容器,向您展示IoC容器背后的内容。您可以在.NET源代码中使用多个IoC容器。