Beefy Boxes and Bandwidth Generously Provided by pair Networks
more useful options
 
PerlMonks  

Seeking Wisdom: proposed fix for a broken CPAN module

by Tommy (Chaplain)
on Dec 28, 2002 at 08:49 UTC ( [id://222708]=perlquestion: print w/replies, xml ) Need Help??

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

Could I get some feedback for this code. I'm attempting to fix the problems with OOorNO.pm (discussed in node id 222259)

Get the code distro from http://ooopps.sourceforge.net/tmp/Class-OOorNO-0.01.tar.gz

  • Comment on Seeking Wisdom: proposed fix for a broken CPAN module

Replies are listed 'Best First'.
Re: Seeking Wisdom: proposed fix for a broken CPAN module
by adrianh (Chancellor) on Dec 28, 2002 at 23:24 UTC

    Some (hopefully constructive) criticism on the module's intent, tests and code.


    First a few general comments on the intent.

    It's a good thing that you are seeing problems and repetitions with your code and are abstracting these out into modules. Carry on doing it. Practice makes perfect.

    As I understand it, you're aim is to make it easier to code subroutines that can be called in an OO and in a procedural/functional way. The next few comments are not aimed at your module in particular - but in the concept of having a single subroutine work in both of these ways.

    In my opinion the correct solution to getting a subroutine to work in both an OO and functional/procedural way is... "don't do it" :-)

    Mixing API styles in the same subroutines in a single module can confuse both module author and user. If you want both styles then write two separate modules, implementing one in terms of the other as appropriate. Simple. Clean. Easy to understand.

    If you find yourself mixing OO and functional/procedural interfaces in a single module it's probably a cue that there is something wrong with your design.

    An example from recent experience would be my Test::Exception module. The functional API in Test::Exception originally started out as a number of methods in another OO based testing module I was writing (an early precursor of Test::Class if anybody cares). However, by looking at the code I saw that they never actually used the object in question and were a likely prospect to be refactored into a separate module with a functional API... and so Test::Exception was born.

    In my opinion a module making mixing OO/functional/procedural styles easier isn't a good idea because mixing OO/functional/procedural styles isn't a good idea.

    You may find the discussion around How to morph a plain module to OO of interest.


    If you absolutely have to have both implemented by the same subroutines then called_as_method from Devel::Caller is what you need. For example:

    use strict; use warnings; { package Foo; use Devel::Caller qw(called_as_method); use Carp; sub new { bless {}, shift }; sub display { my $self; # extract $self if called as a method $self = shift if called_as_method(0); # check we have even number of key/value pairs croak "missing key/value (@_)" unless @_ % 2 == 0; # make a hash of our key/value pairs my %hash = @_; # show how we were called my $type = "subroutine"; $type = (ref($self) ? "object" : "class") . " method" if $self +; print "$type called with\n"; while (my ($k, $v) = each %hash) { print "$k = $v\n" }; print "\n"; }; }; Foo->new->display(a => 400, b=>800); Foo->display(a => 400, b=>800); Foo::display(a => 400, b=>800); Foo::display(a => 400, b=>800, 900);

    will produce something like:

    object method called with + a = 400 b = 800 class method called with + a = 400 b = 800 subroutine called with a = 400 b = 800 missing key/value (a 400 b 800 900) at /Users/adrianh/Desktop/foo.pl l +ine 35

    Now I've finished saying evil things about the intent - some comments on the test suite :-)

    A round of applause for actually having a test suite. This is something many novice module authors miss out.

    However, you are only testing the appearance of your external API (what methods can or cannot be called, whether exported works, etc.). There is not a single test that checks what the various subroutines actually do!

    I can remove the contents of every single subroutine and all the tests will pass 100%. This is not usually perceived to be a sign of comprehensive test coverage ;-)

    You need to write tests that check what the subroutines are supposed to do. You might find The Joy of Test, What goes in a test suite? and Further adventures with testing of relevance.

    You should also consider moving from the Test module to Test::More and the other Test::Builder based modules since they offer a great deal more flexibility and are far easier to use. Very few people use Test in new modules - and usually only in places where the Test::Builder based modules cannot be used.

    You also don't need use lib './'; in your test routines. make test will handle this sort of thing for you.


    Finally, various comments on the code after a quick review.


    This is somewhat a matter of personal taste - but I find your indentation style completely unreadable. Obviously, if it works for you that is fine. However, if you stick to a style that is closer to the norm people will find it easier to read your code. I chucked everything in your distribution through perltidy before I did anything else so I could read it.


    Since you don't fully document what your module is supposed to do (with tests, POD or using examples) it is difficult to give useful feedback since it's not easy to see the intent behind all of the code.

    For example, I'm not going to comment on shave_opts or coerce_array since I'm not certain of what they are supposed to be doing. I can understand what they do by reading the code. I cannot understand what they are supposed to do without more information. Part of this is down to the naming of the function - shave_opts isn't that descriptive.


    This code

    ref($_[0]) and ( ref($_[0]) eq (caller(0))[0] || ( caller(0) )[0] eq __PACKAGE__ )

    Is used twice in different routines and should probably be refactored into its own subroutine. Once And Only Once is a good motto to code by.

    There are also several situations where the test won't work the way you want. For example:

    • If somebody has an object blessed into package named ARRAY it will get confused with array references (you should use blessed from Scalar::Util instead of ref)
    • When you subclass the name of the package a method is called from need not match the name of the package an object was blessed into (you need to use isa not eq).

    It also does not cope with situations where you are calling a subroutine in a functional/procedural style - and where the first argument happens to be an object of the appropriate type. called_as_method does.


    I don't understand why you reference Getopt::Long and CGI in your documentation - they solve completely different problems.


    It's a matter of personal taste (and depends on whether you need to support versions of Perl where our is not supported), but I prefer to use our and use base instead of use vars and @ISA. I find it succinct and easier to read. In this case:

    use vars qw( $VERSION @ISA @EXPORT_OK %EXPORT_TAGS ); use Exporter; $VERSION = 0.01_0; # 12/28/02, 1:54 am @ISA = qw( Exporter ); @EXPORT_OK = qw( shave_opts coerce_array OOorNO myargs myself ); %EXPORT_TAGS = ( 'all' => [@EXPORT_OK] );

    would become

    use base qw(Exporter); our $VERSION = 0.01_0; our @EXPORT_OK = qw( shave_opts coerce_array OOorNO myargs myself ); our %EXPORT_TAGS = ( 'all' => [@EXPORT_OK] );

    None of your code has warnings enabled. Most people consider this to be good style for a couple of reasons:

    • 99% of the time the warnings are sensible ones, and you should fix whatever is causing them.
    • It can make things difficult for users with versions of perl that do not support use warnings since localising warnings is harder.

    If you enable warnings and nothing comes up great - but leave the statements in place and you are protected from any future problems.


    Comments like this:

    # -------------------------------------------------------- # Constructor # -------------------------------------------------------- sub new { bless( {}, shift (@_) ) }

    and

    # -------------------------------------------------------- # Class::OOorNO::Class::OOorNO() # -------------------------------------------------------- sub OOorNO { return ( $_[0] ) if defined( $_[0] ) and UNIVERSAL::can( $_[0], 'can' ); }

    are redundant. You are just repeating what the code says. You need to add comments when you need to explain why something does what it does, not what it does. In this case we can see by reading the code that new() is the constructor and that OOorNO is a subroutine called OOorNO.

    What we cannot tell is what OOorNO is intended to do...


    You should remove:

    sub DESTROY { } sub AUTOLOAD { }

    from the module and 3_can.t.

    The default DESTROY action is to do nothing, so you don't need an empty subroutine to do it for you.

    The empty AUTOLOAD definition actually does harm since it will prevent the AUTOLOAD of any superclasses being called, and causes the class to accept any method call without error. We can demonstrate this bug with a simple test script (always write a new test when you find a bug, that way you know when you have fixed it):

    use strict; use warnings; use Test::More tests => 3; use Test::Exception; BEGIN {use_ok 'Class::OOorNO'}; my $o = Class::OOorNO->new; isa_ok $o, 'Class::OOorNO'; dies_ok {$o->this_method_does_not_exist} 'unknown method dies';

    produces:

    ok 1 - use Class::OOorNO; ok 2 - The object isa Class::OOorNO + not ok 3 - unknown method dies # Failed test (t/foo.t at line 13) + # Looks like you failed 1 tests of 3.

    Remove the AUTOLOAD definition and you get:

    ok 1 - use Class::OOorNO; ok 2 - The object isa Class::OOorNO ok 3 - unknown method dies ok

    UNIVERSAL::can can take undef as an argument so:

    sub OOorNO { return ( $_[0] ) if defined( $_[0] ) and UNIVERSAL::can( $_[0], 'can' ); }

    can be written as

    sub OOorNO { return $_[0] if UNIVERSAL::can( $_[0], 'can' ); }

    If the result from OOorNO is only used as a boolean indicating whether something is an object or not then this could be written as:

    sub OOorNO { UNIVERSAL::can( $_[0], 'can' ); }

    since the value returned by can will be a subroutine reference, which will be considered true by perl.

    In fact, returning $_[0] could sometimes be interpreted as false if $_[0] refers to an object that has cpan://overload|overloaded] bool - so your method will not work in the general case anyway.

    I'm guessing (because there is no relevant documentation or test :-) that OOorNO is intended to determine whether its first argument is an object or not. If this is what you intend then you can use the blessed function from Scalar::Util which does exactly that.


    Names should not be underestimated. OOorNO does not give many clues as to its intended function. Good naming is a hugely important part of producing easily understandable code. If English is not your first language I understand that this can be hard - but it is still very important and extra time should be spent (consult with an English speaker if you can find one).


    I would write shift (@_) as just shift. The fact that shift uses @_ by default in subroutines is well known.

    Many people also write bless without brackets. Putting this together with the comment on the use of shift this:

    sub new { bless({ }, shift(@_)) }

    becomes

    sub new { bless {}, shift }

    Just do return - rather than return undef or just having undef as your last statement in subroutine. This will return undef in a scalar context and () in list context, which is usually what you want.


    Well - that's that from a quick skim over the code. I hope this isn't too discouraging - it's not meant to be. It will probably be worth your time to spend a bit more time on CPAN exploring the modules that are there already. Download them. Look at the code. See how things are layed out.

    (and well done for persisting after your initial trial by fire :-)

      *clap*clap* :)

      Makeshifts last the longest.

      I'm sorry I have been so rude to wait until now before expressing thanks for this unbelievably comprehensive analysis and all it's informative pointers as well. My only excuse is that I've been too busy to do so, because I was too busy doing what I learned from your fine presentation.

      Hopefully I'll get the chance to post a reply tomorrow addressing in a point-by-point fashoin several of the key issues you discussed, and how they've made a difference in my approach and thinking.

      --
      Tommy Butler, a.k.a. TOMMY
      
         Visit the ooOPps! Code Library
         http://ooopps.sourceforge.net/pub
      
         see if I'm online »http://ooopps.sourceforge.net/online
      
         ICQ uin: 163915821
         Y! msgr: tommy_the_butler
           email: perlmonks@atrixnet.com
      
      

      Well, in my most recent efforts I tried to take into account as many of your comments as possible while working on this next draft. Please have a look once more, and let me know if I'm on the right track, but bear in mind that I couldn't get to everything you discussed.

      As for your comments on the namespace, perhaps you could provide some suggestions.

      Thanks!

      --
      Tommy Butler, a.k.a. TOMMY
      

        It's better than it was, but it has two big problems as far as I am concerned:

        • As I said before I think mixing OO and functional/procedural styles in this way is almost always a bad idea. I cannot think of single instance in many years of coding where something like this would be an appropriate solution.
        • Since you don't use called_as_method things like OOorNO and myargs have so many caveats on their usage as to make them useless in the general case.

        A few other commments after a quick skim.

        Your synopsis just shows how to load the module, rather than how to use it. It doesn't mention the subroutines at all. You should be able to look at the synopsis and get a basic idea of how the module is used.

        Something like is_called_as_method would be a better name for the OOorNO subroutine.

        Why the bizzare, and undocumented, coercion of undef keys/values in coerce_array (which is badly named since it takes a list of values, and returns a hashref)? undef and the empty string are different values and should be treated as such.

        The bulk of this module comprises a lightweight, pure-Perl emulation of the Devel::Caller library's "called_as_method()" routine which is written in C.

        Why? There seems to be no point in re-inventing a wheel that works very well and replacing it with one that... well... doesn't :-).

        You're subroutines fail in many cases. Consider:

        Foo::foo( Foo => 12 ); Foo->foo( Foo => 12 );

        This is really nasty since you can get inconsistant behaviour depending on what modules are loaded at the time. Basically, if you're first key is the same as a module name you're in trouble.

        called_as_method is complex because it needs to be to work.

(jeffa) Re: Seeking Wisdom: proposed fix for a broken CPAN module
by jeffa (Bishop) on Dec 28, 2002 at 17:47 UTC
    I must say that when the problem is fixing code that has been labeled "... a useless module that gives you your arguments without the first one if that's a class or object ...", then maybe you should simply concentrate on providing decent documentation. Right now the documentation for OOorNO is horribly lacking in instruction, examples, and has no selling points whatsoever. You should address (at least indirectly) the following questions:
    • What does it do?
    • How do i use it?
    • Why should i use it?
    I am a CPAN author myself, the first thing i did before i posted my module was some RTFM'ing: Ovid hit the nail on the head in one of his use Perl; journal entries: "If you're going to write a CPAN module, don't skimp on the documentation. If I can't understand it, I probably won't use it." brian d foy's words are golden as well - you would be wise to read them.

    Time to toot my own horn: i am proud of the documentation i wrote for DBIx::XHTML_Table, i invite you to peruse it and learn from it. The first page contain a Synopsis that shows how to quickly use the module. Next is a brief Description that together with the Synopsis answers all three questions that i presented above. After that is a lengthy break down of the API and how to use each method. Also, i created a home page for the module which includes a Tutorial and a Cookbook for doing various, specific tasks with the module.

    Even though nobody really uses my module, at least it's well documented. I am glad i wasted many hours on that. :)

    Hope this helps and welcome to the Monastery. :)

    jeffa

    L-LL-L--L-LL-L--L-LL-L--
    -R--R-RR-R--R-RR-R--R-RR
    B--B--B--B--B--B--B--B--
    H---H---H---H---H---H---
    (the triplet paradiddle with high-hat)
    

      Yes, this helps very much. I've been looking for a good POD example/template, as well as a Cookbook example/template. I've been somewhat impressed with those of CGI::Session. Sherzod is a good friend of mine. What I really am digging for is--

      Is this effort a step in the right direction? It's not all there yet, speaking of the documentation especially, so I appreciated your feedback on exactly what sections I need to be focusing on when I continue working on the POD today, even though the SYNOPSIS was a given. All your points were very helpful.

      Thanks again for your feedback

      Let the devine bless(jeffa)

