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

Enlightened Monks!

I have a class with a few attributes, and most of the time, I'm creating a new object of this class with the same, predefined set of values. Let's say that the object looks like this:

package Attack { use Moo; use Types::Standard qw( Str Int ); has type => ( is => 'ro', isa => Str ); has damage => ( is => 'ro', isa => Int ); has speed => ( is => 'ro', isa => Int ); }

While there are times, when I need a custom attack, I use three basic attack objects almost all of the time: light, fast and heavy. I want the code to reflect this by providing an easy way to get each of those three objects. I could do it by providing custom constructor methods:

sub light { my $self = shift; return $self->new( type => 'light', damage => '1', speed => '5', ); } sub fast { my $self = shift; return $self->new( type => 'light', damage => '1', speed => '7', ); } sub heavy { my $self = shift; return $self->new( type => 'heavy', damage => '2', speed => '4', ); }

And it works. But I could also do it by processing the constructor args. By overriding the BUILDARGS method I could create shorthand to call Attack->new() with a single argument. This would look like this:

sub BUILDARGS { my ($self, @args) = @_; if ($args[0] eq 'heavy') { return { type => 'heavy', damage => '2', speed => '4', }; } # same for 'light' and 'fast' of course else { return { @args }; } }

Using the first approach, I would create an object like this:

my $att1 = Attack->heavy;

With the second:

my $att2 = Attack->new('heavy');

Of course, both $att1 and $att2 are identical, I'm just wondering about the style. As you probably figured out from the example, it's nothing serious, it's more of a weekend project, and I'm much more interested in making it work in an elegant way than in making it work at all. The first way seems shorter, and (to me) looks better when called, the second makes it very clear that I'm calling a constructor.

What do you think? What would be your preference, and why?

- Luke

Replies are listed 'Best First'.
Re: Convenient Constructors - a Moo question
by tobyink (Canon) on Dec 06, 2014 at 21:12 UTC

    Personally I prefer the Attack->heavy approach over Attack->new("heavy"). Why? Because when (not "if"!) you mistype it as "haevy", then Perl will automatically generate an error message for you. You don't need to write that logic to check the string is valid; Perl will for you.

    To an extent I agree with hippo's answer, that it's nice for constructors to be called new, but I don't think that warrants throwing out the whole approach. Instead you could just name the method Attack->new_heavy. This is perfectly clear, and similar to things like new_from_file or new_from_xml you see all the time.

      Instead you could just name the method Attack->new_heavy.

      Thank you, this seems like the perfect compromise. Wonderfully simple solution.

      - Luke

Re: Convenient Constructors - a Moo question
by Arunbear (Prior) on Dec 06, 2014 at 21:33 UTC
    Given that you're calling them as class methods, you can make that more clear by writing them as
    sub new_light { my $class = shift; return $class->new( type => 'light', damage => '1', speed => '5', ); } sub new_fast { my $class = shift; return $class->new( type => 'light', damage => '1', speed => '7', ); } sub new_heavy { my $class = shift; return $class->new( type => 'heavy', damage => '2', speed => '4', ); }

      Your idea seems to capture the spirit of the best suggestions in this thread well.

      I was thinking a little more about it and came up with this::

      use constant ATTACK_TYPES => [ { type => 'light', damage => 1, speed => 5, }, { type => 'fast', damage => 1, speed => 7, }, { type => 'heavy', damage => 2, speed => 4, }, ]; foreach my $attack ( @{ATTACK_TYPES()} ) { my $name = "new_$attack->{type}"; { no strict 'refs'; *{$name} = sub { return shift->new(%$attack) }; } }

      Which seems too clever on the one hand, but on the other hand does a nice job of separating configuration data from code. The fact that it mostly eliminates the pitfalls of "Don't Repeat Yourself" is a bonus. But the real advantage is what can come next: The next logical step would be to store the attack types as a JSON file, and then process it like this:

      use IO::All; use JSON 'decode_json'; use constant ATTACK_PARAMS => [ qw/ type damage speed / ]; my $attack_types = decode_json( io('attacks.json')->slurp ); foreach my $attack ( @$attack_types ) { exists $attack->{$_} or die "Malformed attack type: $_\n" for @{ATTACK_PARAMS()}; my $name = "new_$attack->{type}"; { no strict 'refs'; *{$name} = sub { return shift->new(@{$attack}{ATTACK_PARAMS()} +) }; } }

      And now someone who doesn't care to learn a lot about programming could still manipulate the game's parameters without wasting your time.


      Dave

        Something like this has been done, though using ini files.

        Although it's cool that such things can be done, I'm not likely to use it due risk of breakage that it introduces.

Re: Convenient Constructors - a Moo question
by hippo (Archbishop) on Dec 06, 2014 at 17:04 UTC

    My opinion (which is all it could ever be when you are talking about personal policy or style choices like this) is that the second option would be preferable because it adheres to the Principle of Least Astonishment. In this case, the constructor method is called "new" which is pretty much what anyone approaching some unfamiliar code would expect.

    Yes, it is a little more typing. So are comments and you include those in your code, I'm sure. The comments are just extra typing to help make the whole code more maintainable just like this slightly more verbose constructor.

    It's your code, so by all means choose your own path. I would, however, recommend that if you decide to employ the first approach instead that the pod makes it abundantly clear that such methods are constructors in order to avoid any misconceptions.

    So, are you writing a FF-style game or something else?

      Thank you for your reply. I understand that when it comes to style, it's mostly about opinions. But this is exactly what I expect, especially, when they are backed by some reasoning. Using new() is indeed clear and obvious, but this clarity in calling the method is somewhat balanced by the obscurity of BUILDARGS - someone reading the code would have to be familiar with this built-in method, while the additional constructors are self explanatory when you look at their implementation. Of course, one might say that the BUILDARGS method could be explained by one simple line of commentary at the point of its implementation, while the fact, that Attack->heavy is a constructor, will be obscure every time it's called. Still, your opinion puts the odds in favor of the second approach.

      And I don't care about more typing - when I said that the first method is shorter, I was trying to say that it looks clearer when it's read (due to the brevity of 'Attack->heavy'). It could be twice as long to type, if it meant that it would read like plain English. It was an unfortunate choice of words on my side, sorry.

      As to your question, no, I'm not making an FF clone. The game design is already done, and it works moderately well on pen&paper. It's a dungeon crawler, without any RPG elements - there is no character development at all, and no background story about your character.

      It's a series of monster encounters with turn based combat. You choose an attack, the enemy chooses a defense, there are some Games::Dice rolls, and the successful hits are applied. Then you switch sides and choose a defense, predicting the enemy's attack pattern. You have a few attack choices, and a few defense choices, and there is some rock-paper-scissors feel to the mechanics. Dodging works better against heavy attacks (with a chance to stun), parrying may result in a counter attack (but it's poor against heavy attacks).

      There are no more than three Attack / Defense options available to you at a given time (there is more variety for the enemies), and only two d10 rolls are made each turn. If I can make it look nice and simple, I'll post it under CUfP, and maybe someone will add some flavour to it.

      Some examples of the genre:

      I would love to see a Perl implementation of the second one. If I did not have those attack/defense mechanics stuck in my head, this is where I would start - with a Tower Hack clone. Such simple design, so much fun.

      - Luke

Re: Convenient Constructors - a Moo question
by davido (Cardinal) on Dec 06, 2014 at 19:23 UTC

    This seems like an example of where subclasses and polymorphism actually make sense.

    use Attack::Heavy; use Attack::Lite; use Attack::Fast; my @attacks = ( Attack::Heavy->new, Attack::Lite->new, Attack::Fast->new, );

    If you prefer, internally "Attack" could be a role that defines a common interface, and "Attack::Heavy", "Attack::Lite", and "Attack::Fast" could be composed with that role. Or inheritance could be used, whichever is your preference. Actually, inheritance is probably preferable here, because there are probably times where you would just instantiate an Attack object without using the defaults provided by ::Heavy, ::Lite, and ::Fast.

    This is classic Perl-style object polymorphism; As long as an object can 'type', 'damage', and 'speed', it's sufficiently Attack like to be treated like an Attack object.


    Dave

      Thank you for the reply, but I cannot agree with you on this one. Subclassing would be useful if the new classes would add anything to the base class. It seems completely unnecessary here, when the only difference between a Heavy Attack and a Light Attack is the set of attribute values.

      The association between seeing subtypes of anything and thinking about inheritance as a possible solution is a natural one, but it's not always a good thing. It even has it's own section in Ovid's excellent Often Overlooked OO Programming Guidelines. To quote the first couple of sentences in "Don't subclass simply to alter data":

      Subclass when you need a more specific instance of a class, not just to change data. If you do that, you simply want an instance of the object, not a new class. Subclass to alter or add behavior.

      - Luke

        Eh... I have to admit that within a few minutes of making my post I wasn't so convinced either. ;)


        Dave