Breaking up Fat Models With Delegation
Hope everyone had a relaxing Christmas with loads of great food, booze and gossip. I know I did!
So not being a classically trained CS guy, I have no idea what the name for this pattern is, but I find myself using it more and more to break up fat models. It basically involves a lot of explicit delegation in the class, but no decorator classes. I’m still wondering whether this pattern is a “good thing” or not, so I’d appreciate comments.
Use case - your model has been collecting a lot of crufty methods that don’t have anything, really, to do with the logic proper of the model. Classic examples are methods like full_name, which basically only deal with presentation or some orthagonal logic like currency conversion and so on. There have been several approaches to fixing the presentation issue, the most notable of which has been Draper, but I didn’t enjoy using Draper. For one, I don’t want to call UserDecorator.decorate(User.get(params[:id])). Why? I just don’t ok?! Just kidding - actually in my experience, decorating large arrays (think CSV dump for the last months data) takes f.o.r.e.v.e.r and damned if I didn’t get some subtle bugs with DataMapper associations on the decorated class. I didn’t dig too deep, being a shallow and easily influenced guy, and instead started looking for other solutions. I present mine below
| require 'forwardable' | |
| class User | |
| extend Forwardable | |
| attr_accessor :view_delegate_class | |
| def_delegators :view_delegate, :full_name | |
| def initialize(first_name, last_name) | |
| @first_name = first_name | |
| @last_name = last_name | |
| end | |
| # if we set @view_delegate_class, we can override which class we use to show the full name | |
| # thus we can inject a dependency and at runtime choose the class to delegate to. | |
| def view_delegate | |
| (@view_delegate_class || UserViewDelegate).new(self) | |
| end | |
| end | |
| class UserViewDelegate | |
| def initialize(user) | |
| @user = user | |
| end | |
| def full_name | |
| "#{first_name} #{last_name}" | |
| end | |
| # any method not defined here is going to be called on the @user. | |
| # normally, since these classes are extracted from larger classes, it saves us | |
| # having to say @user.first_name, @user.last_name, etc. | |
| # also, there it protects us from changes to the User class if we don't repeat the API here. | |
| def method_missing(name, *args) | |
| @user.send(name, *args) | |
| end | |
| end | |
So there are several funky things about this approach.
- First of all, it is totally explicit. There is absolutely zero magic going on here.
- Secondly, because of the injected dependency, we can choose the class we would like to use to present our object at runtime.
- Thirdly, it saves us from having to explicitly decorate our objects.
- Fourthly, the ViewDelegate object gets access to all the original objects methods using
method_missing. This means that the implicitselfin the ViewDelegate class is the original object for all practical purposes. Thus, any object that responds to the required methods can use this Delegate, not just a User. - Fifthly, the delegate objects are not instantiated until one of the delegated methods are called. This might have performance implications. Of course, the ViewDelegate can be memoized as well.
- Lastly, this can be used for any type of delegation, not just for presentation. For example, one might choose to delegate a
heightmethod to aMetricUnitDelegateor to anImperialUnitDelegate, depending on the context.
I have no idea if this is a good or even an original approach. Would love to hear from you in the comments.