My MVC architecture is made up of three layers, in this order:
- controllers → intercept calls from the ui
- services → business logic, cache
- dao → interactions with the db
- Start. Click on the “save” button calls:
- ProductController.save( form ), calls:
- ProductService.save( productBean ), calls:
- ProductDAO.save( productBean ). End.
I have a problem.
I need to log who did the action in my DAO (or services…) layer.
Something like that:
<cfset getLogger().info("Product inserted by #currentUserId#")>
(Logger is a class that i use to write log).
The problem is that to write the log with the current user (#currentUserId#) I could:
- pass user for all methods (annoying and boring, and maybe even nonsense)
- in the Dao, take the current user directly from the session/request, but this way I have to create a dependency with the scope.
Is there a good practice/design pattern to solve this problem?
We tack commonly required things, like the current user id or start time of a call into the request scope as a “scratch pad”.
OK it sounds to me like that logger could be a
UserActivityLogger, in which case I would initialise that with a
SessionContext object, which is basically a wrapper around session-scope access.
That means you don’t have bare scope access going on (almost always poor practice), the DAO/service doesn’t need to unnecessarily know about user stuff; and the one object that does need to know about user stuff has the relevant data encapsulated, right where it needs it.
I would not use a global variable (which is basically what the request scope is) as a “scratchpad”. That is pretty bad separation of concerns / weak/uncontrolled design. I’d also never access the request scope directly; if an object needed to know about something about the current request, I’d give it a
RequestContext object (wraps the request scope).
Oh I also don’t think it’s the job of a
save method (or even the service that method is in) to know about how to get the logger it needs to use. I would - as I mentioned above - initialise the service with the logger it should be using. That’s an app-initialisation concern, not the job of the logic within a given service object.
And another thought. The methods that wrap CFML constructs that hit external services (eg:
<cfquery>) should do nothing other than call the external services and return the response. Put all other logic (including your logging) in the method that calls the external service wrapper method.
In my DAOs I have zero logic other than receiving (prepared) values from the app, passing them to the external service (eg:
<cfquery>), and returning the result.
The calling code works the data (both input and output).
Avoid using global (“god”) variables
You are indeed right to be wary of session-scoped or request-scoped variables. Such global (“god”) variables increase coupling. You should, whenever you can, either avoid using them or justify why you do. This is not just fancy object-orientation talk. Reducing coupling affects the bottom dollar. Coupling makes your code more difficult to maintain or to extend in the long run. Research has shown that maintenance accounts for more than half the lifetime costs of software.
Apply the Law of Demeter (LoD)
LoD suggests that components should only know about their closest neighbours. In your particular use-case, LoD suggests that myProductController may know about Me (CurrentUser) and about myProductService. But myProductService and myProductDAO may not know about Me.
in other words, LoD suggests that you should avoid what, in effect, amounts to the following construction:
A possible solution based on LoD:
- ProductController.save() saves my product along with my unique productId and currentUserId:
(the ProductController is therefore best placed to save/log the CurrentUser)
- ProductBean=ProductService.getProduct(productId), ProductService.save(productBeanId) saves productBean along with unique productBeanId.
- ProductDAO.get(productBeanId), ProductDAO.save(productBeanId) saves productBean along with unique productBeanId.
(the ProductBean identified by productBeanId - and not the CurrentUser - is the one who did the action in the DAO).