Beefy Boxes and Bandwidth Generously Provided by pair Networks
Come for the quick hacks, stay for the epiphanies.
 
PerlMonks  

Best practices passing database handles, cgi objects, etc.

by xtpu2 (Acolyte)
on Feb 18, 2014 at 09:41 UTC ( [id://1075303]=perlquestion: print w/replies, xml ) Need Help??

xtpu2 has asked for the wisdom of the Perl Monks concerning the following question:

I'm working on a somewhat complex application that uses CGI, CGI::Session and DBI. I've been following a practice of having no globals and passing variables to every subroutine that I have because this served me well in the past. However, I'm running into the problem that I'm passing 5-6 variables to each subroutine, and what's worse, sometimes that particular subroutine doesn't even need the variables, except to pass to another subroutine (!).</p.

So for example:

MAIN: { my $cgi = CGI->new(); my $session = CGI::Session->new(...); my $dbh = $dbh->connect(...); my $result = do_something($cgi, $session, $dbh); print $cgi->header().$result; } sub do_something { my ($cgi, $session, $dbh) = @_; $dbh->foo(...); $cgi->bar(...); my $foo = another_subroutine($cgi, $dbh, $somevar, $othervar); return final_result ($cgi, $session, $foo); } sub another_subroutine { my ($cgi, $dbh, $somevar, $othervar) = @_; return $dbh->baz($cgi, $somevar, $othervar); } sub final_result { my ($cgi, $session, $foo) = @_; my $result = # operations with $cgi, $session and $foo return $result; }

Basically, you get the idea. I'm probably doing something wrong when it comes to the actual flow of the program, but I have no idea what or where to start. Maybe what I'm really asking for are tips on designing perl CGI applications?

Replies are listed 'Best First'.
Re: Best practices passing database handles, cgi objects, etc.
by McA (Priest) on Feb 18, 2014 at 09:48 UTC

    Good question. I'm really curious about the answers as this is an application design question.

    UPDATE: I found it: Global or not global. Have a look at the answers.

    Regards
    McA

Re: Best practices passing database handles, cgi objects, etc.
by tobyink (Canon) on Feb 18, 2014 at 12:33 UTC

    Use object-oriented code. This will allow you to bundle up a few separate variables into a single object.

    package MyApp { use Moo; has cgi => (is => 'ro'); has database => (is => 'ro'); has session => (is => 'ro'); sub do_something { my $self = shift; $self->databsse->foo(...); $self->cgi->bar(...); my $foo = $self->another_subroutine($somevar, $othervar); return $self->final_result($foo); } sub another_subroutine { my $self = shift; my ($somevar, $othervar) = @_; return $self->database->baz($self->cgi, $somevar, $othervar); } sub final_result { my $self = shift; my ($foo) = @_; my $result = $self->cgi + $self->session + $foo; return $result; } }

    Any sufficiently complex application will need more than one class though, so there will still be an element of passing handles around when one object needs to construct another object. But this can be made quite neat and self-contained. For example, let's assume that MyApp objects occasionally need to create MyApp::Article objects, representing a page of content...

    package MyApp { ...; # all that stuff above has article_class => (is => 'ro', default => 'MyApp::Article'); sub create_article_object { my $self = shift; return $self->article_class->new( cgi => $self->cgi, database => $self->database, session => $self->session, @_ ); } sub get_home_page { my $self = shift; return $self->create_article_object(identifier => 1); } sub get_contact_page { my $self = shift; return $self->create_article_object(identifier => 2); } } package MyApp::Article { use Moo; has identifier => (is => 'ro'); has cgi => (is => 'ro'); has database => (is => 'ro'); has session => (is => 'ro'); ...; }

    That's all object-oriented programming is really... packaging up bundles of subs into neat little objects that contain all the data those subs need.

    Aside: you can do the exact same thing with closures, but in Perl objects are somewhat more elegant than closures.

    The venerable master Qc Na was walking with his student, Anton. Hoping to prompt the master into a discussion, Anton said "Master, I have heard that objects are a very good thing - is this true?" Qc Na looked pityingly at his student and replied, "Foolish pupil - objects are merely a poor man's closures."

    Chastised, Anton took his leave from his master and returned to his cell, intent on studying closures. He carefully read the entire "Lambda: The Ultimate..." series of papers and its cousins, and implemented a small Scheme interpreter with a closure-based object system. He learned much, and looked forward to informing his master of his progress.

    On his next walk with Qc Na, Anton attempted to impress his master by saying "Master, I have diligently studied the matter, and now understand that objects are truly a poor man's closures." Qc Na responded by hitting Anton with his stick, saying "When will you learn? Closures are a poor man's object." At that moment, Anton became enlightened.

    Guy Steele

    use Moops; class Cow :rw { has name => (default => 'Ermintrude') }; say Cow->new->name

      If I understand the example correctly, then the main program (the one calling MyApp) will look something like this?

      use MyApp; my $myapp->new( cgi => CGI->new(), session => CGI::Session->new(), database => DBI->connect() ); print CGI->header().$myapp->do_something();

        Yes, or better:

        use MyApp; my $myapp->new( cgi => CGI->new(), session => CGI::Session->new(), database => DBI->connect() ); print $myapp->process_request();

        Where the process_request method is something like:

        sub process_request { my $self = shift; $self->cgi->header . $self->do_something; }
        use Moops; class Cow :rw { has name => (default => 'Ermintrude') }; say Cow->new->name
        I usually include all the functionality into the class so I do not have to care about its implementation in the main application. Therefore, I would use
        $myapp->header

        instead of

        CGI->header

        If you later decide you need to change something in the header (e.g. the encoding/charset), you just edit the header method of the class.

        لսႽ† ᥲᥒ⚪⟊Ⴙᘓᖇ Ꮅᘓᖇ⎱ Ⴙᥲ𝇋ƙᘓᖇ

      I really hoped that one of our Abbots or other master monks would come out of their meditation to enlighten us. Special thanks for posting the addendum ;-)

      I wish I could see more comments on this topic.

      Best regards
      McA

Re: Best practices passing database handles, cgi objects, etc.
by Anonymous Monk on Feb 18, 2014 at 11:13 UTC
    This might not be the best approach but is workable. If we declare these subroutines:
    { my $cgi = CGI->new(); my $session = CGI::Session->new(...); my $dbh = $dbh->connect(...); sub get_cgi { return $cgi; } sub get_session { return $session; } sub get_dbh { return $dbh; } }
    We can then shorten the subroutines' argument lists considerably:
    MAIN: { my $result = do_something(); print get_cgi()->header().$result; } sub do_something { my $dbh = get_dbh(); my $cgi = get_cgi(); $dbh->foo(...); $cgi->bar(...); my $foo = another_subroutine($somevar, $othervar); return final_result($foo); } sub another_subroutine { my ($somevar, $othervar) = @_; return get_dbh()->baz($somevar, $othervar); } # and so on
    The other usual options would be wrapping these subroutines inside a class, so you can store the objects in class or instance variables; or making a package or a (singleton?) class whose only purpose is to initialise, store, and return these objects.

      Fantastic! A technique to avoid using global variables, but while still keeping all the problems associated with global variables!

      The problem with using global variables is not an irrational fear of the our keyword. The problem is that it introduces global state. This means that if you write an app that, say, operates a blog, and has a database handle as part of its global state, it becomes very difficult to reuse any of that code when you want to write an app that manages multiple blogs (across multiple databases). All your functions are picking up the same database handle from some global place, so you can't tell one function that it needs to copy an article from blog A, and another function that the same article needs to be pasted into blog B.

      Global state makes testing very difficult. If function do_something isn't passed a copy of $dbh as an argument, but instead picks it up from global state, then it becomes difficult to test do_something by passing it test/dummy database handles.

      So the enemy is not global variables. The enemy is global state. Global variables are a manifestation of global state, but so are the techniques you suggest above.

      use Moops; class Cow :rw { has name => (default => 'Ermintrude') }; say Cow->new->name
        Sigh.

        What is wrong with keeping a CGI object and a CGI::Session object as global state/variables/whatever? They are request-dependent. They need to be overridden at the start of each request. Are you planning to serve HTTP requests in a non-serial manner or something?

        The database handle: There's usually one per process. One FCGI-ish process usually handles a single web site. What is the problem storing a handle globally in such a case? Certainly, if you plan to connect to multiple databases in your program you don't use that design, but you have to concoct pretty elaborate scenarios to get to a point where this single-handle thing falls down.

        And before you say 'unit testing', you can override those functions, can't you?

        I fail to see how your OO approach is any better; all it does is wrap some global variables inside a class.

        Also, it's silly to store multiple blogs (with identical schemas, I presume?) across multiple databases.

Re: Best practices passing database handles, cgi objects, etc.
by scorpio17 (Canon) on Feb 18, 2014 at 14:27 UTC
    Take look at CGI::Application (and its associated plugins) - it uses an object-oriented approach. The code you actually write simply uses $self->session to access the session object, $self->query to access the cgi object, $self->dbh to access the database object, etc.
Re: Best practices passing database handles, cgi objects, etc.
by sundialsvc4 (Abbot) on Feb 18, 2014 at 14:45 UTC

    While all of the comments made here are “of course, quite valid,” it’s difficult to make categorical statements about things like this.   What I generally recommend and try to do is to put all “truly-global things” into a package, ordinarily named (say ...) AppGlobals, which contains not only the storage for the global values in question, but “aggressive accessors” for them.   (For instance, a subroutine that is supposed to return a database-handle, finding instead for whatever reason that the handle is undef, will die on the spot.)   If we have to deal with things like “multiple blogs” or what-have-you, the logic in this one place is responsible for dealing with it appropriately.

    I specifically prefer not to “pass” such things around, because an error could be introduced by any one (or more) of those “hands” that are supposed to at all times be correctly “holding it” and “passing around the right thing.”   Too many potential points-of-failure for my taste.   So, instead, I define one global place to put things, and I make accessors which are both smart and suspicious.   Clients to this package never access the (private ...) variables directly.   This discipline seems to work out pretty good . . .

      I'm not sure I completely understand how your solution differs from that suggested by tobyink (but then, I'm not sure I completely understand his solution... sorry, I don't have much experience with all this!)

      How are the global variables in the AppGlobals module assigned their values? Does the dbh, cgi, session, etc, have to be passed into AppGlobals first? In that case, don't I just simply end up having to pass the AppGlobals object to every one of my subroutines? I suppose this does reduce the argument list from 3 items to just 1 item.

      Or are you suggesting that the CGI object, session and DB handler are actually created inside the AppGlobals package, and then I access them using AppGlobals::get_dbh, AppGlobals::get_cgi, etc? So really it would be like the solution proposed by Anonymous Monk, except moved into a separate package?

      I'm sorry if I'm missing something that should be obvious...

Re: Best practices passing database handles, cgi objects, etc.
by pajout (Curate) on Feb 18, 2014 at 10:03 UTC
    I recommend to see Bugzilla (http://www.bugzilla.org/) source codes.
Re: Best practices passing database handles, cgi objects, etc.
by McA (Priest) on Feb 19, 2014 at 16:34 UTC

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://1075303]
Approved by hdb
Front-paged by Arunbear
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others chanting in the Monastery: (5)
As of 2024-03-29 09:25 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found