You are working currently on that awesome app and just started thinking about implementing new feature, let's call it feature X. What's the first thing you do? Rolling your own solution or... maybe checking if there's a magical gem that can help you solve that problem? Ok, it turns out there's already a gem Y that does what you expect. Also, it does tons of other things and is really complex. After some time your app breaks, something is definitively not working and it seems that gem Y is responsible for that. So you read all the issues on Github, pull requests and even read the source code and finally find a little bug. You managed to do some monkeypatching first and then send pull request for a small fix and solved a problem, which took you a few hours. Looks like a problem is solved. And then, you try to update Rails to2 the current version. Seems like there's a dependency problem - gem Y depends on previous version of Rails...

Does it sound somehow familiar to you, especially when updating Rails? If it's caused by "big" gem that solves a lot of problems and is still maintained, it's not that bad. How about these "small" gems, which come in handy and look quite complex but you could roll your own solution to that particular problem within half an hour?

I've seen this many times where first thing when implementing new feature is searching for a gem that solves this problem. We need really basic polymorphic tags? Let's use acts-as-taggable-on, we don't need half of the features provided and setting up a few migrations and associations would take 10 minutes anyway but there's no need to reinvent the wheel. Client asked for a simple admin panel with some CRUD stuff involving several models. Let's use active_admin or rails_admin for that! Simple searching / filtering where several fields in one model are involved? Ransack is an obvious choice!

