Consider the following example in C# that I see in many languages of a use case that registers the user with the system, normally I always see a single execute or invoke function to perform the use case action.
public class SignUp
{
private readonly IRepository _repo;
public SignUp(IRepository repo)
{
_repo = repo;
}
public User Execute(UserData data)
{
return _repo.CreateUser(data);
}
}
I recently started studying architectures and bought an online course, basically in the case of ASP.NET, how the use case will be called in the controller via the HTTP request, I thought about:
The user may already exist, an error may also occur, or there may be an error validating some information such as email and password, and I must pass this same information back to the controller so that it can pass the correct response code (500, 201, 200, 400...).
Thinking like this, I don't see much point in a single function doing all this work, so if I, for example, make it return an enum
with the status of the operation, and also create a GetError()
function to obtain a possible error that occurred during the operation or similar Is this ok or does this violate solid's principle of single responsibility?
I'm studying clean architecture at the moment
There are no rules that dictate how many method a class may have. There are, however, forces that drive design like the one outlined in the OP.
Whole books have been written about this topic, so this isn't a question you should expect can be answered on a site like this. That said, here are a few pointers which should, hopefully, not be too opinionated.
If we imagin a situation without the SignUp
class, perhaps you have a Controller method that looks like this:
public ActionResult SignUp(UserData data)
{
// Lots of complicated code, including:
var user = _repo.CreateUser(data);
// More complicated code goes here...
return Ok(user);
}
Little is gained if you move all of that code somewhere else. It doesn't much matter whether you move that code to a private helper method on the Controller, or to a separate Signup
class:
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);
}
}
This would imply that your Controller action now looks like this:
public ActionResult SignUp(UserData data)
{
return new SignUp(_repo).CreateUser(data);
}
What would be the point of that?
A typical motivation for introducing a class like SignUp
is to separate concerns. Notice that, as shown here, the hypothetical SignUp
class has more than one responsibility. It makes business decisions, but it also handles ASP.NET-specifics, which implies that it handles HTTP or user-interface concerns. This is manifested in the return type (ActionResult
) as well as the use of Ok
or OkObjectResult
.
Another tell-tale, that we can't see here, is if the UserData
class is annotated with ASP.NET-specific attributes like [Required] or [NotNull].
In the language of Clean Architecture, those things are details, and according to the Dependency Inversion Principle details should depend on abstractions, rather than abstractions depending on the details.
Thus, if you want to raise the abstraction level just a notch, you now separate those concerns. One way to do that is to introduce a class like SignUp
that handles the use case independently of 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;
}
}
Nothing much is still gained, but at least you've now decoupled the class from any knowledge of ASP.NET. This gives you the opportunity to reuse it in new contexts, for example in unit tests.
The Controller action now looks like this:
public ActionResult SignUp(UserData data)
{
return Ok(new SignUp(_repo).CreateUser(data));
}
There's still not a lot gained, but this should outline the principle. You can now repeat the process:
You probably want to validate the input. This, again, may be application-specific, in that the input for a web application may look different from the input to a batch job. Thus, input validation may be better modelled outside of the SignUp
class. For example, coming back to the remark about UserData
, if you have ASP.NET-specific as part of that class, you should translate it into another data format before you call SignUp.Execute
.
You can keep separating concerns like that until you reach a useful decomposition of your code.