Beefy Boxes and Bandwidth Generously Provided by pair Networks
Perl-Sensitive Sunglasses
 
PerlMonks  

Idea on a Base class API

by dragonchild (Archbishop)
on Jun 13, 2001 at 00:01 UTC ( [id://87918]=perlmeditation: print w/replies, xml ) Need Help??

I've been working in Perl OO for over a year and I've seen a number of different ways to handle objects. I'm not talking about ways to represent an object in memory (hash vs. parallel array vs. whatever else). I'm talking about the way one accesses the internals.

Most of us come from either C++ or Java, when it comes to OO. And, not unsurprisingly, we have brought our old habits with us. For example, having one or two accessors for every single attribute. How many of us have either seen or written code similar to:

sub name { return @_ ? $self->{name} : $self->{name} = $_[0]; }

What about doing a getName() and setName() pair? I'm sure everyone's done something like that before.

In addition, you have to have knowledge of the object's internals within the new() function, and every single class has to have a new() function. There are as many bless's as there are classes. And, almost every single one of them is functionally identical.

If it wasn't for the fact that we are talking about the object's internals, most of us would look at that kind of code, snort in disgust, and say "Why isn't that in a base class?!?" We are so used to having the internals done for us that we forget that this very thing is what OO is best suited for!

I'd like to propose another way of doing things. Some may recognize this from a post satchboost had made about 2 months ago. (I am satchboost reincarnated ... I moved and forgot the password. *blushes*) The important thing about this proposal isn't the implementation, because implementation is the last thing done when designing an object hierarchy. The important thing is the contract between that class and the rest of the world.

The class will do (at least) the following:

  1. define_attributes()
  2. new() - this will call initialize()
  3. initialize()
  4. get()
  5. set()
  6. exists()
Obviously, this is a minimal set, and one of the things I'm hoping to do is to generate discussion on what the contract between the basest class and the rest of the world should entail.

new() does the blessing, then calls initialize() to do any class-specific set-up. The base initialize() is written as such:

sub initialize { my $self = shift; return $self->set(@_); }

The attributes are made through calling define_attributes(). This can be called either from a class or an instance of the class. A define_attributes() call could look like:

Foo->define_attributes( attr1 => 'NUM', attr2 => 'HASH', attr3 => 'STRING', );

Thus, the data-types of the attributes is known by the class. set() can then validate the data-type of the value given (using ref or isa) and enforce data-type integrity. (Obviously, this may or may not be desired. That's one of the things to discuss!)

get() and set() are generic accessors. The idea is to be able to call either one in a scalar or list context, thus allowing multiple get()s or set()s in one call.

Now, many people will say that this is an contract with a lot of overhead, thus it will slow people down. Yes, they're right. However, I ask you - are you really using Perl OO for the execution speed or the ease of development?

So, I put this concept out to the PM community for discussion. I'd be very interested in hearing what the elders, like merlyn and tye, have to say about this idea.

Replies are listed 'Best First'.
Re: Idea on a Base class API
by bikeNomad (Priest) on Jun 13, 2001 at 01:44 UTC
    I'm certainly not a Perl elder, but I've done a lot of OO. No offense meant, but I'd say that what you're doing is not particularly OO. If the external interfaces of your classes include exposure of all your internal data, you've defeated encapsulation. You shouldn't expose your internal data to external clients, generally.

    You're defining things that are primarily data holders (like C structs). Your ideas look like they'd be good for making data holders, if that's what's important to you. This is occasionally useful, but for most people OO is about behavior: your program works because there are a bunch of objects collaborating to do the work. The responsibility for various actions is partitioned between the objects.

    So what is the behavior of an object that just holds data? When we're describing its responsibility, we use terms like "holds", "remembers", "knows", etc. These terms are an indication that we've got something that is more an attribute -- a (possibly composite) data item -- than a real object with its own behavior.

    One important question to answer is: what happens to my interface when I change my implementation?. If your public interface is all behavioral, it doesn't matter how (or if) you encode your object's state (data). But if you expose every field, you've tied your interface to your implementation. This causes brittleness.

    Perhaps part of the problem with this kind of thinking is all the "OO" tutorials that use the same tired "Employee" and "Manager" or "BankAccount" and "Customer" examples. These are more a case of warmed-over database records than objects with real behavior, generally speaking. But then many of the people who wrote those spent too long thinking about databases.

    Anyway, thanks for suggesting this; you should probably look at Class::Struct, which does much the same thing.

      I do apologize for not explaining myself more clearly. I've focused on data typing, primarily because those are the types of objects I've been working with recently. However, the goal is this:
      I have Foo and Bar and Bar inherits from Foo. Since Foo inherits from BaseClass, then all the standard C++/Java OO stuff happens. For instance, if I have the following:
      package Foo; use BaseClass; @ISA=qw(BaseClass); Foo->define_attributes( attrib1 => 'NUM', ); package Bar; use Foo; @ISA=qw(Foo); Bar->define_attributes( attrib2 => 'NUM', );

      Bar would now have two attributes - attrib2 and attrib1. This would be a full implementation of the is-a style hierarchy, something lacking in the native Perl language.

      I'm not looking to create a struct ... I know that's out there and probably done much better than I ever could. However, what I am looking to create is something that would give a solid OO foundation to someone who wants to use it, but doesn't want to re-invent the wheel.

      I don't know ... something that maybe does private, friends, shared, public, etc. I'm not quite sure on how far to take this, or even if this would be well-received by the Perl community. This would be my first modules, so I wanted to start a discussion on PM first, to see what people thought of both the idea in general and my idea in specific.

        I see... Don't worry about how it'd be received. The Perl community is very diverse. Chances are very good that if it's useful for you in the kinds of programming you're doing, it'll be useful to someone else too. If you're looking for encouragement, here's some: go for it!. Either what you build will be better than what's out there, or it'll feel better to you to use, or at least you'll learn a lot doing it. And, with CPAN and Perlmonks, you'll have the advantage of sharing it with others who might find it useful too.

        As far as the technical end, it might be useful to provide a switch of some sort to turn on/off access checking of various kinds. That is, when you're testing or developing, you can check for adherence to the contract (private/public/protected, compatible types, etc.), and be able to switch off the checking later. If you do it right, you can make it run faster without checking.

        To check for violation of visibility, you could use caller() to see where you're being called from.

        Good luck!

Re: Idea on a Base class API
by John M. Dlugosz (Monsignor) on Jun 13, 2001 at 00:48 UTC
    There are cases where I want strong checking on members. Don't set a value unless there is such an attribute defined, that is. Individual get/set functions do this naturally. A generic get/set would naturally need a similar check to be considered the same thing. So to set a newly created attribute, first officially add that attribute, then set it without error.
      That's why you would have to do a Foo->define_attributes() first.

      I should've given an example.

      package Foo; use BaseClass; our @ISA = qw(BaseClass); Foo->define_attributes( name => 'STRING', age => 'NUM', weight => 'NUM', ); sub initialize { my $self = shift; $self->SUPER::initialize(@_); my ($name, $age) = $self->get('name', 'age'); print "My name is $name and I am $age years old!\n"; return 1; } 1; _____ package Bar; use Foo; my $person = Foo->new(name => 'Child1', age => 5); $person->set(weight => 40);
        I think the question is begging for the ability to set more complex data typing that just the 7 that perl has to offer. For example, if I only want data items of a certain class, or that derive from a given class. Or if I only want to include positive numbers, instead of negatives.

        In addition, you need to consider read-only, write-only, and completely private variables. While the latter can be hidden elsewhere, you'd need to have a way to define this.

        Both are possible to include in the setup you have. The read-write-edness of the variable can be done by using arrays as the values for the attributes hash, using the second element of the array as "r", "rw", "w", or undef/0 for private variables.

        The former option, I would suggest that the 'type' can be several things. If a scalar, it can either be the 7 defined perl types, or if not one of those, then a possible class name (such that the argument can be checked by an ISA relationship. If an array, then the arguement can be any one of the types or ISAs in that array. Finally, if the passed argument is a coderef, then when setting, the code should be checked to see if what is passed matches the code, else you lead to failure.


        Dr. Michael K. Neylon - mneylon-pm@masemware.com || "You've left the lens cap of your mind on again, Pinky" - The Brain
Re: Idea on a Base class API
by frag (Hermit) on Jun 13, 2001 at 03:47 UTC

    This definitely seems like a worthwhile WTDI. However, I think I'd rather see the following syntax instead of your define_attributes:

    package Bar; use BaseClass { name => '$', values => '@', anarrayref => '*@', };

    Copy from base the ability to automatically add BaseClass.pm to @Bar::ISA. Copy from Class::Struct the syntax of specifying elements. (Although, personally I'd prefer a slash instead of an asterisk to represent refs and the ability to use '[ ]' and '{}' -- it's more Perlish that way.)

    This way, you've combined the BaseClass, @ISA, and define_attributes statements into one handy statement that's terse but clear.

    Then, to handle inheritance, you could use something like this:

    use BaseClass qw(Foo Fub) { name => '$', };
    And now the object would have the attributes from Foo and Fub, as well as the ones specified in the anonymous hash.

    -- Frag.

      That's a very different approach to how I was thinking about it. You're saying that BaseClass should control all of the inheritance, vs. each class doing the inheritance.

      I'm not looking for technical solutions right now, though. I appreciate them, and I know that's how most people (including me) tend to think, which is why I want to shy away from them for now. I want to create a contract. Something that determines how someone will use BaseClass, regardless of how it's implemented. (I already have two implementations for objects, and I know there have to be more than that!) Maybe, something like this:

      BaseClass will do the following:

      1. Support a define_attributes() function. This will have the following attributes:
        • It will set the attributes that exist in the current class.
        • It will look in that class's @ISA to find all parent classes (if any) and include those classes's attributes.
        • It will be call-able from the package level, in the form Foo->define_attributes(). This will define attributes for the class.
        • It will call-able from the object level, in the form $self->define_attributes(). These will define attributes for the class.
        • define_attributes() will take a hash of hashes, in the form { attribute => { Type => $val1, Default => $val2 }.
        • $val1 will be a listref containing which types are legal for this attribute.
          • NUMBER will require that the value have only digits within it. ref will return undef.
          • SCALAR will allow any alphanumeric scalar. No references. ref will return undef.
          • HASHREF will allow any reference to a hash as determined by ref.
          • LISTREF will allow any reference to a list as determined by ref.
          • SCALARREF will allow any reference to a scalar, as determined by ref.
          • REFERENCE will allow any reference, as determined by ref.
          • OBJECT will allow any reference to a class, as determined by ref.
          • Foo::Bar will allow any reference to the class Foo::Bar, as determined by ref.
          • Foo::* will allow any reference to a class matching that string, as determined by ref.
        • $val2 will be a value of a legal type for this attribute, as checked by $val1, and will be the value copied into the attribute whenever a new object is instantiated.
      2. Support a new() constructor.
        • new() will be call-able from the package level, in the form Foo->new($val1).
        • new() will return a bless'ed reference to an object of class Foo.
        • $val1, if it exists, will be a hash (or hashref), where the keys are pre-defined attribute names and the values are the values for those keys.
        • new() will call a function called initialize() as its last action prior to returning the blessed reference, passing it $val1.
      3. Support an initialize function. This is meant to be overloaded, should there be class-specific initializing actions. Naming attributes and setting default values should be done using define_attributes().
        • The BaseClass initialize() function will solely set the values of all attributes passed into the call to new().
        • Every overload of initialize() should call $self->SUPER::initialize(@_), to make sure that the parent class(es) do their initial setup work, if any.
      4. Support a clone() function. This will be called in the form $self->clone.
        • clone() will return a blessed reference to the class of $self.
        • All values in the original will be copied to the given attribute in the copy.
        • If a value is a reference, then the values referred to will be copied, not the reference. This rule will be recursive, so as to deal with lists of hashes of objrefs, etc.
      5. Support a number of functions that will act as accessors and/or maintenance functions. All of these functions will take a list(ref) or hash(ref), as necessary, and return a scalar or list, depending on calling context. These functions will be called in the form $self->func($val1);
        • exists() will take a list(ref) of attribute names and return the type-list of an attribute, if it exists within the class, or undef.
        • get() will take a list(ref) of attribute names and return the value stored in that attribute, if it exists within the class, or undef.
        • set() will take a hash(ref) of attribute names and values. It will validate that the given value matches the allowable type(s) for that attribute and, if it does, set the attribute to the new value. It will return 1 if that attribute exists and the value was allowable and 0 otherwise.

      Update:Added clone() after reading Damian Conway's book. (Forgot about copying!)

        I'm going to have to read this more carefully later, but I just want to clarify what I was suggesting.
        • First, use BaseClass qw(Foo) would be only one way to handle inheritance. BaseClass would use all of the items in @ISA at that point to determine attributes (as you suggest) but it would also be smart enough to look at that "Foo" in its @_. In other words, your mechanism for dealing with inheritance would still be there; my suggestion would simply provide another way to do it, one that's imho a lot simpler. But my approach wouldn't be mandatory, but optional.
        • Similarly, since any given module is going to have only one call to define_attributes (is that correct?), it makes sense to me that it be part of the use call. If an anon. hash is not on the use BaseClass line, BaseClass would defer setting up attributes, leaving it to a define_atttributes call later in the module. There'll be more than one way to do it.

        I think I'm not questioning the underlying ideas of your approach, but rather, I'm suggesting a simplified user interface towards invoking it. Or if not simplified, then in keeping with currently existing Perl modules that already get a lot of use and that imho have good interfaces.

        Oh, and just I gotta say: I really hate the idea of having to type "SCALAR" et al. Perl already has the sigil set - why duplicate them? (Except in a 'use English' way, but again, that should be an optional WTDI.)

        -- Frag.

Re: Idea on a Base class API
by stephen (Priest) on Jun 13, 2001 at 20:44 UTC

    First, I just want to say that I really enjoy this kind of post, and ++ dragonchild for doing this.

    That said, I'm not sure I understand what the advantage is for a single get(), set() etc. method. If you're objecting to the additional work of defining all of the individual accessor/mutator methods, then there are implementation-based ways to eliminate this, like Class::Accessor, Class::Struct, and Class::MethodMaker.

    I agree that it would be nice to have a facility for checking data types in members. Class::MethodMaker offers this for class types.

    I'm a bit worried about the define_attributes() method. What is the expected behavior when it's called on a class where there are already instances out there? Does it suddenly alter the class behind the scenes? I think that could get pretty confusing.

    I'd like to see a contract like this. However, my personal preference would be toward labeled get_foo() and set_foo() methods, since they're clearer to my eye. Plus, having a separate method for each member means that I can enforce any kind of consistency check I'd like without tacking on conditionals. If "foo" should be a multiple of pi, for example, and I want to have a method that tests this on sets, I don't want to have to override set() and insert an if ($name eq 'foo') { ... } statement, since avoiding confusing conditionals is one of the things that OO is supposed to help you avoid.

    Anyway, just my thoughts on the matter.

    stephen

      I agree that there are a number of ways by which the definition of individual accessors can be defined. However, I'd like to give a snippet to demonstrate the english-ness of this method.

      my @attribute_names = ('attr1', 'attr2', 'attr3'); foreach my $attribute (@attribute_names) { my $value = $self->get($attribute); print "$attribute => $value\n"; }

      This is as opposed to how it would have to be using get_foo() or set_foo(). That would look something like:

      my @attribute_names = ('attr1', 'attr2', 'attr3'); foreach my $attribute (@attribute_names) { my $function_name = "get_$attribute"; my $value = $self->$function_name; print "$attribute => $value\n"; }

      I ask you ... which is clearer? Of course, there's an even clearer way to do that exact same thing:

      my @attr_names = ('attr1', 'attr2', 'attr3'); my @values = $self->get(@attr_names); print "$attr_names[$_] => $values[$_]\n" for (0 .. $#attr_names);

      Personally, I think that Example 3 is the cleanest of all.

      Now, the biggest problem I have with Example 2 (and its cousin, $self->$attribute;) is that you're using a string as a function name. Can you guarantee that this function exists? Maybe, but it's a lot of work, and you restrict your options. For code that will always handle exceptions gracefully, you want to have your function names hard-coded. That way, you will always be executing something.

      A nifty benefit, too, is that you're expanding your options of viable code, as you can see from Example 3.

      The expected behavior of define_attributes() when called on a class with instances out there already is ... well ... not determined yet. Certain implementations of objects mandate that the attributes will get added immediately, and others mandate that it has to be done by hand. I, personally, would say that if you call define_attributes again, that you will add new attributes to all instances, present and future, and that's just the way it has to be.

      A more important problem is what happens when you call define_attributes() on the same attribute. Or, what happens when Foo overrides the allowable types on attrib1 inherited from Bar. These are the decisions that have to be put into the contract, and that's why I'm putting the draft of the contract out there for everyone to read and comment on. :)

      Now, to your objection about needing to add extra conditionals when dealing with set() ... that's a good objection, and one that I hadn't really thought of. Three possible answers immediately come to mind:

      1. Do something like $self->set(foo => $val) unless $foo % PI; ... put the conditional outside the call to set().
      2. Add something to the allowed type, something like MULTIPLE(x) where x is something you've supplied. So, you could have:
        FooClass->define_attributes( foo => { Type => 'MULTIPLE(x)', Default => 0 }, );
        And, no, I'm not quite sure how that would be implemented. But, that's not the point in this discussion. The idea is to figure out what is desired.
      3. Create a set_foo() method that will encapsulate Option 1.
      Do I know which is the best answer? Not yet. If I were to implement something right now, I'd use Option 3. (In fact, I have used Option 3, in a slightly different context.)
C++ is a step backward
by tedv (Pilgrim) on Jun 14, 2001 at 21:09 UTC
    I worry that you're combining the worst aspects of C/C++ (convoluted syntax) and Perl (flexability that gets you into trouble when you use the wrong option). One of the cornerstone ideas of Perl is that new tools are optional, and hence can only add to understanding. It seems like there's just an awful lot of overhead, involving a lot of syntax, to get the work done. The API doesn't mesh with Perl's shallow learning curve.

    For example, defining data type values for each attribute painfully reminds me of C data types. I'd rather have an API that did the work for me-- the API isn't serving the programmer if the programmer does all their work in the function arguments. For example, you could figure out most data types via the get() and set() functions. If you want to set an object "strict", the object could warn (or prohibit) when an old reference value was changed to a scalar, for example.

    I think you need to plan this from a totally different mindset. Don't start from, "This is what C++ says classes are-- how do I implement that?" Start from, "What's the easiest, cleanest way for the programmer to implement classes, and what API serves that purpose best?"

    -Ted
      Ok ... I probably got a little crazy to start with, trying to add strict typing right off the bat. So, let's start from the beginning.

      An class needs to be able to do the following things:

      • Define which attributes exist within any instance of the class. (This also needs to be able to pull in any attributes defined in any parent classes.)
      • Create an instance of the class
        • Values come explicitly, in attribute-value pairs
        • Values come implicitly, from an object reference of the same class (or some parent class?)
      • check to see if this instance of this class is identical to another instance of this class (or some parent class?)
      • check to see if an attribute exists or not
      • set the value of an attribute
      • get the value of an attribute
      Did I miss anything, from the highest level? Did I add something that shouldn't be there?
        Why do you want an object interface in the first place? How will it serve you? Design your interface based on, "What do I need?", not "What can this do?".

        -Ted
Re: Idea on a Base class API
by princepawn (Parson) on Jun 14, 2001 at 21:37 UTC
    M. SImon Cavaletto of Evolution Systems in NYC has developed an extended version of Class::MethodMaker. He has posted RFCs on comp.lang.perl.modules. It seems to cover most of what you are trying to do.
      Ok. I'll have to read up on that. (/me makes a notation on the pad with the growing list of things to read ... at some point.)

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlmeditation [id://87918]
Approved by root
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others making s'mores by the fire in the courtyard of the Monastery: (6)
As of 2024-04-18 10:50 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found