Ask Brian: Handling Custom Object Behavior

Robb asks:

I have an architecture question. I am building a site with MG2/Transfer/Coldspring and looking to refine my model. I have a User object that can handle several different "user-roles" i.e. admin, prospect, agent, etc... I have been struggling with the best way to provide the configuration details/properties for each user-role. I don't want to end up with class explosion with say a strategy pattern or even a quasi-abstract CF factory method. I am not sure that configuration details/properties should be a singleton CS managed object as they are more transient but what is the best way to handle?

Robb is looking for a way to assign custom behavior to his User object. To his credit, he isn't immediately trying to go down the inheritance path, which is what many people will initially try. The idea of creating a base User object and extending it with AdminUser, AgentUser, etc. looks easy and appropriate upon first glance. Unfortunately, it has a big problem: inheritance is static and it is a "one or the other" proposition.

In other words, what if you have an Agent who is also an Admin? This gets nasty very quickly. What do you do, go with User > Agent > AdminAgent? Or User > Admin > AgentAdmin? This is the very definition of a class explosion, and again, it is good that Robb appears to see the problems with trying that approach.

Robb mentioned the Strategy Pattern, which might be a good fit. His fear of a class explosion here may be misplaced though. Clearly, the Strategy pattern will cause much less of a class explosion than inheritance probably would. And the bottom line is that the custom behavior has to live somewhere. So my take would be something like this:

There's really nothing wrong with this, and there really is no class explosion. You have one class for each type of specialized behavior you which to apply to a User. It doesn't get much tighter than that. He can also associate more than one Strategy with a User if necessary. The only downside here is that the User object needs to have a static API (unless you want to start playing with method injection but that is another topic and would add another layer of complexity). In other words, the User object would need a method for doAgentThing() even if that particular User instance did not use the Agent role strategy. This may not be an issue, and in fact it may be a good thing for the object to always have a known API.

There is another take on the problem, which essentially reverse the composition relationship. This is the Decorator Pattern. It might look like this:

You can see that it is almost identical to the Strategy Pattern except that the relationship is reversed. Essentailly, the Decorator "wraps around" the original object and adds additional behavior. Again we have one class per custom set of behavior we want to apply. And a User can have more than one Decorator applied (sort of a "russian dolls" approach where we have more than one wrapper). But the difference here is that the API of the Decorator would not remain static. The object would have a doAgentThing() method only if the Agent decorator was applied to the User.

This might be a benefit or a drawback depending on how you create and use the object and how certain you are of what kind of object you are dealing with. But it does introduce coupling, because now the code using the object needs to know what kind of User it is dealing with. This is called "type coupling" because the calling code has to know which type of User it is interacting with. In some cases this might be fine. i.e. if you know you are only calling agent-related methods from within some other agent-specific code, this may not be a problem. (One could also specify all possible Decorator methods in the abstract UserDecorator class and have it throw an error or do nothing if the User doesn't have a specific decorator applied, but then we get into needing to handle exceptions in the calling code which would also add more complexity.)

As you can see, there are a few ways one might address this problem and they each have their own benefits and consequences. Which one Robb might use probably depends on how he uses the User, how important it is that the User object maintain a stable API, and how much type coupling he is willing to accept when using the object from other code.

Hopefully that gives Robb some ideas to consider. If anyone has comments or other ideas, please feel free to comment and give your thoughts. Thanks!

Comments (Comment Moderation is enabled. Your comment will not appear until approved.)
Hi Brian,

I always enjoy reading your posts. I can really appreciate the strategy pattern now since I just used inheritance to solve this exact same problem.

I have a question about the implementation of this design in cf.

Is it necessary to create the RoleStrategy class since cf is a dynamic language?

From a previous post of yours http://www.briankotek.com/blog/index.cfm/2007/5/9/... on this same topic, you created an abstract class called PayStrategy. This class was extended by HourlyPayStrategy and SalariedPayStrategy classes. Then you used a factory design pattern, EmployeeFactory, to create the employees with the either an instance of the HourlyPayStrategy or SalariedPayStrategy.

The Employee instance is expecting to receive an object of type PayStrategy object in the init method which then calls the setPay method. This method is also expecting to receive an object of type PayStrategy.

My question revolves around using the object type PayStrategy. If we changed the type from "PayStrategy" to "any" for arguments and returnTypes in the Employee class and removed extends="PayStrategy" from the HourlyPayStrategy and SalariedPayStrategy classes, cf could still call the the employee's correct calculatePay() method without using the PayStrategy Class.

Is this a good implementation?

Thanks in advance for you reply,

Anthony
# Posted By Anthony | 6/16/08 3:24 PM
Hi Anthony, thanks for the kind words. It would only be technically necessary to have an abstract RoleStrategy if there was behavior common to all of the concrete strategy implementations that you wanted to use across all of them. However, there's really no downside to having the abstract base class since it has little overhead and it means you can easily add general behavior to all of the strategy classes later if you need to.

Keep in mind that nothing actually ever directly uses the abstract RoleStrategy (or PayStrategy from the example you mention). They only use the subclasses. The point of the Abstract class is polymorphism. It means that, as much as possible, external code doesn't know *which* kind of strategy object is actually being used. It only knows it is using *some kind* of strategy object.

Changing the type to "any" doesn't change much other than the fact that you're dropping the type checking and taking on the responsibility yourself. Anything you pass in for that argument still needs to implement the correct strategy API or you'll get an error when your User tries to use the strategy object. But you are correct that, if there is no common behavior across the strategy objects, the abstract base class is not technically required, only recommended. As long as whatever CFC you pass into the User can respond to the proper method calls, you're OK. Make sense?
# Posted By Brian Kotek | 6/16/08 3:44 PM
BlogCFC was created by Raymond Camden. This blog is running version 5.9.1. Contact Blog Owner