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

Not strictly a Perl question although the solution will be implemented in Perl and the modules will probably appear on CPAN...

I am writing three, perhaps more, related modules which will have the same basic methods. The three modules will post content to different social media sites. I want to be able to use them something like this:

my $social; if ($network eq 'Facebook') { $social = Bod::Social::Facebook->new( ... ); } if ($network eq 'Twitter') { $social = Bod::Social::Twitter->new( ... ); } if ($network eq 'LinkedIn') { $social = Bod::Social::LinkedIn->new( ... ); } unless ($social) { # Handle invalid network; } # Post text content to whichever network has been selected without car +ing which $social->post("Some test text"); # Post text and image content my $image_handle = $social->upload("images/test.jpg"); $social->post("Test text with image", $image_handle);
There are at least three four options of how to implement this, probably more. I am looking for some advice on which way I should choose.

1 - Three separate module
Simply write three modules with similar names as in the code above. Each module has methods with the same names and similar new method. All social media platforms use OAuth2 so new can be largely the same.

2 - Three modules that all inherit from one class
Have a Bod::Social module that defines the methods. Then have modules as in the code above that inherit from Bod::Social and implement the methods. A bit like what I understand an interface to be in Java1. I don't see any advantage in this option in Perl over option 1 but there are certainly modules on CPAN that do this. Either there is good reason or they are written by people coming from stricter OOP languages.

3 - Have a single method and Service Providers
Have a single module that the code uses. In the new method, specify a Service Provider that is different for each social network. Each service provider implements the platform specific calls needed for interacting with the network. LWP::Authen::OAuth2 is implemented this way and it seems to work reasonably well but, again, I don't understand the advantages and disadvantages of this approach. Like this:

my $social; if ($network eq 'Facebook') { $social = Bod::Social->new( service_provider => 'Facebook', ... , ); } if ($network eq 'Twitter') { $social = Bod::Social->new( service_provider => 'Twitter', ... , ); } if ($network eq 'LinkedIn') { $social = Bod::Social->new( service_provider => 'LinkedIn', ... , ); }

4 - Use a Factory Class
Use a Factory Class as we discussed here -> Factory classes in Perl
As these modules will always run in the same environment this strikes me as overkill.

I don't see there being a need to add new networks very frequently but it is quite possible that others will need adding. Which approach would you take or would you use a different solution I haven't considered? Why would you do it that way?

This more general than just this application. Since writing Business::Stripe::WebCheckout I have decided that it would be useful if there was also Business::PayPal::WebCheckout that behaves exactly the same. Therefore the end user's code only has to call a different constructor and everything else gets called the same for multiple payment gateways. I am sure there will be more requirements for multiple related module.

1 I'm not a Java programmer and only use it when I need to create simple Android apps.

Edit 1: - Added option 4

Edit 2: - Added reference to Business::Stripe::WebCheckout and corrected spelling errors.

Replies are listed 'Best First'.
Re: Designing multiple related modules
by choroba (Cardinal) on May 12, 2021 at 12:24 UTC
    5 - Have a role that all three modules do
    The role says which methods have to be implemented and can contain the common behaviour. If you don't use Moose or Moo, you can still use Role::Tiny.

    The Factory pattern was the first thing that came to my mind. At work, we are using it a lot (with Moose) and it makes the code much simpler and the relations between classes more explicit. In fact, the Bod::Social in example 3 already is kind of a factory class.

    See also design-patterns.

    map{substr$_->[0],$_->[1]||0,1}[\*||{},3],[[]],[ref qr-1,-,-1],[{}],[sub{}^*ARGV,3]
      In fact, the Bod::Social in example 3 already is kind of a factory class

      Yes, it is of sorts.
      The first time I came across that methodology was with LWP::Authen::OAuth2 where it seems to work well. It easily allowed me to create a LWP::Authen::OAuth2::ServiceProvider for LinkedIn and allows others to create modules to connect to other social networks. The only disadvantages I see with this is that all the Service Providers get installed, even if they are not needed for the end user's application. Also, in the case of LWP::Authen::OAuth2, the author has to integrate new Service Providers into the distribution. It is quite easy to create a standalone one, as I've done with LinkedIn so I don't see that as a major problem.

Re: Designing multiple related modules
by bliako (Monsignor) on May 12, 2021 at 13:22 UTC

    I am in favour of (2) creating an "interface" although I would call it "base class" because it will have common functionality implemented in there, centrally and for once. For example a toString() method or export/import_session() which would not be in general needed to be overwritten by subclasses.

    An advantage of this is that 3rd party developers can contribute implementation classes for different social media while the API is centrally controlled. Provided that there is indeed a common API with these and future social media sites (hmm not so sure about that).

    An example for the above is HTTP::Cookies and the large number of subclasses (by different contributors) overwriting its load() and save() methods for various browsers and cookie standards. The key point is: contributions by 3rd party. And the biggest problem is for you to identify the API and make sure it will be generic and can survive in time, respecting the contributions by others.

    PS. I may be missing something but (4) Factory works in conjuction with (2), not as an alternative.

    bw, bliako

      Agree with your PS. A factory is the way you hide your implementation classes (quoth c2: "It allows classes to defer object creation to a separate method") from callers who just use that API to obtain the specific flavour they're wanting with something like Bod::Social->new_for( 'Twitter' ) rather than explicitly hardcoding creating an instance of a specific subtype. End users just know they're getting something that isa Bod::Social and can expect it to provide that same interface specialized for their particular kind of thing. You also don't have to go change callers to have them instead instantiate Bod::Social::NewTwitter n months hence when you create a new subclass which is faster or handles some new wrinkle; change the factory and callers get the improved version without knowing they have to ask for it.

      Prossibly not germane in this particular case but if all of the different "flavors" vary only in how to implement some common behavior you might could use Strategy to implement that instead of having full subclasses for each network/type (although you might still want to use a factory method for instantiating the different strategy implementations).

      Or it could just be too early and ENOCAFFEINE and I'm rambling . . .

      The cake is a lie.
      The cake is a lie.
      The cake is a lie.

        Yes, I agree to your agreement. Do you have any proposal for "who owns the Factory" when Bod owns the "interface"/base-class and 3rd party develop different subclasses to it? Someone has to update the Factory...

      PS. I may be missing something but (4) Factory works in conjuction with (2), not as an alternative

      I my mind, (2) would involve creating an instance of the subclass directly through Bod::Social::Twitter->new whereas (4) would rely on the factory class to create the subclass with Bod::Social::Factory->new(network => 'Twitter')

      I think that (3) and (4) are probably the same thing but with just different terminology...aren't they?

        Well, again I would call the Factory not a class but a package with a "static" method: Bod::Social::Factory::new_for(network => 'Twitter') (borrowing from Fletch's comment above). Perl makes that distinction possible whereas I guess in Java it would technically be a class. OTOH, why not if it suits you?

        That said, and purisms aside, I would expect the constructor of a class to return ... erm ... an object of that same class. In Java, I hope I guess correctly, it is impossible for a constructor to return anything other than the object of that class (there is no return statement as such). So, Bod::Social::Factory->new(network => 'Twitter') returning TwitterClass or XYZClass or ... depending on input params strikes me as a bit weird, or unexpected. Where Bod::Social::Factory::new_for(network => 'Twitter'); (notice the difference in ::new_for) is more intuitive for me. But even TIMTOWTDI is spelled in many ways hehe!

        Now, what would be inside new()/new_for()? I would put in there this:

        use Bod::Social::Twitter; sub new_for { my ($network, $params) = @_; if( $network eq 'Twitter' ){ return Bod::Social::Twitter->new($param +s) } elsif( ... ){ ... } die "don't know network $network" }

        Which also implies a class Bod::Social::Twitter, which can be a subclass of Bod::Social (option (2)) or be completely independent, if you did not find enough common ground to create a social API in Bod::Social

        That was helpful to me:, but thankfully Perl is way less restrictive than Java.

        bw, bliako

Re: Designing multiple related modules
by rje (Deacon) on May 13, 2021 at 20:25 UTC

    "1 - Three separate modules."

    "Simply write three modules with similar names as in the code above. Each module has methods with the same names and similar new method. All social media platforms use OAuth2 so new can be largely the same."

    I did just this in a personal project. I didn't want classes, because the only thing they have in common is the API -- I'd use interfaces but as long as each package supplies the required methods it works fine.

    Common code is either retained in the main program, or else stashed in a utility package ("component") that everyone uses.

    A factory is the nicest way to do this I think. Depending.

A reply falls below the community's threshold of quality. You may see it by logging in.