Re: Seeking Wisdom: proposed fix for a broken CPAN module
by Aristotle (Chancellor) on Dec 28, 2002 at 16:22 UTC
    Ok guys, enough with the (justified) etiquette lectures, now let's help him out. :) Here's his code:

    Makeshifts last the longest.

      It would be blasphemy to not say 'thanks'.
Re: Seeking Wisdom: proposed fix for a broken CPAN module
by adrianh (Chancellor) on Dec 28, 2002 at 11:52 UTC

    It's the norm on perlmonks to include the code (wrapped in <code></code> tags) on the node. You'll get a better response because people won't have to go through the bother of downloading it.

    If it's long wrap it in <readmore></readmore> tags.

Re: Seeking Wisdom: proposed fix for a broken CPAN module
by Juerd (Abbot) on Dec 28, 2002 at 12:34 UTC

    Get the code distro from http://ooopps.sourceforge.net/tmp/Class-OOorNO-0.01.tar.gz

    I second adrianh's reply. I'm a lazy guy, and don't like to download module distributions if I'm not going to use them. If search.cpan.org didn't provide a handy source link, I would never have reviewed Handy::Dandy or even have seen it in the first place. (I haven't actually tried the module. The entire rant is based on what I read.)

    - Yes, I reinvent wheels.
    - Spam: Visit eurotraQ.
    

      Thanks for your reply.

      Well, the POD was also part of what I was seeking wisdom for, and that would be hard to render correctly here. What would you suggest? Should I run it through pod2text and include it as well?

      Thanks.

        Well, the POD was also part of what I was seeking wisdom for, and that would be hard to render correctly here. What would you suggest? Should I run it through pod2text and include it as well?

        No, just include the whole module's source here in <code> tags. We all know POD :)

        - Yes, I reinvent wheels.
        - Spam: Visit eurotraQ.
        

Log In?
Username:
Password:

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

How do I use this?Last hourOther CB clients
Other Users?
Others contemplating the Monastery: (5)
As of 2024-04-26 08:33 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found