考虑下面的 C# 示例,我在许多语言中看到了向系统注册用户的用例,通常我总是看到单个执行或调用函数来执行用例操作。
public class SignUp
{
private readonly IRepository _repo;
public SignUp(IRepository repo)
{
_repo = repo;
}
public User Execute(UserData data)
{
return _repo.CreateUser(data);
}
}
最近开始学习架构,买了一个在线课程,基本上都是以ASP.NET为例,如何通过HTTP请求在控制器中调用用例,我思考:
用户可能已经存在,也可能发生错误,或者验证某些信息(例如电子邮件和密码)时可能出现错误,我必须将这些相同的信息传递回控制器,以便它可以传递正确的响应代码(500 、201、200、400...)。
这样思考,我认为单个函数完成所有这些工作没有多大意义,因此,例如,如果我让它返回带有操作状态的
enum
,并创建一个 GetError()
函数获取操作过程中可能发生的错误或类似的错误,这可以吗,还是违反了solid的单一责任原则?
我现在正在研究干净建筑
没有规则规定一个类可以有多少方法。然而,有一些力量推动设计,就像 OP 中概述的那样。
关于这个主题已经写了很多书,所以你不应该指望在这样的网站上可以回答这个问题。也就是说,这里有一些建议,希望不要太固执己见。
如果我们想象一种情况没有
SignUp
类,也许你有一个如下所示的控制器方法:
public ActionResult SignUp(UserData data)
{
// Lots of complicated code, including:
var user = _repo.CreateUser(data);
// More complicated code goes here...
return Ok(user);
}
如果将该代码的全部移到其他地方,则收效甚微。无论您将该代码移动到控制器上的私有帮助器方法,还是移动到单独的
Signup
类,都没有多大关系:
public class SignUp
{
private readonly IRepository _repo;
public SignUp(IRepository repo)
{
_repo = repo;
}
public ActionResult Execute(UserData data)
{
// Lots of complicated code, including:
var user = _repo.CreateUser(data);
// More complicated code goes here...
return OkObjectResult(user);
}
}
这意味着您的控制器操作现在如下所示:
public ActionResult SignUp(UserData data)
{
return new SignUp(_repo).CreateUser(data);
}
这样做有什么意义?
引入像
SignUp
这样的类的典型动机是分离关注点。请注意,如此处所示,假设的 SignUp
类具有多个职责。它做出业务决策,但它也处理 ASP.NET 特定的事务,这意味着它处理 HTTP 或用户界面问题。这体现在返回类型 (ActionResult
) 以及 Ok
或 OkObjectResult
的使用中。
我们在这里看不到的另一个问题是
UserData
类是否使用 ASP.NET 特定的属性进行注释,例如 [Required] 或 [NotNull]。
用清洁架构的语言来说,这些东西就是细节,根据依赖倒置原则,细节应该依赖于抽象,而不是抽象依赖于细节。
因此,如果您想将抽象级别提高一个档次,您现在可以将这些关注点分开。一种方法是引入一个类似SignUp
的类,它独立于 ASP.NET 处理用例:
public class SignUp
{
private readonly IRepository _repo;
public SignUp(IRepository repo)
{
_repo = repo;
}
public User Execute(UserData data)
{
// Lots of complicated code, including:
var user = _repo.CreateUser(data);
// More complicated code goes here...
return user;
}
}
虽然仍然没有获得什么,但至少您现在已经将类与 ASP.NET 的任何知识解耦了。这使您有机会在新上下文中重用它,例如在单元测试中。public ActionResult SignUp(UserData data)
{
return Ok(new SignUp(_repo).CreateUser(data));
}
虽然收获还不是很多,但这应该概述了原理。您现在可以重复该过程:您可能想要验证输入。同样,这可能是特定于应用程序的,因为 Web 应用程序的输入可能看起来与批处理作业的输入不同。因此,输入验证可以在
SignUp
UserData
的评论,如果该类中有特定于 ASP.NET 的部分,则应在调用
SignUp.Execute
之前将其转换为另一种数据格式。
您可以继续分离这样的关注点,直到对代码进行有用的分解。