Beefy Boxes and Bandwidth Generously Provided by pair Networks
Clear questions and runnable code
get the best and fastest answer
 
PerlMonks  

Conditional and opt-in breaking change: is this design viable and my use of 'import' OK?

by Anonymous Monk
on Oct 04, 2024 at 16:33 UTC ( [id://11162059]=perlquestion: print w/replies, xml ) Need Help??

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

I want my OO module to be used in either 'classic' or 'modern' mode. Users type either of the 2:

use Foo; use Foo ':modern';

Some methods behave slightly differently depending on the package-scoped lexical boolean flag. When it is not too cumbersome or performance critical, the flag is checked inside a method, perhaps in a few places. Otherwise, methods are redefined, which, sadly, leads to code duplication. Additionally, in modern mode some constants are exported to the user, so they can use them. "Classic" users don't know nor care about these constants. Here is my implementation, is it OK or can be done better?

package Foo; use strict; use warnings; use feature 'say'; use parent 'Exporter'; my $MODE = 0; my %const; BEGIN { %const = ( ABC => 'abc', XYZ => 'xyz', # etc. # longer list ) } use constant \%const; our @EXPORT_OK = keys %const; sub import { my ( $self, $flag ) = @_; if ( defined $flag and $flag eq ':modern' ) { $MODE = 1; Foo-> export_to_level( 1, 'Foo', keys %const ); no warnings 'redefine'; *Foo::m2 = \&Foo::Modern::m2; # etc. # longer list } } sub new { my $class = shift; bless {}, $class } sub m1 { say $MODE ? 'modern 1' : 'classic 1' } sub m2 { say 'classic 2' } package Foo::Modern { sub m2 { say 'modern 2' } }; 1;

And a user does:

use strict; use warnings; use feature 'say'; use lib '.'; use Foo ':modern'; my $obj = Foo->new; $obj->m1; $obj->m2; say ABC;
  • Comment on Conditional and opt-in breaking change: is this design viable and my use of 'import' OK?
  • Select or Download Code

Replies are listed 'Best First'.
Re: Conditional and opt-in breaking change: is this design viable and my use of 'import' OK?
by choroba (Cardinal) on Oct 04, 2024 at 18:38 UTC
    For object oriented modules, your approach is fragile and non standard.

    Usually, when you need to instantiate objects, but the objects could be of several different classes based on what the user needs, you can use the Builder pattern. When building the builder object, you pass it all the arguments common to all the constructors of the different classes it can instantiate, it can then have several builder methods, one for each class, which take the arguments specific for the class's constructor.

    If some of the methods have the same implementation in multiple classes, create an abstract parent class from which all the classes inherit (Foo::Common below), or use roles to implement the methods (not shown in the example below).

    OO classes don't usually export anything. To import constants, use a dedicated class.

    For a small project, this might seem like a Java-level verbosity. For larger projects, one would get mad at handling the different flags and ternaries in method implementations; with the Builder pattern you always know where to look when you need to see the implementation of a particular behaviour - and if it's not in the file, it tells you where to look further (parent or role).

    I've used this at a $job - 2 with tens of constructor parameters, built on Moose. It worked great.

    Also note that this approach addresses my other comment: you can easily instantiate both the classic and modern objects in the same program.

    map{substr$_->[0],$_->[1]||0,1}[\*||{},3],[[]],[ref qr-1,-,-1],[{}],[sub{}^*ARGV,3]

      Thank you very much for detailed answer, I appreciate. Never seen __PACKAGE__ for "true" at the end of module, learning something new each day.

      Constants go to dedicated module and "modern" user is told to "use" it. Gratefully accepted.

      To answer your other comment: yes, the OOP, shared code/behaviour and separate data/properties, I have some vague idea. If "import" redefines the subroutines, it's once and for all. Though I didn't expect such mixed usage scenario, I planned to eventually do at least something about it, like (assuming $MODE undefined initially):

      sub import { my ( $self, $flag ) = @_; if ( defined $flag and $flag eq ':modern' ) { if ( defined $MODE ) { if ( $MODE == 0 ) { warn "Note: Foo is in classic mode\n" } } else { $MODE = 1; Foo-> export_to_level( 1, 'Foo', keys %const ); no warnings 'redefine'; *Foo::m2 = \&Foo::Modern::m2; # etc. # longer list } } else { if ( defined $MODE ) { if ( $MODE == 1 ) { warn "Note: Foo is in modern mode\n" } } else { $MODE = 0 } } }

      If someone does just (or mixes with other uses) "require Foo;" or "use Foo();" it'll be considered an act of direct sabotage ;-) But in the end I'll probably abandon the idea to abuse "import", therefore doesn't matter. I don't understand, however, what's the benefit of "Builder". The "script.pl" is written by the end user, all she wants is simply "use Foo;". If asked instead from this day on to:

      use Foo::Builder; my $builder = 'Foo::Builder'->new; my $object = $builder->build('classic');

      then I'm afraid to be pointed at the door. Or if in less grumpy mood/mode, she'll agree to hit a few more keys to "use Foo::Classic;". And if "modern" user happens nearby, he'll say "Aha, I'll do use Foo::Modern; use Foo::Constants; then!"

      I was hoping to use a single ternary to check a flag in, say, 60 lines method (and there are several such methods) so that 2 of these lines are executed in this or that mode; instead of keeping almost exact duplicates in two packages. I'll think more about it and ask further. Thank you.

Re: Conditional and opt-in breaking change: is this design viable and my use of 'import' OK?
by choroba (Cardinal) on Oct 04, 2024 at 16:49 UTC
    Do you have an idea what happens if one module uses the classic mode, another module uses the modern mode, and then a program tries to use both these modules?

    map{substr$_->[0],$_->[1]||0,1}[\*||{},3],[[]],[ref qr-1,-,-1],[{}],[sub{}^*ARGV,3]
Re: Conditional and opt-in breaking change: is this design viable and my use of 'import' OK?
by parv (Parson) on Oct 04, 2024 at 18:35 UTC

    Personally would have created 2 wholly separate versions. And soon after would have picked one to continue to maintain & develop.

Log In?
Username:
Password:

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

How do I use this?Last hourOther CB clients
Other Users?
Others having an uproarious good time at the Monastery: (4)
As of 2025-01-24 16:20 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?
    Which URL do you most often use to access this site?












    Results (68 votes). Check out past polls.