There is a lot to go over here and I will try to be as thorough as I can. This is a complex issue about which there is much debate and so I will share my experience to apply some pragmatism as well as go over the logic (dogma?) behind the purpose of SRP, encapsulation, and many of these other concepts.
Firstly
Well done in reading / researching these topics. Too often developers focus on getting the solution and moving on. In software there is something to be said for problem solving (as that is what we are paid for) but another part that is at least as important, is writing code that makes sense and can be maintained. Good job then on asking.
Code
Here are some code smells that I noticed in your source listing.
(Side Note: It seems like you followed the example here: http://weblogs.asp.net/rashid/asp-net-mvc-best-practices-part-1 for your ExportModelStateToTempData
method. I would NOT consider this even a GOOD practice for reasons I will elaborate on as we go through this answer, but that is my opinion)
In your Controller:
1) ModelState and Redirection
If the service call fails, you use the ModelState to add the errors returned by the service call. While this normally is what I would expect to see, your next line of code redirects the user to the Register
action, which will lose the errors you have just added to the ModelState. It would seem as though you use the ExportModelStateToTempData
attribute to persist the ModelState errors to the redirected page (TempData
uses Session state). I had to read over this several times to notice this and while this doesn't fit the standard definition of a side-effect, it is at the very least difficult to read / understand. As a personal note, I don't care for TempData
for several reasons I have found in my own coding:
- It's not strongly typed (more difficult to test / debug)
- It uses session under the covers (requires a lot of infrastructure if used in a web-farm)
- It encourages lazy coding standards (as do
ViewBag
/ ViewData
)
- Using an attribute to "hide" this functionality will lead to difficulties debugging and comprehending the code's function (and frankly there are much better ways of persisting state)
2) Var
keyword
You use var to store the result of the call to the customerService.Register
method. Since you know what the return type will be, you can increase the readability and maintainability by skipping using the var
keyword like this:
SignInStatus status = customerService.Register(binding);
There are some other gotchas with the var
keyword as well, outlined here: https://stackoverflow.com/questions/356846/will-using-var-affect-performance.
I like your use of the DefaultUrl
property (a commonly overlooked feature) and your use of the null coalescing operator (??) in the Redirect, that helps to keep the code very concise.
In your Service:
Just to clarify: In your Register
method, the parameter is of type UserRegistration
but as far as I can work out, when called from your controller, the controller passes a type of AccountRegisterBindingModel
. Does the AccountRegisterBindingModel
type derive from UserRegistration
?
1) SRP
This is in direct answer to your question: "Does this do too much?" -- Yes
As you correctly identified, Register is not a sufficient name for what this does and registration is itself an atomic process and should be handled as such. Here is my reasoning:
Signing out should be an atomic operation performed elsewhere in the flow of the website. Under what circumstances could a signed-in user register for a new account? Given this restriction, the sign-out process should already have been accounted for and if it hasn't, that is the code smell that needs to be addressed, not having a "last-ditch" attempt to sign the user out in the register method. This can and should be removed from Register.
The userService.CreateUser(data)
call absolutely belongs here, though I would use the same reasoning about the var
keyword I used above.
The check for result.Succeeded
, the word is spelled incorrectly, it should be Succeeded, not Succedded (I'm assuming this is a typo in your question, not the actual code).
This check does nothing with a failure. In the controller you add errors, but I don't see any code that communicates what those errors are or how the Errors
property referenced in your controller is filled with the relevant data.
The communicationService.SendWelcomeEmail call is pretty gray area. I would argue that Registration is a "business" operation and therefore "business" operations apply here (login / logout for example are not business operations, but SendWelcomeEmail
is a "business" operation).
For the MergeSessionCustomerWithUserCustomer
I'm confused. I thought the Register method was in the CustomerService
, so why in the Register
method do you refer to customerService
as an external dependency as opposed to referring to this.MergeSessionCustomerWithUserCustomer()
?
As for login, it shouldn't be here. The website should after the registration occurs offer a link (or a checkbox prior to completion) that specifies "log me in when I'm done" or "Thnaks for creating an account, sign in now..." or something to that effect. This will have the result of removing this from the Register method.
With that in mind, the return type of the Register
method is one of SignInStatus
... This smells funny. Why should my register also SignIn, shouldn't it instead return a RegistrationStatus
?
Finally, I wouldn't call the MergeSessionCustomerWithUserCustomer
method here at all. This seems like something that should be done from the controller as part of a login (as it relates to session, which is a website concept, not a business concept).
With the register method free of non-related calls, any other consumer of this service can "Register" a user without unintended consequences. Without doing this, there are problems.
Let's say I wanted to write a console application that bulk-read in an excel or flat file of user registrations from a legacy system. I'd like to be able to use the CustomerService
, but I can't because the register method produces side-effects (or worse, I use it not knowing about the side-effects).
So, to summarize, I would write the code like this:
In the controller:
[HttpPost, ValidateModelState, ValidateAntiForgeryToken]
public RedirectResult Register(AccountRegisterBindingModel binding, string returnUrl) {
//Check to see if user is already logged in...
//if (isAlreadyLoggedIn) Do something...
RegistrationStatus status = customerService.Register(binding);
if (!status.Succeded) {
ModelState.AddModelErrors(status.Errors);
return View("MyformWithErrors");
} else {
customerService.MergeSessionCustomerWithUserCustomer();
return Redirect(Url.Action("Register", "Account", new { returnUrl }));
}
return Redirect(returnUrl ?? DefaultUrl);
}
In your service:
public RegistrationStatus Register(UserRegistration data) {
RegistrationStatus result = userService.CreateUser(data);
if(result.Succeeded) {
communicationService.SendWelcomeEmail(data);
}
}
Really what you want is code that doesn't have side effects and only ever has ONE REASON to change (bug fixes). Following SOLID principles is very helpful but you also have to develop a keen nose for code smells.
I would suggest writing some unit tests for the methods and asking yourself these questions:
- How much setup is required to test method 'x'?
- How many test cases are required to thoroughly test method 'x'?
- How likely is it that a change to type 'y' will cause me to have to modify method 'x' or the tests for method 'x'?
- Does this test accurately describe the expected behavior of method 'x'?
Ideally, you want minimal setup, with only a few test cases. You also want the tests to not be 'brittle' and require modification whenever a dependency is changed and the tests should serve as the primary documentation for what the method does and what it's expected use is, which helps with re-use later.