FYI: Don’t judge the code in the following article part too harshly.  It’s all 
been written off-the-cuff.  I have not yet tried to save it out into a final 
app and make sure it runs.  That will be part of the polishing passes to come.  
The main thing I want comment on is the overall ideas presented, not the niggly 
details.

I don’t mean that I do not welcome nit-picking, just that I prefer that you 
keep it brief.  Yes, I realize that the code probably doesn’t work as-is yet, 
and yes, I realize that I probably have committed spelling and grammar 
mistakes.  Kindly just point them out, without discussing how this pile of 
errors reflects on my ancestry. :)



Part 2: Route Definitions
-------------

By the time a Dancer app grows large enough that you want to start breaking it 
up into multiple Perl modules, as in the previous article in this series, 
you've probably also defined enough routes that you're starting to have 
problems managing them all. Just as with the Perl code, Dancer lets us break up 
the monolithic route definition set, too.

If you structured your app in the way recommended in the first part of this 
series, each major feature of your web app is in its own Perl module. That Perl 
module's name likely corresponds to some part of your app's URL scheme. Let's 
say you're exposing the features of `App::MajorFeature` as `/mf` in URLs, with 
sub-features underneath that.

If you extend the generated `lib/App.pm` file in the most obvious way, using 
the simplest examples from the Dancer documentation, you might have a mess that 
looks something like this:

    get '/mf' => sub {
        # Lots of Perl code to return the top-level MajorFeature view
    };
    get '/mf/subfeature' => sub {
        # Implementation of a sub-feature of MajorFeature
    };
    post '/mf/subfeature' => sub {
        # Maybe you need a way to add new subfeature objects
    };
    put '/mf/subfeature' => sub {
        # And maybe also a way to edit existing subfeature objects
    };
    del '/mf/subfeature/:id' => sub {
        # And a way to delete them, too
    };
    
The first thing to fix here is that almost all of the Perl code implementing 
each route handler should move to `lib/App/*.pm`. Ideally, each route handler 
body should do nothing more than call a function in one of these modules:

    get  '/mf'                => sub { App::MajorFeature::get(context); };
    get  '/mf/subfeature'     => sub { App::MajorFeature::sub_feature(context); 
};
    post '/mf/subfeature'     => sub { App::MajorFeature::add(context); };
    put  '/mf/subfeature'     => sub { App::MajorFeature::modify(context); };
    del  '/mf/subfeature/:id' => sub { App::MajorFeature::remove(context); };
    
The `context()` function is a small wrapper I define at global scope within the 
app:

    sub context {
        return {
            config  => config(),
            request => request(),
            session => session(),
            conn    => App::Utility::get_db_conn(),
            etc     => ...
        };
    }
    
That is, it just bundles up a bunch of Dancer objects into a hash for you. I 
find this convenient, but perhaps you will want to use the DSL instead.

The URL scheme defined above is quite redundant. We can factor out that 
redundancy in two stages.

First, Dancer has the awesome 
[`prefix`](https://metacpan.org/pod/distribution/Dancer2/lib/Dancer2/Manual.pod#prefix)
 feature, which lets us express the URL hierarchy directly in the code, without 
repeating each element:

    prefix '/mf' => sub {
        get '/' => sub { App::MajorFeature::get(context); };
        prefix '/subfeature' => sub {
            get  '/'    => sub { App::MajorFeature::sub_feature(context); };
            post '/'    => sub { App::MajorFeature::add(context); };
            put  '/'    => sub { App::MajorFeature::modify(context); };
            del  '/:id' => sub { App::MajorFeature::remove(context); };
        };
    };

Factoring out the `/mf` part was a net loss of 1 character per route with our 
4-space indents, but factoring out `/subfeature` saved us a net 6 characters 
per route, which really helps make the code easier to read.

You may find after rewriting your URL handlers this way that you see structural 
patterns in the URLs that will lead you toward a better URL design. This can be 
especially helpful in REST API design, which we will discuss in a later part of 
this article series.

But to get back to our refactoring work, a second excellent feature of Dancer 
lets us shorten those lines of code still further.

So far, we've been using explicitly-qualified function names. This is because 
we want to use short function names within the modules (e.g. `Get()`) without 
causing namespace collisions by exporting all of the functions. But in fact, 
there is actually no need to expose the API of your modules outside the module 
itself. Dancer doesn't care *where* you define the route handlers, just that 
they're all defined by the time your caller wants to use them. In the previous 
part of this article series, we said `use App::MajorFeature` and such within 
`lib/App.pm`, so every one of our app's modules gets executed on startup. This 
means that any code at global scope within these modules also runs at startup.

Therefore, we can move all of the route definitions above from `lib/App.pm` to 
the end of `lib/App/MajorFeature.pm`:

    prefix '/mf' => sub {
        get '/' => sub { get(context); };
        prefix '/subfeature' => sub {
            get  '/'    => sub { sub_feature(context); };
            post '/'    => sub { add(context); };
            put  '/'    => sub { modify(context); };
            del  '/:id' => sub { remove(context); };
        };
    };

Now all the function calls are made within the module itself, so we don't need 
to qualify them.

If you are using my `context()` idea, you will have to move it out of 
`lib/App.pm`, such as into the `App::Utility` module, exported by default:

    package App::Utility {
        use Dancer2 appname => 'App';
     
        require Exporter;
        use base qw(Exporter);
        our @EXPORT = qw(context);

        sub get_db_conn {
            # do something useful here;
            # not exported, since context->{conn} holds our return value
        }
   
        sub context {
            return {
                config  => config(),
                request => request(),
                session => session(),
                conn    => get_db_conn(),
                etc     => ...
            };
        }
    }
    
In the first part of this article series, we moved almost all of the Perl code 
from `lib/App.pm` into a collection of tightly-scoped `lib/App/*.pm` modules. 
In this second part, we moved most of the route definitions into those modules, 
too, not only cleaning up `lib/App.pm`, but also shortening the definition of 
those route handlers considerably by removing redundant code. All that should 
be left in `lib/App.pm` is app-wide Dancer setup code, such has global hook 
definitions.

In the next part, we will consider how this application restructuring affects 
the design of other parts of the web app.
_______________________________________________
dancer-users mailing list
[email protected]
http://lists.preshweb.co.uk/mailman/listinfo/dancer-users

Reply via email to