我们能很容易的通过替换对象来实现程序的功能切换。另外的一个好处我觉得很多人都不太重视。那就是对象间的解耦给自动化的单元测试提供了可能性
想象下我们测试我们的业务逻辑代码。如果业务逻辑代码使用了数据库,网络。比如处理一个订单这个用例。这个用例包括根据用户购买的产品和用户的等级来计算价格
并生成订单插入到数据库中,然后给客户发送邮件通知。如果我们想要测试我们的计算价格部分代码。如果各个模块是耦合在一起的,为了测试计算价格部分的代码,你就得准备好数据库
和邮件服务器。在这种情况下能够实现自动化测试,是不可能的。你不能保证数据库中的数据是你期望的。你也不能保证邮件服务器一直都运行正常。
所以如果我们的对象之间的依赖是松耦合的(通过接口),我们就能替换数据库访问层对象和邮件发送对象为mock对象来模拟数据库和邮件服务器
。这样我们才能保持我们的测试计算订单的单元测试每次都能按照期望的方式运行。我觉得这是使用ioc解耦带来的一个很重要的好处。甚至比能够实现功能替换更实用。
不知道有谁现在体会到了能够实现功能替换带来的好处。将业务层代码和数据访问层代码解耦后,可以切换数据库实现。但是有几个会这么做的
所以说能够实现自动化单元测试实际上比能切换数据库实现看起来更实用些。
上面的讨论主要是说明解耦的一个很重要的好处,就是方便单元测试。下面写两个使用ioc来管理对象的例子。虽然都是用了容器来管理依赖。但最后一个对单元测试
更友好一点。因为它不过分依赖外部的配置文件。我们很容易就能把在单元测试中测试。
代码
public class Order
{
//some properties
public double Price { get; set; }
public string UserEmail { get; set; }
}
public interface IOrderService
{
void CreateOrder(Order order);
}
public interface IEmailSender
{
void Send(string email,string content);
}
public class EmailSender:IEmailSender
{
public void Send(string email, string content)
{
//access network
}
}
public interface IOrderRepository
{
void SaveOrder(Order order);
}
public class OrderRepository:IOrderRepository
{
public void SaveOrder(Order order)
{
//access DB
}
}
public class OrderServiceV1:IOrderService
{
public void CreateOrder(Order order)
{
IOrderRepository orderRepository = IocContainer.Resolve<IOrderRepository>(); //获取OrderRepository实现
IEmailSender emailSender = IocContainer.Resolve<IEmailSender>();
order.Price = 11;//计算价格算法...
orderRepository.SaveOrder(order);
emailSender.Send(order.UserEmail, "thanks!");
}
}
public class OrderServiceV2 : IOrderService
{
//将依赖接口从方法中分离方便单元测试时替换mock实现
IOrderRepository orderRepository;
IEmailSender emailSender;
//默认构造函数从容器中获取实现
public OrderServiceV2()
{
orderRepository = IocContainer.Resolve<IOrderRepository>(); //获取OrderRepository实现
emailSender = IocContainer.Resolve<IEmailSender>();
}
//提供不依赖配置文件的接口实现
public OrderServiceV2(IOrderRepository orderRepository, IEmailSender emailSender)
{
this.orderRepository = orderRepository;
this.emailSender = emailSender;
}
public void CreateOrder(Order order)
{
orderRepository = IocContainer.Resolve<IOrderRepository>(); //获取OrderRepository实现
emailSender = IocContainer.Resolve<IEmailSender>();
order.Price = 11;//计算价格算法...
orderRepository.SaveOrder(order);
emailSender.Send(order.UserEmail, "thanks!");
}
}
{
//some properties
public double Price { get; set; }
public string UserEmail { get; set; }
}
public interface IOrderService
{
void CreateOrder(Order order);
}
public interface IEmailSender
{
void Send(string email,string content);
}
public class EmailSender:IEmailSender
{
public void Send(string email, string content)
{
//access network
}
}
public interface IOrderRepository
{
void SaveOrder(Order order);
}
public class OrderRepository:IOrderRepository
{
public void SaveOrder(Order order)
{
//access DB
}
}
public class OrderServiceV1:IOrderService
{
public void CreateOrder(Order order)
{
IOrderRepository orderRepository = IocContainer.Resolve<IOrderRepository>(); //获取OrderRepository实现
IEmailSender emailSender = IocContainer.Resolve<IEmailSender>();
order.Price = 11;//计算价格算法...
orderRepository.SaveOrder(order);
emailSender.Send(order.UserEmail, "thanks!");
}
}
public class OrderServiceV2 : IOrderService
{
//将依赖接口从方法中分离方便单元测试时替换mock实现
IOrderRepository orderRepository;
IEmailSender emailSender;
//默认构造函数从容器中获取实现
public OrderServiceV2()
{
orderRepository = IocContainer.Resolve<IOrderRepository>(); //获取OrderRepository实现
emailSender = IocContainer.Resolve<IEmailSender>();
}
//提供不依赖配置文件的接口实现
public OrderServiceV2(IOrderRepository orderRepository, IEmailSender emailSender)
{
this.orderRepository = orderRepository;
this.emailSender = emailSender;
}
public void CreateOrder(Order order)
{
orderRepository = IocContainer.Resolve<IOrderRepository>(); //获取OrderRepository实现
emailSender = IocContainer.Resolve<IEmailSender>();
order.Price = 11;//计算价格算法...
orderRepository.SaveOrder(order);
emailSender.Send(order.UserEmail, "thanks!");
}
}
当然也可以把OrderServiceV2默认构造函数也去掉。让我们的Service彻底和配置文件和容器解耦。然后在需要IOrderService时也从
容器中获取。并通过构造函数注入对IOrderRepository和IEmailSender的实现。OrderServiceV1则很糟糕。解耦了对具体对象实现的依赖,缺引入了对配置文件的依赖
要想运行单元测试必须先配置好容器的配置文件。还要处理依赖的依赖。比如A依赖B,B依赖C.你必须配置好B的实现,还的配置好C的实现。一直到谁都不依赖为止
其实你运行A是只需要处理好和A的直接依赖对象就行了。根本不用考虑间接依赖。
千万不小忽视这点小的差别。第一种实现过分的依赖配置文件会让写单元测试的人很快就因为麻烦而放弃写自动化的单元测试。最后大家单元测试项目就演变成了调试项目了
。
总结:不要把任何读配置文件的代码放到实现方法中。应该放到构造函数或者属性。允许单元测试时方便的替换配置文件内容。
像ConfigurationManager.AppSettings["xxx"]这样的语句也不能出现在实现方法中。对容器的使用时最好能采用约定大于配置的方式。减少配置文件大小,减轻配置管理任务
比如IOrderRepository的实现是OrderRepository就不需要配置了。根据命名规则读取实现就行了!