Beefy Boxes and Bandwidth Generously Provided by pair Networks
XP is just a number
 
PerlMonks  

A different OO approach

by fruiture (Curate)
on Dec 13, 2002 at 19:55 UTC ( [id://219728]=perlmeditation: print w/replies, xml ) Need Help??

The background

This meditation directly follows the discussion around the OOP tutorial, especially the thread after this node.

I must admit i hadn't thought of a solution to that problem successfully before, although i've had this problem whenever i really wanted to subclass a module i didn't write. It wasn't possible, because whatever new attribute i introduced, i could not be sure it wouldn't break something with the next version of the Superclass...

Abigail-II's solution does not only seem to solve that, it does solve the problem. As pointed out in posts in the above mentioned discussion, this technique makes it neccessary to use accessor/mutator methods, which is what we want when we use OO.

So I've come to the conclusion that these "Inside Out" objects (as Abigail calls them) are the better approach when you want subclassable OO. There's no need to rewrite classes that use the popular way, most classes aren't subclassed anyway, but if you wanted to subclass GD::Image you'd have a problem: how to call your member variables?

The next step

There are some weaknesses that might seriously stop you from thinking the solution is useful. But wait: They're there because it's not the popular way.

My contribution is an RFC: A superclass for all classes using that new methodology to solve at least four of these problems: the DESTROY method, the lexically limited data-variables, the serialization issue and finally the methodmaker. Probably the stringification problem can be solved generally in that class.

I have already posted an idea in the disucussion, but this already has developed and here comes the result:

package OO; use strict; use warnings; use Carp qw/croak/;

This is the first question: the name, how should that superclass be called? 'OO' is just a thought and probably no too good idea.

#-------------------------------------------------------------------- # The registry itself: storing all the data of your objects #-------------------------------------------------------------------- my %Object = (); # Object => Class => Attribute

This is not a question, but the idea: Store all the information right here in that lexical. A question is the structure: Class,Field,Object, which would more directly inherit Abigail's idea or Object,Class,Field, which has the advantage that the destruction can be done easily without tricks or search.

#-------------------------------------------------------------------- # inheritable constructor with generalized behaviour #-------------------------------------------------------------------- sub new { my $class = shift; my $self = bless [caller], $class; # is that neccessary if you can override new()? if( $self->can( 'initialize' ) ){ $self->initialize( @_ ); } return $self; }

Should there be something like this constructor at all? You see, it is quite unneccessary, but what else should a constructor do and why should every class write something similar?

#-------------------------------------------------------------------- # Use these two methods to get and set members of your # object and they will do encapsulation for you # # BE CONSISTENT our your OO will BREAK! # #-------------------------------------------------------------------- sub oo_get { my $obj = shift; my $field = shift; # member hash is based on caller class # and may be overwritten by third argument to get() my $class = @_ ? shift : caller; $Object{ $obj }{ $class }{ $field } } sub oo_set { my $obj = shift; my $field = shift; my $value = shift; my $class = @_ ? shift : caller; $Object{ $obj }{ $class }{ $field } = $value; }

These two are the crucial point of the whole thing: They get/store attribute data depending on the caller. What? Yes. These two are not to be used anywhere except in the package that defines a class. This way each class has it's own data slot as the idea is.

The names of these two must be carefully chosen, but probably 'oo_' can be avoided by classes easily.

#-------------------------------------------------------------------- # most important: DESTROY #-------------------------------------------------------------------- sub DESTROY { my $obj = shift; delete $Object{ $obj } #that's why the structure of the regist +ry was chosen like that }

Jep, that's all about the possible memory leaks. Object deletion causes object data deletion. by having this in the superclass, you needn't rewrite it in every class.

#-------------------------------------------------------------------- # create_accessor class method to create simple accessor/mutator + methods #-------------------------------------------------------------------- sub oo_create_accessor { my $pkg = shift; no strict 'refs'; # we're messing around with the symbol table foreach my $mem ( @_ ){ my $symbol = $pkg . '::' . $mem; if( defined *{ $symbol } ){ croak "Attempt to redefine $symbol via create_ +accessor"; } else { *{ $symbol } = sub { my $self = shift; if( @_ ){ $self->oo_set( $mem , $_[0] , +$pkg ); } else { $self->oo_get( $mem , $pkg ); } }; } } }

This is clear and is subject to be made more advanced but shows that it's easy to create accessor/mutator methods ala MethodMaker.

#-------------------------------------------------------------------- # debugging function/method (as you like it) #-------------------------------------------------------------------- sub oo_registry { return \%Object } 1;

further steps

Now one can write a serialization/deserialization method in that class. The stringification thing can be helped out using overload::StrVal as proposed by adrianh.

I hope this RFC is an inspiration to some as Abigail's post was inspiration to me.

--
http://fruiture.de

Replies are listed 'Best First'.
Re: A different OO approach
by shotgunefx (Parson) on Dec 14, 2002 at 03:31 UTC
    I liked Abigail's take as well. The DESTROY problem is easy to get around.
    my (%Attr1, %Attr2,%Attr3); my @USED = (\%Attr1, \%Attr2, \%Attr3); sub DESTROY { my $self = shift; delete $_->{$self} foreach @USED; }
    The serialization is a larger problem. I don't like the idea of everyone having to agree on a standard way of doing things to get the benefits of prior work. (This is Perl after all.) There are hooks in Data::Dumper and Storable, for serialization, it seems to me, that it might make better sense to provide a serialization hook instead. (Though getting the modules to recognize this from subclasses is another issue. Perhaps a patch to Dumper that looks for a Freezer method automatically if the class isa Dumper. (Same for Storable). Seems like it would be easier to patch the few main serialization modules then every other module on CPAN that you may want to inherit from.

    -Lee

    "To be civilized is to deny one's nature."
      my @ATTR = \my (%Attr1, %Attr2, %Attr3);

      Makeshifts last the longest.

        ++ That is very nice. I didn't realise that you could use my and \() like that. Learn something new every day!

        The only pain is that you have to remember to do:

        my @A = \my %Foo;

        not

        my @A = \my (%Foo);

        ... which could lead to some nasty buggettes if you're not careful.

        I thought I remembered seeing it done something like that. I had tried my \(%Attr1,%Attr2,%Attr3). Thanks for the tip.

        -Lee

        "To be civilized is to deny one's nature."
      The DESTROY problem is easy to get around...

      That just moves the problem from keeping DESTROY in sync, with keeping USED in sync :-)

      The serialization is a larger problem. I don't like the idea of everyone having to agree on a standard way of doing things to get the benefits of prior work.

      I'm not sure I follow this argument?

      If you have a complex serialisation problem you'll be writing custom freeze/thaw methods anyway so it's not an issue.

      If you're not - and basically want something that has all the attributes in it for you to Dump/freeze/thaw, then a base class would seem the best solution to put a generator for this sort of thing?

      ... or am I missing the point again... it is late...

        I don't see keeping @USED in sync a problem. It's a fairly simple thing to do, and if you use a function generator to make your accessors, then you really can't forget.

        I'm not saying a base class doesn't have any merit, but what about everthing that already exists on CPAN? How would this play with them? Ideally, I shouldn't have to know the internals of a base class if I want to inherit from it.

        Most freezing and thawing is done with a handful of modules, patching those modules to look for a certain method before serializing would let someone inherit from these "inside out" modules in a fairly transparent fashion, regardless of whether the inheriting class used a common "base" module or not. In fact the descendant wouldn't have to know or care how the object was implemented. To it, it would work pretty much like any other hash based object. The Serialization routine could just return the appropriate data blessed into a helper class. The helper class when accessed through a method, could turn itself into a proper "inside out" object.

        The other thought would be using a HoHoH as you do, would probably be at least 30% slower than Abigail-IIs method of just using lexical hashes. For many things, speed is not a priority, but for objects that will be called often (parsers, tokenizers, etc) this could be a big hit. So my main argument would be that if it would interfere with using other modules in a transparent fashion, I think it would be of limited value in the general case.

        -Lee

        "To be civilized is to deny one's nature."
Re: A different OO approach
by adrianh (Chancellor) on Dec 15, 2002 at 01:07 UTC

    Let's look at the main problems with hash based objects that Abigail-II's inside-out objects solve:

    1. A sub-class can accidentally overwrite a super-class's attribute, by using the same hash key, without causing a compile time error.
    2. All attributes have global scope.
    3. Typing $self->{fo} when you meant to type $self->{foo} won't give you a compile time error.
    4. You can't inherit from classes implemented with non-hash references.

    Unfortunately, you're class only solves the first one :-)

    • oo_get/set allows you to get the attribute from any class through your optional third argument.
    • Since attributes are still hash keys you can mis-spell an attribute name without any compile time errors.
    • Having your own new() method makes mixing this into an existing class hard.

    You could get much the same affect as your class by the traditional hash based approach, but adding another layer of indirection based on the class, e.g.:

    { package Foo; sub new { bless {}, shift; }; sub foo1 { my $self = shift; @_ ? $self->{+__PACKAGE__}->{foo} = shift : $self->{+__PACKAGE__}->{foo} }; }; { package FooBar; use base qw(Foo); sub foo2 { my $self = shift; @_ ? $self->{+__PACKAGE__}->{foo} = shift : $self->{+__PACKAGE__}->{foo} }; }; my $o = FooBar->new; $o->foo1("the foo from the base class"); $o->foo2("the foo from the sub-class"); print $o->foo1, "\n"; print $o->foo2, "\n"; # produces the foo from the base class the foo from the sub-class

    ... but you don't have to worry about the DESTROY method :-)

    You also have the problems with overloading and re-blessing objects, but that's solvable like this.

    It might be worth having another read of the first half of this node where Abigail-II talks, somewhat forcefully, about the strict/scoping issues :-)

    I've got a base class that does some of the stuff that I think you're after... I'll spend a little time and clean it up enough to make public :-)

      I can't agree:

      All attributes have global scope.

      What does that mean? What global scope shall these attributes have?

      Typing $self->{fo} when you meant to type $self->{foo} won't give you a compile time error.

      You can't solve this problem as long as you use a hash at all. But the danger can be limited to inside the class by enforcing accessor methods. Your Code doesn't help this either.

      You can't inherit from classes implemented with non-hash references.

      You can. You cannot implement non-hash objects using the OO module, but you can inherit from any object, because _your_ class' data is kept somehwere else. Anyways: the new() was in question. It's probably no good idea, although you're always allowed to override it.

      oo_get/set allows you to get the attribute from any class through your optional third argument.

      Just as Perl gives you symbolic references. This is for example needed when you create accessor methods automatically.

      The idea of putting the data back into the object seperated by classes is great. It makes the object classically serializable and gets us rid of complicated DESTROY methods because it's all in one place. "Ay, there's the rob", it is then impossible to inherit from classes that don't use that new scheme.

      If you can accept it that you only need the new scheme for new classes and not for subclassing others, it's quite easy to put that abstraction into a superclass as well (also avoiding use of __PACKAGE__):

      package OO; use strict; use warnings; # A constructor sub oo_create { my $class = shift; bless {}, $class; } # Set Data reference for current class sub oo_set { my $obj = shift; my $val = shift; my $class = @_ ? shift : caller; $obj->{ $class } = $val; } # Get Data reference for current class sub oo_get { my $obj = shift; my $class = @_ ? shift : caller; $obj->{ $class }; } 1;

      In use that's:

      package Foo; use base 'OO'; sub new { my $class = shift; ( my $self = $class->oo_create )->oo_set( [] ); # array based object! $self } sub bar { my $self = shift; my $data = $self->oo_get; $data->[0] = shift if @_; $data->[0] }

      You could now even use OO as library, not as class...

      I think we're on the right way ;)

      --
      http://fruiture.de

        If you can make everyone adhere to your concept then it will indeed work. But that's not a whole lot different from saying that if everyone puts their data in a hash, prepends their attributes with their package name, and uses accessors only, we avoid all the problems.

        But you don't know if everyone does.

        With inside out objects, you don't have to care what your superclass does. It can be implemented in any way. Inside out objects always work.

        Your approach is nice in theory, but violates the principle that classes inheriting from each other should not - and should not have to - know anything about their base class other than the public interface.

        Makeshifts last the longest.

        What does that mean? What global scope shall these attributes have?

        You can get at the attributes of a class in it's sub-classes, for example:

        package Inc; use base 'OO'; sub next { my $self = shift; my $next = $self->oo_get('inc'); $self->oo_set('inc', ++$next); return($next); }; package Inc2; use base qw(Inc); sub current { my $self = shift; $self->oo_get('inc', 'Inc'); };

        Some people consider being able to do this sort of thing bad. You cannot hide implementation details so you can change them without affecting other classes. With inside-out objects the hash can be lexically scoped to the class - so there is no way you can get at it from the super-class.

        { package Inc; my %Value = (); sub new { bless [], shift; }; sub next { ++$Value{+shift}; }; }; { package Inc2; # ... no way to get at %Value here ... };

        While you may not want this feature yourself, it is something that inside objects can do, that your implementation cannot.

        Typing $self->{fo} when you meant to type $self->{foo} won't give you a compile time error.
        You can't solve this problem as long as you use a hash at all. But the danger can be limited to inside the class by enforcing accessor methods. Your Code doesn't help this either.

        You can solve it with inside out objects - one of their great advantages. For example, If I made a typo when I implemented Inc using your module.

        sub next { my $self = shift; my $next = $self->oo_get('inc'); $self->oo_set('incc', ++$next); return($next); };

        I won't get any compile-time error - it will just fail silently.

        If make a typo with the field name with the inside-out object implementation.

        sub next { ++$Valuee{+shift}; };

        I'll get a compile time error with use strict because %Value isn't defined.

        I know the code I showed didn't solve the problem. I wasn't trying to. I was demonstrating that you could get most of what your modules gives you using hash-based objects, rather than a global hash.

        You can't inherit from classes implemented with non-hash references.
        You can. You cannot implement non-hash objects using the OO module, but you can inherit from any object, because _your_ class' data is kept somehwere else. Anyways: the new() was in question. It's probably no good idea, although you're always allowed to override it.

        If you read what I wrote again, you'll see I said:

        Having your own new() method makes mixing this into an existing class hard.

        Having a new function means that you have to take care with the order of your classes in the inheritence hierarchy, otherwise your new will override the new from the other class you are inheriting from.

        For example:

        package AppointmentDate; use base qw(OO Date::Simple); sub appointment { my ($self, $appointment) = @_; $self->oo_set('appointment', $appointment) if $appointment; return( $self->oo_get('appointment') ); };

        won't work because it needs to be use base qw(Date::Simple OO) to get the correct new() method called. In general classes that you intend to mixin to existing class hierarchies should avoid constructors.

        oo_get/set allows you to get the attribute from any class through your optional third argument.
        Just as Perl gives you symbolic references. This is for example needed when you create accessor methods automatically.

        Just because you can do it, doesn't mean that it's a good idea :-) The fact that you cannot do this with Abigail-II's implementation of inside out objects as lexically scoped hashes is a feature, not a bug.

        There are many other ways to make accessor methods without having the ability to get arbritary attributes from objects (e.g. with source filters or like this).

        The idea of putting the data back into the object seperated by classes is great. It makes the object classically serializable and gets us rid of complicated DESTROY methods because it's all in one place. "Ay, there's the rob", it is then impossible to inherit from classes that don't use that new scheme.

        Completely true. The point I was trying to make was that this was the only advantage your system gave you - while dropping the other advantages of using lexically scoped hashes:-)

Log In?
Username:
Password:

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

How do I use this?Last hourOther CB clients
Other Users?
Others taking refuge in the Monastery: (2)
As of 2024-03-19 06:55 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found