我有一个关于设计类的最佳方法的问题,以便测试友好。假设我有一个OrderService类,用于下订单,检查订单状态等等。该类需要访问客户信息,库存信息,发货信息等。因此OrderService类将需要使用CustomerService,InventoryService和ShippingService。每个服务也有自己的后备存储库。
设计OrderService类以便轻松测试的最佳方法是什么?我见过的两种常用模式是依赖注入和服务定位器。对于依赖注入,我做这样的事情:
class OrderService
{
private ICustomerService CustomerService { get; set; }
private IInventoryService InventoryService { get; set; }
private IShippingService ShippingService { get; set; }
private IOrderRepository Repository { get; set; }
// Normal constructor
public OrderService()
{
this.CustomerService = new CustomerService();
this.InventoryService = new InventoryService();
this.ShippingService = new ShippingService();
this.Repository = new OrderRepository();
}
// Constructor used for testing
public OrderService(
ICustomerService customerService,
IInventoryService inventoryService,
IShippingService shippingService,
IOrderRepository repository)
{
this.CustomerService = customerService;
this.InventoryService = inventoryService;
this.ShippingService = shippingService;
this.Repository = repository;
}
}
// Within my unit test
[TestMethod]
public void TestSomething()
{
OrderService orderService = new OrderService(
new FakeCustomerService(),
new FakeInventoryService(),
new FakeShippingService(),
new FakeOrderRepository());
}
这样做的缺点是每次我创建一个我在测试中使用的OrderService对象时,在我的测试中调用构造函数需要大量代码。我的服务类最终还为他们使用的每个Service和Repository类提供了一堆属性。当我扩展我的程序并在各种Service和Repository类之间添加更多依赖项时,我必须返回并向我已经创建的类的构造函数添加越来越多的参数。
对于服务定位器模式,我可以这样做:
class OrderService
{
private CustomerService CustomerService { get; set; }
private InventoryService InventoryService { get; set; }
private ShippingService ShippingService { get; set; }
private OrderRepository Repository { get; set; }
// Normal constructor
public OrderService()
{
ServiceLocator serviceLocator = new ServiceLocator();
this.CustomerService = serviceLocator.CreateCustomerService()
this.InventoryService = serviceLocator.CreateInventoryService();
this.ShippingService = serviceLocator.CreateShippingService();
this.Repository = serviceLocator.CreateOrderRepository();
}
// Constructor used for testing
public OrderService(IServiceLocator serviceLocator)
{
this.CustomerService = serviceLocator.CreateCustomerService()
this.InventoryService = serviceLocator.CreateInventoryService();
this.ShippingService = serviceLocator.CreateShippingService();
this.Repository = serviceLocator.CreateOrderRepository();
}
}
// Within a unit test
[TestMethod]
public void TestSomething()
{
OrderService orderService = new OrderService(new TestServiceLocator());
}
我喜欢在调用构造函数时服务定位器模式如何导致更少的代码,但它也提供了更少的灵活性。
建议使用依赖于其他几个服务和存储库的Service类的建议方法是什么,以便可以轻松测试它们?上面显示的方式中的一种或两种都是好的,还是有更好的方式?
答案 0 :(得分:7)
只是一个非常快速的答案,让你走上正确的轨道。 根据我的经验,如果你的目标是易于测试的代码,你往往会得到干净的可维护代码作为一个很好的副作用。 :-)
要记住的一些要点:
SOLID原则将真正帮助您创建良好,干净,可测试的代码。
(S + O + I)将此服务分解为只执行一项操作的较小服务,因此只有一个原因需要更改。至少下订单和检查订单的状态是完全不同的事情。如果您对此深入思考,那么您并不需要遵循最明显的步骤(例如,检查信用卡 - >检查库存 - >检查运费),其中一些可以不按顺序完成 - 但是那可能需要不同商业模式的另一个故事。无论如何,如果你真的需要,你可以使用Facade模式在这些较小的服务之上创建一个简化的视图。
使用IoC容器(例如统一)
使用Mocking框架(例如Moq)
服务定位器模式实际上被认为是反模式/代码气味 - 所以请不要使用它。
答案 1 :(得分:1)
代码是"可测试的"和松散耦合的代码。
使用DI的主要目的是松耦合。可测试性是从松散耦合的代码中获得的附带好处。但是可测试的代码并不一定是松散耦合的。
虽然注入服务定位器显然比静态引用更加松散,但它仍然不是最佳实践。最大的缺点是lack of transparency of dependencies。您现在可以通过实现服务定位器来保存几行代码然后认为您正在获胜,但是当您实际必须撰写您的应用程序时,这样做会带来任何损失。查看intellisense中的构造函数以确定类具有哪些依赖关系以查找该类的源代码以尝试计算它具有的依赖关系,这是一个明显的优势。
所以,正如您可能已经猜到的那样,我建议您使用构造函数注入。但是,您的示例中还有一个称为bastard injection的反模式。混蛋注入的主要缺点是你通过在内部对它们进行新的紧密耦合。这可能看似无辜,但如果您需要将服务移动到单独的库中会发生什么?很有可能会在您的应用程序中导致循环依赖。
处理此问题的最佳方法(特别是在处理服务而不是配置设置时)是使用pure DI或DI容器,只需要一个构造函数。您还应该使用guard子句来确保无法在没有任何依赖关系的情况下创建订单服务。
class OrderService
{
private readonly ICustomerService customerService;
private readonly IInventoryService inventoryService;
private readonly IShippingService shippingService;
private readonly IOrderRepository repository;
// Constructor used for injection (the one and only)
public OrderService(
ICustomerService customerService,
IInventoryService inventoryService,
IShippingService shippingService,
IOrderRepository repository)
{
if (customerService == null)
throw new ArgumentNullException("customerService");
if (inventoryService == null)
throw new ArgumentNullException("inventoryService");
if (shippingService == null)
throw new ArgumentNullException("shippingService");
if (repository == null)
throw new ArgumentNullException("repository");
this.customerService = customerService;
this.inventoryService = inventoryService;
this.shippingService = shippingService;
this.repository = repository;
}
}
// Within your unit test
[TestMethod]
public void TestSomething()
{
OrderService orderService = new OrderService(
new FakeCustomerService(),
new FakeInventoryService(),
new FakeShippingService(),
new FakeOrderRepository());
}
// Within your application (pure DI)
public class OrderServiceContainer
{
public OrderServiceContainer()
{
// NOTE: These classes may have dependencies which you need to set here.
this.customerService = new CustomerService();
this.inventoryService = new InventoryService();
this.shippingService = new ShippingService();
this.orderRepository = new OrderRepository();
}
private readonly IOrderService orderService;
private readonly ICustomerService customerService;
private readonly IInventoryServcie inventoryService;
private readonly IShippingService shippingService;
private readonly IOrderRepository orderRepository;
public ResolveOrderService()
{
return new OrderService(
this.customerService,
this.inventoryService,
this.shippingService,
this.orderRepository);
}
}
// In your application's composition root, resolve the object graph
var orderService = new OrderServiceContainer().ResolveOrderService();
我也同意戈登的回答。如果您有4个服务依赖项,那么您的类承担了太多责任,这就是代码味道。您应该考虑重构aggregate services以使您的课程成为单一的责任。当然,有时需要4个依赖项,但总是值得退一步看看是否存在应该是另一个显式服务的域概念。
注意:我不一定说Pure DI是最好的方法,但它可以用于一些小应用程序。当应用程序变得复杂时,使用DI容器可以通过使用基于约定的配置来支付股息。