Beefy Boxes and Bandwidth Generously Provided by pair Networks
go ahead... be a heretic
 
PerlMonks  

Factory classes in Perl

by Bod (Parson)
on Jan 08, 2021 at 22:28 UTC ( [id://11126634]=perlquestion: print w/replies, xml ) Need Help??

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

I've been reading some of the Perl Advent Calendar trying to expand my learning.

On this day there is the problem of Cyber-Santa having to create classes of either Robo-Reindeer or Bio-Reindeer depending on which is available. The module tries to use Robo-Reindeer but if they are not available because their lamp batteries are not charged (or the module just isn't installed) then use Bio-Reindeer instead.

I think I understand what this Reindeer Factory Class does and why it might be useful in other languages. However, in Perl we have CPAN from which we install our modules...this allows us to specify dependencies be be installed when we install our module.

In this Perl Advent Calendar example, why would we create a ReindeerFactory instead of just forcing our module to install Robo-Reindeer if that's the one we prefer?

Replies are listed 'Best First'.
Re: Factory classes in Perl
by choroba (Cardinal) on Jan 08, 2021 at 22:34 UTC
    Making the example a bit more realistic, imagine the two types of Reindeer are in fact a *nix Reindeer and a MSWin Reindeer. You want to build the one that's compatible with the OS, but the final product should work on both the platforms transparently.

    Basically, any time you write a package that instantiates different classes based on something, you should consider the factory pattern.

    map{substr$_->[0],$_->[1]||0,1}[\*||{},3],[[]],[ref qr-1,-,-1],[{}],[sub{}^*ARGV,3]
      Making the example a bit more realistic

      What??? You don't have Robo-Reindeer pulling your sleigh?

      are in fact a *nix Reindeer and a MSWin Reindeer

      Ah yes!
      I totally understand when a Factory Class would be useful now. Not necessarily because of what modules are or are not installed but because of the environment in which the script might find itself with different OS, hardware, drivers or even other software...

      Thanks

Re: Factory classes in Perl (updated)
by haukex (Archbishop) on Jan 08, 2021 at 23:35 UTC
    However, in Perl we have CPAN from which we install our modules...this allows us to specify dependencies be be installed when we install our module. In this Perl Advent Calendar example, why would we create a ReindeerFactory instead of just forcing our module to install Robo-Reindeer if that's the one we prefer?

    IMHO, you're right to question this. Factory classes are often used in strongly typed languages like Java, I myself have for example written a Logger interface with different underlying implementations. The important thing to note is that the user of my library specifically chooses which implementation they want, and the factory class is helpful because the language is compiled and strongly typed; it's not as simple as Perl's "a constructor can return whatever it wants".

    However, the conditional loading of modules depending on something outside of the user's control is something that I nowadays think should be very sparingly used for exactly the reason you hint at: if I've written my code to depend on a certain module or functionality (RoboReindeer), then trying to adapt my code to environments where that module isn't available (BioReindeer-only) is often more work than either backporting RoboReindeer to that environment, or simply declaring that my code requires RoboReindeer, with whatever limitations that brings.

    For me, it comes down to the question of well-defined behavior. If I'm writing a library, I don't like telling the user of my code "you ask for a reindeer and maybe I'll give you a BioReindeer or maybe a RoboReindeer, why do you care?" because more often than not there is a difference between the two that will have my user jumping through hoops to cope with that difference down the line (if ( $reindeer->isa("RoboReindeer") ) { do_this() } elsif ( $reindeer->isa("BioReindeer") ) { do_that() } else { die "unknown species" }).

    On the practical side, let's say I write a library that uses *Reindeer, and when the user installs my library they only have BioReindeer installed, so only that subset of my module's test suite gets run. If they install RoboReindeer later, my test suite may never get executed against the RoboReindeer they installed, which again may lead to problems.

    So for these reasons nowadays I lean very strongly towards simply saying "the users of my library are getting RoboReindeers, period".

    There are of course exceptions, an important one is the OS-dependent implementation of a platform-independent interface, as choroba indicated - one of the big ones in the Perl core is File::Spec, for example. Another one is the automatic loading of XS-based versions of otherwise pure-Perl modules when they are available (as long as the two really behave identically and the XS version is just an optimization). And a last reason that I can think of at the moment may be if you're trying to gradually transition your code away from one dependency onto the other.

    (It's late and I don't have the time to investigate further right now, but I'd be genuinely curious if there are other useful uses for the "recommends" and "suggests" relationships of CPAN::Meta::Spec.)

    Update: Thinking about this a bit more, I guess what I am saying is that my experience simply doesn't line up with the premise of the article, in particular the first word:

    Often we find ourselves writing code that copes with optional dependencies; Code that will use a module to do amazing things if it's installed, but will muddle through with some lesser code path if that dependency is missing or unavailable on the system.

    Instead, I think that most of the time one thinks one is in this situation, there is a different and often more pragmatic solution possible.

      I'd be genuinely curious if there are other useful uses for the "recommends" and "suggests" relationships of CPAN::Meta::Spec.

      You might consider the recommendation or suggestion of optional modules used in the test suite. Often previously I have found that a dist will require various modules for testing which themselves have long dependency chains. This is fine if you want 100% coverage but a lot of the time I (as a user) just want to be able to install the module, test the essential components and be done without downloading lots of modules for the test suite that I might never use again.


      🦛

        None of my modules are large enough to have a massive test suite, but I imagine in a large test suite you're right that it might be possible to make some tests optional. I personally try not to require a ton of modules for testing (except in my author tests), i.e. the same policy as for the modules themselves (e.g. use Moo instead of Moose when possible and so on). Also, though I'm not sure at the moment how the different CPAN clients handle this, I believe that TEST_REQUIRES dependencies are the ones that don't need to be installed into the user's environment.

        Update: In fact, as an example, because I really only need one functionality from Test::Exception Test::Fatal and Test::Warnings each, I now quite simply inline that:

        sub exception (&) { eval { shift->(); 1 } ? undef : ($@ || die) } sub warns (&) { my @w; { local $SIG{__WARN__} = sub { push @w, shift } +; shift->() } @w }
      Thinking about this a bit more, I guess what I am saying is that my experience simply doesn't line up with the premise of the article, in particular the first word:

      I think that is it exactly....I read:

      Often we find ourselves writing code that copes with optional dependencies; Code that will use a module to do amazing things if it's installed

      ...and thought - "why not just install the missing module then?"

      I assumed I must be missing something because the author clearly knows their (Perl) onions but it seems they were using something genuinely useful in a place where it didn't need using at all. Glad I questioned it :)

        > ...and thought - "why not just install the missing module then?"

        because different "missing" modules have different interfaces !

        The factory creates objects/classes with the same interface (which might themselves make use of a specialized CPAN module under the hood ... or not)

        Example: you create something with a complicated dynamic table, with rows and cells.

        Tables with rows and cells are a universal concept, but the details differ a lot.

        The factory could produce output-backends for

        • HTML,
        • TK,
        • Excel,
        • Word,
        • PDF,
        • SVG
        • LaTeX
        • you-name-it,

        but all now with the same interface for geometry, color, font, etc.

        And just supporting the features you really need for the problem at hand.

        The code for the complicated table is just using the factory and doesn't need to care about the output details.

        And if someone needs to target new media - like a fancy JS-library for grids - you'll just add an implementation to the factory.

        I don't know if that's even possible (or how useful the feasable may be), but it's an example.

        Now take a look at all the modules for Tables and ask yourself how you'd write reusable code to use multiple of them.

        UPDATE

        Part of the problem here is that most of the literature for factory classes will be written with JAVA like languages in mind. Perl is far more TIMTOWTDI.

        In Perl I could imagine a factory "package" without any OOP involved.

        Or a factory "function" (aka a generator) to create functions. (generators are actually quite common in functional programming but rarely as complex as packages)

        Cheers Rolf
        (addicted to the Perl Programming Language :)
        Wikisyntax for the Monastery

Re: Factory classes in Perl
by davido (Cardinal) on Jan 08, 2021 at 23:33 UTC

    A more real-world case that I deal with fairly regularly is one of code portability across environments that have differing constraints. Lets consider the idea of a JSON parsing class. I know this is a solved problem, but for the purposes of illustration it may be useful.

    I may have one class of servers where it is acceptable to assume that JSON::XS is available. I may have another class of servers where a recent enough Perl version is available that I can assume JSON::PP is available. So that means 5.14 or newer. And there's yet another class of servers where I cannot make any assumptions other than core Perl, and whatever code I explicitly bundle. In those environments, I have a lot of constraints that prevent me just sucking in whatever I want from CPAN (for example, tens of thousands of servers where a goal is to keep our footprint minimal).

    But additionally, we have these concerns: Primary: We should use the most CPU-efficient JSON parser available. Secondary: We should use the most standardized JSON parser available. Tertiary: We should use a JSON parser that has no dependencies other than what we package and ship with our code. There is a module, JSON::Any, which handles this decision for us, but may not have the same prioritization, plus it is another dependency that I'm not guaranteed to have available in all environments. So here's a home-grown strategy to select the JSON module for us, based on the order that I might prefer for this example:

    #!/usr/bin/env perl use strict; use warnings; use Data::Dumper; { my @JSON_MODULES = qw( JSON::XS JSON JSON::PP JSON::Tiny ); my $json_class; for my $module (@JSON_MODULES) { if (eval "require $module;") { $json_class = $module; last; } } die "No JSON parsing class found. $0 requires one of ", join(', ', @JSON_MODULES), ".\n" unless $json_class; warn "Using JSON module '$json_class'.\n"; $json_class->import(qw(encode_json decode_json)); } my $json = '{"foo":"bar", "biff":["baz","buzz"]}'; print "\nData structure parsed from JSON:\n", Dumper(decode_json($json +));

    If I run that on a system that has JSON::XS installed, I get this output:

    Using JSON module 'JSON::XS'. Data structure parsed from JSON: $VAR1 = { 'foo' => 'bar', 'biff' => [ 'baz', 'buzz' ] };

    If I run it in an environment that has neither JSON::XS nor JSON, but is Perl 5.14 or newer, I'll get this:

    Using JSON module 'JSON::PP'.

    If I run it in an environment that has no non-core modules, and only what I bundle with the code I'm shipping, I better be sure to ship JSON::Tiny, and if I do as I should, the output would be:

    Using JSON module 'JSON::Tiny'.

    ...followed by my data structure dump. The point being that now I've provided flexibility that places a low burden for what modules must be packaged or available. Sure, for many environments I can just pull something from CPAN. For some, I need to have the CPAN distribution built out as an RPM, and for some, keeping the dependencies down to only what ships with the code itself may be best.

    This example used a set of modules that don't require an object oriented interface, but if they did, that would be just as easy; rather than calling $module->import, I could call my $json_parser = $module->new, assuming they all implemented the same OO interface.

    If you are referring specifically to the Factory pattern, the OO version I mentioned is closer to that pattern. But the Factory pattern has uses beyond only dealing with not knowing what module may be available in a given environment. The classic examples usually deal with subclasses that can't be decided at time of writing the code, but that must share a common interface with some base class, pure virtual base class, or interface role. For example, a driver needs to be able to $car->refuel, but whether the car is an instance of a Ford Van class, or a Honda Sedan class is handled by the factory.


    Dave

Re: Factory classes in Perl
by eyepopslikeamosquito (Archbishop) on Jan 12, 2021 at 01:03 UTC

    However, in Perl we have CPAN from which we install our modules...this allows us to specify dependencies be be installed when we install our module.

    Yes, but you need to be careful, see "Dependencies" section at Writing Solid CPAN Modules for some general advice.

      Thank you for a really useful link...that one's now bookmarked!

      It's very timely as well as I have just got a PAUSE account and I'm sure it won't be long before I start to package up a module for CPAN. I will, of course, seek the wisdom of the Monastery at this auspicious occasion.

        " I will, of course, seek the wisdom of the Monastery at this auspicious occasion."

        Yes, definitely feel free to do so. There are several prolific and long-standing CPAN authors that frequent this site who will be happy to go over your code, as well as your entire distribution in detail before your first upload, to provide you with any advice or recommendations that may be beneficial.

Re: Factory classes in Perl
by jszinger (Scribe) on Jan 11, 2021 at 19:24 UTC
    Here’s how I use factories. I have a module which can generate graphics in several different formats (SVG, PDF, etc). I created an OutputRole and a separate class implementing that role for each format ('Output::SVG', etc). Then, when it is time to create output, the OutputFactory takes the name of the output format and returns the appropriate class.

Log In?
Username:
Password:

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

How do I use this?Last hourOther CB clients
Other Users?
Others studying the Monastery: (7)
As of 2024-04-23 20:02 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found