http://qs1969.pair.com?node_id=11126640


in reply to Factory classes in Perl

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.

Replies are listed 'Best First'.
Re^2: Factory classes in Perl (updated)
by hippo (Bishop) on Jan 09, 2021 at 11:46 UTC
    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 }

        I have some huge test suites that have a number of needed modules that dwarfs that of the software itself.

        I mean the software installs in seconds, but if I force-required all of the modules needed to test the entire application at the level I need it tested before release, the install can take over 30 minutes (or longer).

        What I do is make the tests optional, if the needed modules for testing aren't installed. If a test has all needed modules, they run. I can have many of these scenarios in a single test file sometimes.

        I ensure that ALL tests are run on several versions of Perl locally (with pre-installed test modules), and also put into my CI configuration the cpanm install commands for all of the test modules, so that they run on various Perls on several OSs there as well.

        The Makefile.PL that is uploaded to the CPAN contains only the distributions that the software needs to run, plus any basic ones that the build (ie. test) phase needs. By running all tests locally as well as CI, I can be quite reasonably sure that the software will work for my users too.

        The only time I won't bypass an install of a large module or collection of modules used for testing, is if the part of code that's being tested is in the critical path, that is it is a critical piece of functionality that if tests fail, the software may be rendered inoperable, or worse. I need these tests to pass on each client device, not just on a selection of Perls and OSs upstream somewhere.

Re^2: Factory classes in Perl (updated)
by Bod (Parson) on Jan 11, 2021 at 23:00 UTC
    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

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

        I think there is a difference: the advent calendar article is talking about "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". In the example you name, typically the author of the module providing the common "Table" interface will provide at least one (reference) implementation as part of their distribution. All the other backends from other distributions are additional to the existing functionality - I'd say they are "plugins" instead of "fallbacks".

        Just to clarify: I wasn't advocating against modularity, writing abstract interfaces, or "factory" classes that produce one of the implementations of an interface. What I was saying was that when faced with the situation "the dependency I want to use isn't available everywhere", it may be more worth it trying to figure out either "can I live without this dependency" (example) or "I'll require this dependency, and figure out how to get it into the environments where it doesn't work" (similar in spirit to Yes, even you can use CPAN) instead of this "maybe you'll get the full functionality of my code, maybe you won't" situation.

        I had this dilemma with my module IPC::Run3::Shell. Though the initial release was in Aug 2014, if I go into my unpublished dev repository, I actually started working on it over a year earlier, and it initially had three backends, IPC::Run3, IPC::System::Simple, and system. After fighting with exactly the situation that the other two backends were "lesser" for several months, causing me to jump through all sorts of hoops and having very complicated documentation due to the limitations of the other backends, I realized that if I wanted to give the users of my module a solid, reliable experience, then I'd have to depend on the module that provides all the features I need - of course it helps that IPC::Run3's testers matrix is very good.

        I also learned the point about optional dependencies possibly not being tested through the experience with my module Shell::Tools, which actually does have five "recommends" dependencies for use in Shell::Tools::Extra. If I were re-releasing that today, I'd turn it into two distributions: Shell::Tools with its core-only dependencies, and Shell::Tools::Extra which would require all of its CPAN dependencies.

        As I said above, there are of course exceptions to all this, and with "you're right to question this" I meant that one should give this some serious thought before going down that road.

        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.

        I agree. Although I'm far from an expert on the theory, I think Dependency inversion principle is probably an interesting and relevant read.