困难的测试设置是一种代码味道,我想你已经看到了。答案是更多的牛铃(抽象)。
这是作为 UI 控制器和编排业务流程的控制器方法中的常见错误。第 5 步和第 6 步可能属于一起,第 1、3 和 4 步同样应该抽象到另一种方法。实际上,控制器方法应该做的唯一一件事就是从视图接收数据,将其传递给应用程序或业务层服务,然后将结果编译到新视图中以显示给用户(映射)。
编辑:
您在评论中提到的AccountManager 类是迈向良好抽象的良好一步。它与 MVC 代码的其余部分位于相同的命名空间中,不幸的是,这使得交叉依赖关系变得更加容易。例如,将视图模型传递给AccountManager 是“错误”方向的依赖项。
想象一下这个理想化的网络应用架构:
应用层
- 用户界面(JavaScript/HTML/CSS)
- 模型-视图-控制器(Razor/ViewModel/Navigation)
- 应用服务(编排/应用逻辑)
业务层
- 域服务(域 [EF] 模型/工作单元/事务)
- WCF/第三方 API(适配器/客户端代理/消息)
数据层
- 数据库
在此架构中,每个项目都引用它下面的项目。
推断您的代码的一些事情,AccountManager 至多是应用程序服务(在引用层次结构中)。我不认为它在逻辑上是 MVC 或 UI 组件的一部分。现在,如果这些架构项位于不同的 dll 中,IDE 将不允许您将视图模型传递给 AccountManager 的方法。这会导致循环依赖。
除了架构问题之外,很明显视图模型不适合传递,因为它总是包含支持视图渲染的数据,这对AccountManager 来说是无用的。这也意味着 AccountManager 必须了解视图模型中属性的含义。视图模型类和AccountManager 现在相互依赖。这会给代码带来不必要的脆弱性。
更好的选择是传递简单的参数,或者如果您愿意,可以将它们打包到一个新的数据传输对象 (DTO) 中,该对象将由合同在与 AccountManager 相同的位置定义。
一些示例接口:
namespace MyApp.Application.Services
{
// This component lives in the Application Service layer and is responsible for orchestrating calls into the
// business layer services and anything else that is specific to the application but not the overall business domain.
// For instance, sending of a confirmation email is probably a requirement in some application process flows, but not
// necessarily applicable to every instance of adding a user to the system from every source. Perhaps there is an admin back-end
// application which may or may not send the email when an administrator registers a new user. So that back-end
// application would have a different orchestration component that included a parameter to indicate whether to
// send the email, or to send it to more than one recipient, etc.
interface IAccountManager
{
bool RegisterNewUser(string username, string password, string confirmationEmailAddress, ...);
}
}
namespace MyApp.Domain.Services
{
// This is the business-layer component for registering a new user. It will orchestrate the
// mapping to EF models, calling into the database, and calls out to the third-party API.
// This is the public-facing interface. Implementation of this interface will make calls
// to a INewUserRegistrator and IExternalNewUserRegistrator components.
public interface IUserRegistrationService
{
NewUserRegistrationResult RegisterNewUser(string username, string password, ...);
}
public class NewUserRegistrationResult
{
public bool IsUserRegistered { get; set; }
public int? NewUserId { get; set; }
// Add additional properties for data that is available after
// the user is registered. This includes all available relevant information
// which serves a distinctly different purpose than that of the data returned
// from the adapter (see below).
}
internal interface INewUserRegistrator
{
// The implementation of this interface will add the user to the database (or DbContext)
// Alternatively, this could be a repository
User RegisterNewUser(User newUser) ;
}
internal interface IExternalNewUserRegistrator
{
// Call the adapter for the API and update the user registration (steps 5 & 6)
// Replace the return type with a class if more detailed information is required
bool UpdateUserRegistrationFromExternalSystem(User newUser);
}
// Note: This is an adapter, the purpose of which is to isolate details of the third-party API
// from yor application. This means that what comes out from the adapter is determined not by what
// is provided by the third party API but rather what is needed by the consumer. Oftentimes these
// are similar.
// An example of a difference can be some mundance detail. For instance, say that the API
// returns -1 for some non-nullable int value when the intent is to indicate lack of a match.
// The adapter would protect the application from that detail by using some logic to interpret
// the -1 value and set a bool to indicate that no match was found, and to use int?
// with a null value instead of propagating the magic number (-1) throughout your application.
internal interface IThirdPartyUserRegistrationAdapter
{
// Call the API and interpret the response from the API.
// Also perform any logging, exception handling, etc.
AdapterResult RegisterUser(...);
}
internal class AdapterResult
{
public bool IsSuccessful { get; set; }
// Additional properties for the response data that is needed by your application only.
// Do not include data provided by the API response that is not used.
}
}
需要记住的是,这种一次性设计与 TDD 正好相反。在 TDD 中,当您从外向内测试和编写代码时,对这些抽象的需求变得显而易见。我在这里所做的是跳过所有这些,直接跳到根据我脑海中的想象设计内部结构。在几乎所有情况下,这都会导致过度设计和过度抽象,而 TDD 自然会防止这种情况发生。