More gems mean: slower boot time of your application, more dependencies (don't look only at Gemfile, Gemfile.lock is the real deal), more things that can break, more issues to take care of when updating Rails and the gem itself (reading changelogs, issues etc.).

When to use third party gem when implementing feature X?

Gem is actively being maintained, there were some commits not that long ago (basic prerequisite)

Gem solves the exact problem you have and does only that

Gem does plenty of other things but it deals with areas you are not really familiar involving security, encryption etc. (e.g. symmetric-encryption)

Gem does many other things and you need just a small part of it but rolling your own solution would take really a lot of time (e.g. devise)

Gem deals with complex infrastructure things (e.g. carrierwave)

Gem has a nice integration with many other gems you already use

Remember that using gem Y means also reading docs and it might be beneficial to read some parts of source code, just to get the general idea how it works. It also takes some time. Why not implement your own solution? It might look like reinventing the wheel but no gem will be that customizable to the extent you need. Or you will be using just a small part of it and the models / controllers will have tens of additional methods you don't need (and hundreds more with several gems).

Writing own solution

How much work does it really take to reimplement a gem? Let's take a look at something popular - draper gem. Draper is a pretty good solution for decorators/presenters for your models in Rails apps. I've been using it for quite long a time, had some issues but managed to solve them rather quickly. Unfortunately, the source code looks quite complex, especially extracting view_context with a bit global-variable-like RequestStore. And there are some other complex parts that I don't really use. Let's write custom presenter and call it DecentPresenter. What kind of interface and conventions would I expect from it?

Include some module in controllers (ApplicationController) - I want to be explicit here, without including it automatically on Rails app boot. Also, I don't want to include it in models - model doesn't have to know that it can be presented in one way or another

Establish naming convention: default presenter for User would be UserPresenter

Call present(user) in controller which would wrap user by UserPresenter and present(User.all) which would handle collections

in controller which would wrap user by and which would handle collections Ability to specify other presenter than the default one - present(user, with: OtherPresenter)

Have access to helpers within presenters

Presenters will inherit from some base class ( DecentPresenter::Base )

Doesn't really look that hard. Getting access to Rails helpers might seem difficult but we can get it from view_context in controllers. Let's start with integration test for presenters. We want to include a module to a class (Controller), which would mix in present method. Let's call it DecentPresenter::Exposable:

# spec/decent_presenter/exposable_spec.rb require 'spec_helper' class DummyModelForExposableTest def name "name" end end class DummyModelForExposableTestPresenter < DecentPresenter :: Base def name "presented name" end end class DummyModelForExposableOtherPresenter < DecentPresenter :: Base def name "other presented name" end end class DummyObjectErrorPresenterExposable include DecentPresenter :: Exposable def present_model ( model ) present ( model ) end end class DummyObjectPresenterExposable def view_context ; end include DecentPresenter :: Exposable def present_model ( model ) present ( model ) end def present_model_with_options ( model , options ) present ( model , options ) end def present_collection ( collection ) present ( collection ) end def present_collection_with_options ( collection , options ) present ( collection , options ) end end describe DecentPresenter :: Exposable do context "view_context prerequisite" do it "raises DoesNotImplementViewContextError if view_context method is not defined" do model = DummyModelForExposableTest . new expect do DummyObjectErrorPresenterExposable . new . present_model ( model ) end . to raise_error DecentPresenter :: Exposable :: DoesNotImplementViewContextError , "Object must implement :view_context method to handle presentation" end it "doesn't raise DoesNotImplementViewContextError if view_context method is defined" do model = DummyModelForExposableTest . new expect do DummyObjectPresenterExposable . new . present_model ( model ) end . not_to raise_error end end context "presentation" do let ( :model ) { DummyModelForExposableTest . new } let ( :collection ) { [ model ] } subject { DummyObjectPresenterExposable . new } it "presents model with default presenter" do expect ( subject . present_model ( model ). name ). to eq "presented name" end it "presents model with specified presenter" do expect ( subject . present_model_with_options ( model , with: DummyModelForExposableOtherPresenter ). name ). to eq "other presented name" end it "presents models collection with default presenter" do expect ( subject . present_collection ( collection ). first . name ). to eq "presented name" end it "presents models collection with specified presenter" do expect ( subject . present_collection_with_options ( collection , with: DummyModelForExposableOtherPresenter ). first . name ). to eq "other presented name" end end end

What happens here? First off, we set up some dummy classes: DummyModel with name method, two presenters for testing with default presenter and other presenter and two classes, where we include DecentPresenter::Exposable module. Why two? Just to check that if the object implements view_context method. If the view_context method is not implemented, we provide descriptive error. Then we write some tests for a single model / collection and default / custom presenter to check if they are presented. Let's write some code:

# lib/decent_presenter/exposable.rb module DecentPresenter module Exposable def present ( presentable , options = {}) if respond_to? :view_context , true # decorate the presentable object here else raise DecentPresenter :: Exposable :: DoesNotImplementViewContextError . new ( "Object must implement :view_context method to handle presentation" ) end end class DoesNotImplementViewContextError < StandardError ; end end end

And just define base class for presenters:

# lib/decent_presenter/base.rb moduleDecentPresenter class Base end end

Tests within presentation context still fail but they will be the last ones that will pass. Let's leave them for now and thing about base class - DecentPresenter::Base and it's subclasses, our presenters. We want to have access to helpers by helpers method and h for shorthand. We also need have access to presented object: by object and model methods. If the method isn't implemented by presenter, it should be delegated to presented model. Sounds like method_missing ? That's one possibility. Let's try something different - SimpleDelegator. SimpleDelegator is a pretty cool core class with two public methods __getobj__ and __setobj__ - the first one exposes decorated object and the latter sets objects to which all method calls will be delegated. The object is set when we pass it to the constructor of SimpleDelegator . Looks like our DecentPresenter::Base class will inherit from SimpleDelegator . But we will need to override constructor to pass view_context . Also, it would be quite useful to be able to present other objects within our presenters. Let's write some tests:

# specs/decent_presenter/base_spec.rb require 'spec_helper' class DummyModelForBaseClassTest def name "dummy_model_name" end def stuff "stuff" end end class DummyViewContext ; end class DummyModelPresenter < DecentPresenter :: Base def name "presented #{ model . name } " end end describe DecentPresenter :: Base do let ( :model ) { DummyModelForBaseClassTest . new } let ( :view_context ) { DummyViewContext . new } context "base" do subject { DecentPresenter :: Base . new ( model , view_context ) } it "exposes model as model" do expect ( subject . model ). to eq model end it "exposes model as object" do expect ( subject . object ). to eq model end it "exposes view_context as h" do expect ( subject . h ). to eq view_context end it "exposes view_context as helpers" do expect ( subject . helpers ). to eq view_context end end context "subclass" do subject { DummyModelPresenter . new ( model , view_context ) } it "decorates model's methods" do expect ( subject . name ). to eq "presented dummy_model_name" end it "delegates method calls to model when the method is not defined within presenter" do expect ( subject . stuff ). to eq "stuff" end it "implements presentable interface" do expect ( subject ). to respond_to :present end end end

Like before, we setup some DummyClasses and write some tests for the requirements we've just discussed. Our base class implements view_context so it looks like we just need to include DecentPresenter::Exposable module and we will be able to decorate other objects within our presenters. We cover it be checking if the subclasses implement required interface. We also cover some delegation stuff in tests, SimpleDelegator ensures it will be delegated but it is a core functionality for presenters so it might be a good idea to test for it. Let's write the implementation:

# lib/decent_presenter/base.rb module DecentPresenter class Base < SimpleDelegator include DecentPresenter :: Exposable attr_reader :view_context private :view_context def initialize ( object , view_context ) super ( object ) @view_context = view_context end def model __getobj__ end alias :object :model def helpers view_context end alias :h :helpers end end

We need a main interface which would wrap our models within presenters. Sure, we could do it in present method but I don't really like the idea that the e.g. controller would know, how to present a model. Let's implement dedicated interface which is going to be used by present method. We have some integration tests for presenters, base class is already covered, so we will just need to check, if a model or collection is decorated by presenters:

# spec/decent_presenter/exposure_spec.rb require 'spec_helper' class DummyModelForExposureTest ; end class DummyModelForExposureTestPresenter < DecentPresenter :: Base ; end class DummyModelForExposureTestOtherPresenter < DecentPresenter :: Base ; end describe DecentPresenter :: Exposure do let ( :presenter_factory ) { double ( :presenter_factory ) } let ( :view_context ) { double ( :view_context ) } let ( :model ) { DummyModelForExposureTest . new } let ( :collection ) { [ model ] } subject { DecentPresenter :: Exposure . new ( view_context , presenter_factory ) } before ( :each ) do allow ( presenter_factory ). to receive ( :presenter_for ) . with ( model ) { DummyModelForExposureTestPresenter } end it "presents model with default presenter" do presented_model = subject . present ( model ) expect ( presented_model ). to be_instance_of DummyModelForExposureTestPresenter end it "presents model with specified presenter" do presented_model = subject . present ( model , with: DummyModelForExposureTestOtherPresenter ) expect ( presented_model ). to be_instance_of DummyModelForExposureTestOtherPresenter end it "presents models in collection with default presenter" do presented_collection = subject . present ( collection ) expect ( presented_collection . first ). to be_instance_of DummyModelForExposureTestPresenter end it "presents models in collection with specified presenter" do presented_collection = subject . present ( collection , with: DummyModelForExposureTestOtherPresenter ) expect ( presented_collection . first ). to be_instance_of DummyModelForExposureTestOtherPresenter end end

The Exposure class is going to have one public method which takes model or collection as the first argument and the options hash, where we can specify presenter. If it's not specified the default presenter will be used. And how do we know what the default presenter is? Don't know yet, so let's introduce a collaborator, which takes model and returns default presenter for it. We will call it presenter_factory . We also need to remember about the view_context dependency. Let's write the implementation:

Note: again, the implementation is quite clean, but remember the TDD cycle: red, green, refactor, write minimal implementation for the first test, make it pass and repeat. This post is not about how to TDD properly and to focus on the core things I just give code after the refactoring phase.

# lib/decent_presenter/exposure.rb module DecentPresenter class Exposure attr_reader :view_context , :presenter_factory private :view_context , :presenter_factory def initialize ( view_context , presenter_factory ) @view_context = view_context @presenter_factory = presenter_factory end def present ( presentable , options = {}) if presentable . respond_to? ( :size ) present_collection ( presentable , options ) else present_model ( presentable , options ) end end private def present_model ( presentable , options = {}) presenter = options . fetch ( :with ) do presenter_factory . presenter_for ( presentable ) end presenter . new ( presentable , view_context ) end def present_collection ( collection , options = {}) collection . map { | el | present_model ( el , options ) } end end end

We need somehow to distinguish between collection and a single model. The collection will probably implement the size method. What if the model also implements size method? Looks like we need to make some paranoid check. Let's modify test for DecentPresenter::Exposure and add size method to DummyModel:

# spec/decent_presenter/exposure_spec.rb # other code class DummyModelForExposureTest def size ; end end # other code

Now the tests fail. How can we make sure the collection really is a collection? Besides size , it'll probably implement to_a and first methods. Let's update the implementation:

# lib/decent_presenter/exposure.rb module DecentPresenter class Exposure attr_reader :view_context , :presenter_factory private :view_context , :presenter_factory def initialize ( view_context , presenter_factory ) @view_context = view_context @presenter_factory = presenter_factory end def present ( presentable , options = {}) if presentable_is_a_collection? ( presentable ) present_collection ( presentable , options ) else present_model ( presentable , options ) end end private def present_model ( presentable , options = {}) presenter = options . fetch ( :with ) do presenter_factory . presenter_for ( presentable ) end presenter . new ( presentable , view_context ) end def present_collection ( collection , options = {}) collection . map { | el | present_model ( el , options ) } end def presentable_is_a_collection? ( presentable ) [ :size , :to_a , :first ]. all? { | method | presentable . respond_to? method } end end end

Looks like we only have DecentPresenter::Factory left. The factory should return a default presenter (constant) based on model's class. What if it doesn't exist? We will provide descriptive error message. Let's write tests:

# spec/decent_presenter/factory_spec.rb require 'spec_helper' class DummyModelForFactoryPresenter ; end class DummyModelForFactory ; end class OtherDummyModel ; end describe DecentPresenter :: Factory do subject { DecentPresenter :: Factory } it "implements DecentPresenter Factory interface" do expect ( subject ). to respond_to :presenter_for end describe ".presenter_for" do it "gives presenter class based on object's class in convention: KlassPresenter" do model = DummyModelForFactory . new expect ( subject . presenter_for ( model )). to eq DummyModelForFactoryPresenter end it "raises PresenterForModelDoesNotExist error if presenter class is not defined" do model = OtherDummyModel . new expect do subject . presenter_for ( model ) end . to raise_error DecentPresenter :: Factory :: PresenterForModelDoesNotExist , "expected OtherDummyModelPresenter presenter to exist" end end end

I also added test for covering factory interface to guard against changing interface which is used in DecentPresenter::Exposure - you can imagine the situation where the method name is changed and the tests still pass. We have integration tests for it ( DecentPresenter::Exposable ) but having these kind of tests underlines the fact that it shouldn't be changed. To extract the presenter's name from model we will ask model for it's class, add "Presenter" suffix and use classify method:

# lib/decent_presenter/factory.rb module DecentPresenter module Factory extend self def presenter_for ( model ) presenter_class_name = " #{ model . class } Presenter" begin presenter_class_name . constantize rescue NameError raise PresenterForModelDoesNotExist . new ( "expected #{ presenter_class_name } presenter to exist" ) end end class PresenterForModelDoesNotExist < StandardError ; end end end

The last thing is to make integration tests pass, let's finish the present method:

# lib/decent_presenter/exposable.rb module DecentPresenter module Exposable def present ( presentable , options = {}) if respond_to? :view_context DecentPresenter :: Exposure . new ( view_context , DecentPresenter :: Factory ). present ( presentable , options ) else raise DecentPresenter :: Exposable :: DoesNotImplementViewContextError . new ( "Object must implement :view_context method to handle presentation" ) end end class DoesNotImplementViewContextError < StandardError ; end end end

Seems like we are almost done. All tests pass but one common use case still won't work - pagination. We can use some array pagination but it's pretty inconvenient. We need somehow to keep a reference of the original collection, delegate some pagination methods to the original collection and other methods should be handled by presented collection. Sounds like a proxy? Let's write tests for DecentPresenter::CollectionProxy :

# spec/decent_presenter/collection_proxy_spec.rb require 'spec_helper' TEST_COLLECTION_PROXY_PAGINATION_METHODS = [ :current_page , :total_pages , :limit_value , :model_name , :total_count , :total_entries , :per_page , :offset ] class DummyOriginalForCollectionProxy TEST_COLLECTION_PROXY_PAGINATION_METHODS . each do | method | define_method method do "original" end end end class DummyPresentedForCollectionProxy def presented_method "presented" end def other_presented_method "presented" end end describe DecentPresenter :: CollectionProxy do subject { DecentPresenter :: CollectionProxy . new ( DummyOriginalForCollectionProxy . new , DummyPresentedForCollectionProxy . new ) } it "delegates pagination-related methods to original collection" do TEST_COLLECTION_PROXY_PAGINATION_METHODS . each do | pagination_method | expect ( subject . send ( pagination_method )). to eq "original" end end it "delegates other methods to presented collection" do [ :presented_method , :other_presented_method ]. each do | presented_method | expect ( subject . send ( presented_method )). to eq "presented" end end end

I searched for some pagination-related methods and put them in TEST_COLLECTION_PROXY_PAGINATION_METHODS . I also introduced two dummy collection classes - one for pagination methods and the latter for handling other methods. To DRY out the implementation I use define_method . The tests verify that the method calls are properly delegated. Let's write the implementation with method_missing :

# lib/decent_presenter/collection_proxy.rb module DecentPresenter class CollectionProxy delegate :current_page , :total_pages , :limit_value , :model_name , :total_count , :total_entries , :per_page , :offset , to: :original_collection attr_reader :original_collection , :presented_collection private :original_collection , :presented_collection def initialize ( original_collection , presented_collection ) @original_collection = original_collection @presented_collection = presented_collection end def method_missing ( method , * args , & block ) presented_collection . send ( method , * args , & block ) end end end

The delegate method from ActiveSupport comes in handy for proxies. We need to modify DecentPresenter::Exposure#present and wrap the presented collection and the original collection in proxy. Let's add a test for that also:

# spec/decent_presenter/exposure_spec.rb require 'spec_helper' describe DecentPresenter :: Exposure do # other code it "wraps collection in CollectionProxy" do presented_collection = subject . present ( collection ) expect ( presented_collection ). to be_instance_of DecentPresenter :: CollectionProxy end end

# lib/decent_presenter/exposure.rb module DecentPresenter class Exposure attr_reader :view_context , :presenter_factory private :view_context , :presenter_factory def initialize ( view_context , presenter_factory ) @view_context = view_context @presenter_factory = presenter_factory end def present ( presentable , options = {}) if presentable_is_a_collection? ( presentable ) present_collection ( presentable , options ) else present_model ( presentable , options ) end end private def present_model ( presentable , options = {}) presenter = options . fetch ( :with ) do presenter_factory . presenter_for ( presentable ) end presenter . new ( presentable , view_context ) end def present_collection ( collection , options = {}) presented_collection = collection . map { | el | present_model ( el , options ) } DecentPresenter :: CollectionProxy . new ( collection , presented_collection ) end def presentable_is_a_collection? ( presentable ) [ :size , :to_a , :first ]. all? { | method | presentable . respond_to? method } end end end

And we are done! The test for verifying if the instance of DecentPresenter::CollectionProxy is returned when handling collection introduces some coupling but I'm pretty comfortable with it. It won't be handled by any other object than the CollectionProxy.

The DecentPresenter is much simpler than Draper, it offers most of the stuff I need in my presenters and writing it was pretty enjoyable :). It didn't take much time and I have a solution, which I'm familiar with and if something breaks I will know why. In fact, I like it so much that I'm going to release it as a gem :).

Wrapping up

Using third party gem isn't always the best solution, sometimes it's quite easy to write similar solution. Many gems are quite complex because they need to handle all possible use cases and that level of complexity and other dependencies might not be necessary for your app.