Beefy Boxes and Bandwidth Generously Provided by pair Networks
P is for Practical
 
PerlMonks  

How to morph a plain module to OO

by Rudif (Hermit)
on Sep 07, 2002 at 18:04 UTC ( [id://195887]=perlquestion: print w/replies, xml ) Need Help??

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

I have a plain old module that I wrote before I was conversant with Perl OO.
It provides a several subroutines, which I call from many scripts, built up over several years.

At present, I want to morph my module to OO (so that each instance can have a state which is accessible to all subs). However, I want to morph the module in a such way that my old scripts continue to work, and that my new scripts can use the OO syntax and other goodies. I found one way to do this, shown below on a very simple example.

Learned monks, I would like to see your comments on other, perhaps better ways to achieve my goal.

MyModule before the change

#! perl -w use strict; package MyModule; sub isFile { my $file = shift; # a file in local directory expected my $verbose = shift; # optional if (! -f $file) { print STDERR "*** no such file $file in current directory\n"; return 0; } else { print STDERR "... isFile $file\n" if $verbose; return 1; } } # more subs ... 1;
A test script, before the change
#! perl -w use strict; use MyModule; # before change to OO and after MyModule::isFile("MyModuleTest.pl"); MyModule::isFile("NotHere");

MyModule after the change

#! perl -w use strict; package MyModule; sub new { my ($class, %args) = @_; my $self = { class => $class, verbose => $args{verbose} || 0, otheropt => $args{otheropt} || 0, }; bless $self, $class; return $self; } sub isFile { my $self = shift if ref $_[0] && ref $_[0] eq $_[0]->{class}; my $file = shift; # a file in current directory expected if (! -f $file) { print STDERR "*** $file: file not found in current directory\n +"; return 0; } else { print STDERR "... $file: file found in current directory\n" if + $self->{verbose}; return 1; } } # more subs ... 1;

A test script, after the change

#! perl -w use strict; use MyModule; # works before change to OO and after MyModule::isFile("MyModuleTest.pl"); MyModule::isFile("NotHere"); # works after change to OO my $mod = MyModule->new; $mod->isFile("MyModuleTest.pl"); $mod->isFile("NotHere"); $mod = MyModule->new( verbose => 1 ); $mod->isFile("MyModuleTest.pl"); $mod->isFile("NotHere");
rudif

Replies are listed 'Best First'.
Re: How to morph a plain module to OO
by sauoq (Abbot) on Sep 07, 2002 at 19:10 UTC

    My first suggestion is not to do this. If you have a working module and it is already heavily used, just write a new module for the OO stuff and have it use the old module under the covers. It is far too easy to break the original code in subtle ways.

    As an illustration, you already broke the functionality of your original module because your new isFile() doesn't take a verbose argument when it isn't used as a method.

    -sauoq
    "My two cents aren't worth a dime.";
    
      My first suggestion is not to do this. If you have a working module and it is already heavily used, just write a new module for the OO stuff and have it use the old module under the covers.

      I second that suggestion :-)

      My process for this sort of thing is as follows:

      1. Create your new OO module that calls the original module where it needs functionality. Do not touch the implementation of the original module. If you do have to touch the implementation, skip immediately step 3. (You will, of course, be creating a nice comprehensive test suite while you implement the new module ;-)
      2. Stop - unless the new module is inelegant, or you want to retire the original module.
      3. If it doesn't have one already, write a comprehensive test suite for the original module - so you can be sure that you don't break anything during the move.
      4. Do not skip the last step. I really mean it!
      5. Repeat the following until the original module is implemented in terms of the OO module:
        1. Move a chunk of the implementation from the original module to the OO module.
        2. Re-implement the functionality you've just moved in the original module in terms of the OO module.
        3. Run the test suites for both modules. Fix bugs. Repeat until no bugs.
      6. Add some "deprecated" warnings (see perllexwarn) to the original module.
      7. Remove original module if you have to (after an appropriate time of mourning).

      Basically, do it gradually and incrementally. Don't try and have a single module be OO and non-OO at the same time. It's complicated and will cause you more hassle than it will save.

      (and I really mean it about those tests :-)


      This nodes spelling mistakes were...

      s/morning/mourning/ - thanks Arien

      s/depreciated/deprecated/ - thanks to sauoq

Re: How to morph a plain module to OO
by dws (Chancellor) on Sep 07, 2002 at 18:15 UTC
    What you're doing will work, but storing the class name in each instance is overkill.
    sub isFile { my $self = shift if ref $_[0] && ref $_[0] eq $_[0]->{class}; my $file = shift; # a file in current directory expected
    is still going to break if someone passes a reference argument that isn't an instance of the class. ($file will end up holding the errant reference.) You might as well just do
    sub isFile { my $self = shift if ref $_[0]; my $file = shift;
      . . .is still going to break if someone passes a reference argument that isn't an instance of the class.

      At least it will break in the same way that it used to break if someone passed a reference. Unless, of course, that reference isn't a hash reference. Or even if it is a hash reference, it is running with warnings enabled and the referenced hash doesn't have a class key...

      *shiver*

      -sauoq
      "My two cents aren't worth a dime.";
      
Re: How to morph a plain module to OO
by blssu (Pilgrim) on Sep 08, 2002 at 00:06 UTC

    Perl OO is a lot more flexible than most people are used to. Two self-imposed beliefs are tripping you up. First, you can have more than one package in a module. Second, you don't need to bless your object to the package the constructor is defined in.

    Here's a clean version of your example written with two packages -- the original module with function call API and the new "improved" module with the object API. The constructor for the object API is defined in the old module so the actual class name of your new object API is hidden from the user. (Users don't need to change their require statements and can slowly adopt the OO API.)

    ## MyModule.pm first package MyModule; use strict; sub isFile { my($file, $verbose) = @_; print "checking file '$file': " if ($verbose); if (! -f $file) { print "not found\n" if ($verbose); return 0; } else { print "found\n" if ($verbose); return 1; } } sub new { my $class = shift; my %self = @_; return bless \%self, $class . 'OO'; } package MyModuleOO; use strict; sub isFile { my($self, $file, $verbose) = @_; $verbose = $self->{verbose} unless (defined $verbose); MyModule::isFile($file, $verbose); } sub verboseOff { my($self) = @_; $self->{verbose} = 0; } 1; ## t.pl usage test use strict; use MyModule; if (MyModule::isFile('MyModule.pm', 1)) { print "ok\n"; } my $env = MyModule->new(verbose => 1); if ($env->isFile('MyModule.pm')) { print "ok\n"; } $env->verboseOff; if ($env->isFile('MyModule.pm')) { print "ok\n"; }
Re: How to morph a plain module to OO
by Revelation (Deacon) on Sep 07, 2002 at 20:53 UTC
    One thing that is quintisential to all object orriented programming is inheritance. I don't believe your script allows for that (what if module script use()es it, and put @ISA = yourModule). I'm sure there are other ways to preserve inheritance, but the one I've used is that of CGI.pm, which has a self_or_default function. That function creates a blessed object, if there is not one to begin with. A more simple version, which would be fine for you would be:
    use strict; my $Q; sub return_with_object { unless (defined($_[0]) && (ref($_[0]) eq 'ModuleNAME' || UNIVERSAL::isa($_[0],'ModuleNAME' +)) ) { $Q = ModuleNAME->new unless $Q; unshift(@_,$Q); } return wantarray ? @_ : $Q; }
    All the subroutine does is checks that an object is defined, and that it is the object for the class you are using. If there is no object, the script creates one, for use throughout the script, without the user knowing that it exists. It's actually a really cool idea, because an oblivious user can't mess with an object, although it exists, but I'm sure some guru has a reason not to use it. It also preserves inheritance by using Universal::isa (UNIVERSAL::isa ( VAL, TYPE ) ), which returns true if the VAL is the name of a package that inherits from (or is itself) package TYPE.

    Update: And to use the subroutine -
    sub SomeSubWithNothingElseToShift { my $self = return_with_object(@_); } sub SomeSubWithOtherStufftoShift { @_ = return_with_object(@_); my $self = shift; my $barz = shift; }

    Gyan Kapur
    gyan.kapur@rhhllp.com
      By definition you can not have more than one quintessence of
      something(1). I do agree that inheritance is the
      fundamental element or the typical characteristic of OO.

      (1) Apologies to the Humpty Dumpty-ists among us.

      That does not mean that every class should be inheritable.
      A mechanism to prevent the inheritance of classes is a
      good thing. That inheritance was not anticipated during
      the design of a class, or a the usage of a class being
      deprecated are two reasons off the cuff.

      I don't know of such a mechanism for Perl. In Perl
      this might be expensive.

      To the initial query:
      Start another package to avoid disturbing the your existing
      clients.

      I appreciate the return_with_object idea, but
      see it as necessary in certain social situations. Coding
      so as to protect oblivious coders seems like a silent
      endorsement of oblivion. I realize this is not necessarially
      so, nor always a bad thing; but to write to a level
      below your abilities cannot be good in itself.

Re: How to morph a plain module to OO
by Rudif (Hermit) on Sep 08, 2002 at 13:06 UTC
    Learned monks, thank you all for the replies.

    You spoke of Perl programming tricks and techniques and of patterns and strategies (as I hoped you would).

    Let me try to summarize the lessons learned.

    On Perl programming:

    • dws ... storing the class name in each instance is overkill.

      True enough. I could store it in a package variable

          my $thisClass = "MyModule";
      In addition, I could encapsulate this variable by placing it together with the module code inside a block.

    • Revelation ... using Universal::isa (UNIVERSAL::isa ( VAL, TYPE ) ), which returns true if the VAL is the name of a package that inherits from (or is itself) package TYPE.

      Good point. I expected that there would be a way to check the class name better than my clumsy

          if ref $_[0] && ref $_[0] eq $_[0]->{class};
      and I have seen UNIVERSAL::isa mentioned in print before. Now I could write
          if $_[0]->isa($thisClass); 
    • Revelation ... sub return_with_object {...} ... That function creates a blessed object, if there is not one to begin with.

      Looks interesting, but maybe more than what I need right now.

    • blssu ... Perl OO is a lot more flexible than most people are used to. Two self-imposed beliefs are tripping you up. First, you can have more than one package in a module. Second, you don't need to bless your object to the package the constructor is defined in.

      Sure, I knew this, from reading the Camel Book. But now I see how it can apply to my case.

      ... The constructor for the object API is defined in the old module so the actual class name of your new object API is hidden from the user.

      Indeed, my unstated objective was not to have new names for subs promoted methods or for the new OO module.
      Your suggestion seems to meet this goal nicely. It also dovetails with sauoq's and adrianh's.
      BTW, you look like a Monastery novice who is not a Perl novice.

    On strategies for morphing:

    • sauoq ... It is far too easy to break the original code in subtle ways.
      adrianh ... write a new module for the OO stuff and have it use the old module under the covers.

      Fundamentally right. It seemed a bit heavy, until blssu showed how to do it in one compact module.

    • adrianh ... If it doesn't have one already, write a comprehensive test suite for the original module - so you can be sure that you don't break anything during the move.
      (and I really mean it about those tests :-)

      I agree 100% (maybe more). The old module does not have a test suite, but it will have one soon.

    Rudif
Re: How to morph a plain module to OO (use a compatibility layer)
by Flexx (Pilgrim) on Sep 08, 2002 at 23:45 UTC

    A lot of good suggestions here. Some time ago, however, I went the other way round: I wrote some improved, all-new gizmo-packed object-oriented modules, that covered my previous functions. Then I wrote a compatibility layer module that presented a procedural interface for some functions to my old scripts.

    Here's an (untested) quick shot to illustrate what I mean:

    #!/usr/bin/perl -w use strict; package MyClass; sub new { my ($class, %args) = @_; my $self = {verbose => $args{verbose} || 0}; return bless $self, $class; } sub verbose { my $self = shift; $self->{verbose} = shift if @_; return $self->{verbose}; } sub isFile { my $self = shift; my $file = shift; # Did you really want to print the error message # when -f fails even if not in verbose mode? print STDERR "$file ", (-f $file ? 'found' : 'not found') if $self->verbose; return -f _; } package MyModule; sub isFile($;$) { my $file = shift; my $object = MyClass->new(verbose => shift || 0); return $object->isFile; }

    With that approach, of course, switching to the new module could break your old scripts, should you realize that your compatibility layer just isn't. ;) Thus, extensive testing is needed before going live with the new module. I would not reccomend that for large, critical applications. On the other hand, you'd keep the old module around, so you could easily downgrade in case of problems...

    What I like about this approach, is that you don't pack your OO code with old procedural interface stuff. Applications that use only the OO interface with have no overhead at all. On the other hand, of course, your old apps might suffer from the overhead of the disguised object creation and method calls.

    Besides all that, I suggest to study the code of CGI.pm or some of the other CPAN modules that offer both an object and a procedural interface (no I can't think of another right now... (help me!)), if you really want to get serious on this.

    BTW, does any of you brothers and sisters know of some literature/articles on this topic?

    Updated: Just added a comment in the code above, since it functionally diverts from Rudif's original code.

    So long,
    Flexx

      A function-based API can be (almost) automatically generated from methods -- all you need is a customized import sub. A default object is created during import and each package using the module has a private default object. This approach usually works a bit better than Flexx's because (1) it avoids creating lots of temporary objects, and (2) there's a long-lived object around to hold module state, so all the state-setting methods still work.

      I wouldn't recommend the CGI.pm module for studying this technique. CGI.pm is difficult to understand because of its long and glorious history. Take a look at my code below, read the docs on packages and symbolic references, look at Exporter.pm and then you're ready to crack open CGI.pm.

      ## MyClass.pm package MyClass; use strict; my @export = qw(verbose isFile); sub new { my ($class, %args) = @_; my $self = {verbose => $args{verbose} || 0}; return bless $self, $class; } sub verbose { my $self = shift; $self->{verbose} = shift if @_; return $self->{verbose}; } sub isFile { my ($self, $file) = @_; my $found = -f $file; if ($self->verbose) { print STDERR "$file ", ($found ? 'found' : 'not found'), "\n"; } return $found; } sub import { my $module = shift; my $mode = shift || ':methods'; if ($mode eq ':functions') { no strict 'refs'; my ($package) = caller(); my $object = MyClass->new(@_); foreach my $name (@export) { my $sub = $MyClass::{$name}; *{$package.'::'.$name} = sub { &{$sub}($object, @_) }; } } } 1; ## test.pl package verbose_main; use strict; use MyClass (':functions', verbose => 1); sub test { if (isFile('MyClass.pm')) { print "ok\n"; } if (verbose()) { print "verbose mode on\n"; } } package quiet_main; use strict; use MyClass (':functions'); sub test { if (isFile('MyClass.pm')) { print "ok\n"; } verbose(1); if (isFile('NotFound')) { print "ok\n"; } } package oo_main; use strict; use MyClass; my $env = MyClass->new(verbose => 1); sub test { if ($env->isFile('MyClass.pm')) { print "ok\n"; } } package main; use strict; verbose_main::test(); quiet_main::test(); oo_main::test();

        Cheers, ++blssu!

        First off: I like your solution! So, just because I like this discussion, allow me to throw in a few more thoughts.

        (1) it avoids creating lots of temporary objects

        Granted, my quick example forwarded this impression. Yet, it would also be possible to create a "master" object in the BEGIN block of the compatibility module, on which the modules' subroutines operate. (Shomehow though, in the example given, I didn't see very much sense in a class implementation in the first place, but I'm sure this was just an exemplary snipplet to begin with).

        (2) there's a long-lived object around to hold module state, so all the state-setting methods still work.

        If the old module uses package globals like $MyModule::verbose to store state information, I'll be not easy to provide this functionality (for both of our solutions) if you want to have these as properties in the object interface. Besides keeping them as class globals (properties), I could only imagine tackling this problems by tie-ing those variables with method calls (but admittedly, I have never tried this, never needed this).

        Your solution appears to focus on (and succeeds/excel in) creating an elegant class/module hybrid which strength is it's flexibility to provide whichever interface the caller expects. This implies to some extent, a direct (old) function to (new) method translation.

        To clarify: The mindset and propagated essence of my idea is not to create an OO version of the original module, but a set of (probably more specialized, pure OO) classes, that exaust the abilities of the original module and beyond, possibly even implementing things quite differently. With other words: Keep the focus on the improvements you want to archive, and avoid carrying old burdens as much as you can (or at least bundle them together and shift them to something "external").

        Then, in a somewhat independent effort (which could well be carried out by another developer/team), build the compatibility layer you need for your old modules. The whole idea behind this, as I'd anticipate from what Rudif layed out, was to enable more than maintenance -- enhanced development -- of a module that has reached it's limits within the bounds of it's current design pattern, while minimizing maintenance effort for older applications.

        Having to build (and maintain) an interface module which doesn't really implement anything appears to be a bearable burden to me, especially if you can settle on an OO model and interface contract for the new module class(es) beforehand.

        I wouldn't recommend the CGI.pm module for studying this technique. CGI.pm is difficult to understand because of its long and glorious history. Take a look at my code below, read the docs on packages and symbolic references, look at Exporter.pm and then you're ready to crack open CGI.pm.

        To be honest, CGI.pm was the only module having a hybrid interface that came to my mind at once. Probably there are better (simpler) examples, that was why I yelled for help naming more hybrid modules/techniques.

        So long,
        Flexx

Log In?
Username:
Password:

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

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

    No recent polls found