Programming languages give us a variety of options for sharing code among multiple classes. In this article, let's look at the 3½ ways that ColdFusion allows us to get code reuse in our components.
Let's start with a problem – we've got two ColdFusion components that need the same logic shared between them. One is a Duck, and the other is a Platypus. Both need to lay eggs. How do we share the egg-laying logic between the two?
1. Inheritance
The first option, and one that many programmers jump to way too soon, is inheritance. To solve our problem by using inheritance, we create a third component, called EggLayer, which looks like this:
Now, we simply add an extends attribute to our Duck and Platypus CFCs:
Now our Duck and Plataypus objects each have a layEggs() method.
A main disadvantage of inheritance is that you can only inherit from one component. So, now that Platypus has extended EggLayer, it can't also extend Mammal. Bummer!
Another big disadvantage is that the subclass (e.g., Duck and Platypus) gets access to all sorts of private information and operations in the superclass (e.g., EggLayer). We call this tight coupling. In the example here, there's not really anything that needs to stay private to EggLayer. But in a more real-world situation, you'd probably have more data and methods that you'd want to keep hidden away from other classes – even your subclasses!
2. Composition
Our second option is composition. In this case, we wrap our EggLayer class in our Duck and Platypus classes. In other words, we store a reference to an EggLayer object in the variables scope. So, our EggLayer class stays the same:
But there's a much bigger change to the Duck and Platypus classes:
Now, there are a few things that make this cooler than inheritance:
- Platypus has the logic for laying eggs and can still use inheritance to extend Mammal.
- What if you want more than one kind of egg-laying logic? Maybe you want a second one that says "I laid an egg" one time for each egg laid. If you used inheritance, you're stuck with the original implementation. But by using composition, you can change out the egg-laying logic… at runtime! You'd just provide a setter method that overwrites variables.eggLayer.
Now, one of the problems we've got with composition above is that we had to add a layEggs() method to Duck and Platypus. Of course, the method was just calling the same method on the EggLayer object, but still, it feels redundant to have to write out the function again. How can we get around this?
3. Mixin
Mixins are our third option. They give us a way to get the egg-laying logic…
- …without having to retype the function
- …without storing a member variable, and
- …without needing to sacrifice our one opportunity for inheritance.
Here's what it looks like.
Notice that eggLayer is a cfm, not a cfc. By using a cfinclude, we got layEggs() into Duck and Platypus without using inheritance or composition. Now, mixins have some problems, too.
- If your class already has a layEggs() method, you'll get an exception. In other words, you can't override a method using mixins.
- Unlike with composition, you can't change out the egg-laying logic at runtime… well, not without patching, which is next on our list!
3½. Patch
Patching only gets half of a point because it's very similar to mixins -- you've got a freestanding function that gets added to a class. The difference is that Mixins are declared in the cfc, but when Patching, you set it at runtime. Here's how it looks:
The layEggs() method is not defined in the Duck or Platypus classes at all -- it's set from outside the classes. This can be helpful in a few ways:
- You can leave the Duck and Platypus classes unmodified.
- You can replace existing functions with new ones. So if your platypus already has a layEggs() method, you can replace it using a patch.
- Like with composition and mixins, we've saved our inheritance opportunity for something more appropriate.
Patching also has a number of challenges with it, though:
- You have to assign the function to the object after the object has been created. It doesn't happen automatically when you create it.
- Debugging can become a pain. When an exception happens, you'll know where the layEggs() function is defined, but you have to hunt for the place where you tacked it onto the object.
For these reasons, although it's flexible, I generally avoid patching if I can.
Also of interest, in my particular version of CF9 (version 9,0,0,251028), functions added via mixins and patches do not show up on a cfdump. It does work fine in my CF10 (version 10,282462).
Which one should I use?
Naturally, it depends. Here are a few guidelines to help you choose:
- If your component is a more specific type of the other component (e.g., Duck is a specific type of Bird), inheritance might make sense.
- If your component is not a more specific type of the other component, but simply has a capability that's defined in it (e.g., Platypus can lay eggs even though it's a mammal), then composition or a mixin might be a good choice. If you need to change out the logic at runtime, composition would be favorable.
- Generally avoid patching if you can. It might make sense if you're building tools to operate on other objects, such as unit testing tools, object inspectors, etc. But use it as a last resort.
No comments:
Post a Comment