Class::Encapsulate - catch naughty code

ChrisDolan on 2006-10-07T04:38:10

Earlier today, Curtis "Ovid" Poe wrote in his journal about a trick to restrict accesses to a blessed hash to packages in that instance's inheritence tree. He saw this as a clever way to add strict encapsulation to objects.

I got intrigued by this idea not for real code but for detecting naughty code during debugging. It overloads hash accesses to throw exceptions if anyone tries to access object properties directly instead of through accessor methods.

Even more interesting, I figured out how to insert the encapsulation at runtime!

I created a prototype (not headed to CPAN yet) that I'm calling Class::Encapsulate for now. Feedback on that name is welcome. As proof of concept, I applied this prototype to the Perl::Critic test suite. Here's the block of code I inserted into the Perl::Critic test code in t/40_criticize.t:

# Strict object testing -- prevent direct hash key access eval { require Class::Encapsulate::Runtime; }; if ( !$EVAL_ERROR ) { for my $pkg ( '', '::Config', '::Policy', '::Violation' ) { diag 'apply Class::Encapsulate to Perl::Critic'.$pkg; Class::Encapsulate::Runtime->apply_to('Perl::Critic'.$pkg); } }

My pre-0.01 prototype is in the Clotho SVN


Bravo

Alias on 2006-10-07T06:59:49

I think I MUCH prefer testing for this your way than enforcing the encapsulation via all that extra code the inside-out way.

Nits

jjore on 2006-10-07T16:59:00

ref() lies about object blessings.

my $pkg = ref $self; -> my $pkg = blessed $self;

Only undef indicates "not-blessed." Some classes are false.

if ( !$pkg ) { -> if ( !defined $pkg ) {

You might want to look at ->DOES( ... ) as well. It's new to 5.10 but it gets at the idea of interfaces that don't use ISA. Allomorphism and all that stuff chromatic is on about.

The return result of _hash_overload should probably be the object itself. You don't need to have a separate hash in %instances, store the object itself. From overload.pm: "As a special case if the overload returns the object itself then it will be used directly (provided it is the correct type)."

DESTROY isn't using SUPER.pm or SUPER:: to propagate ->DESTROY up to its parents. You might prevent proper destruction by not redispatching.

Good job!

Re:Nits

Aristotle on 2006-10-07T18:41:37

In fact he needs to duplicate all the things that Class::InsideOut provides in order to be leak- and thread-safe.

Re:Nits

ChrisDolan on 2006-10-07T18:52:19

Agreed. Which is why I don't want to bother making it super-safe. Josh's comments are much appreciated, and I'll implement as many of them as are easy, but I've made it clear in the POD that this is a hack. I do NOT recommend this for production code. Instead I intend to angle this specifically toward debugging.

I've considered calling it Devel::Encapsulate. If you want real encapsulation, you should use a real implementation like inside-out objects.

Re:Nits

jjore on 2006-10-07T21:58:04

It occurs to me that he can remove the DESTROY method along with the %instances hash and just throw errors when unauthorized access is attempted.

No storage is even required to do this.

Re:Nits

ChrisDolan on 2006-10-07T18:55:24

Excellent points. Thanks!

The !$pkg part was just to catch a bug that I've since removed, so I think it's obsolete. ref() is good enough for this hack module because if you are using it with something other than blessed hashes, then you get what you deserve.

Re:Nits

jjore on 2006-10-07T21:56:09

Ok? But ref() is still inaccurate. defined( blessed( ... ) ) is accurate and you already have Scalar::Util. Why not go for full fidelity?

Re:Nits

ChrisDolan on 2006-10-08T03:08:32

Ok? But ref() is still inaccurate. defined( blessed( ... ) ) is accurate and you already have Scalar::Util. Why not go for full fidelity?


I don't understand. ref() and blessed() only differ in behavior for SCALAR, HASH, ARRAY, CODE, etc. right? Are you worried that someone is going to apply Class::Encapsulate to HASH? Would that even work???

Re:Nits

jjore on 2006-10-08T17:36:07

ref() returns a true value for all references even if they're not blessed. It returns a false value for objects blessed into the two classes 0 and ''.

defined( blessed() is full fidelity because it always returns true for all objects and it always returns false for all non-objects. Plain bool( blessed() ) is *not* sufficient because not all class names evaluate to true.

This is why blessed() returns undef instead of '' for it's "false" value. blessed( bless ..., 0 ) is false and so is blessed( bless ..., "\0" ). Wrapping defined( ... ) around that gives the proper answer.

It's cheap, it's easy, it's the right thing.

Re:Nits

ChrisDolan on 2006-10-08T19:06:10

I understand the merits of blessed(), but they are not relevant here. I already *know* my object is blessed. It wouldn't get into my overload function if it wasn't. If you look at the current SVN code you'll see that I'm not using ref() in boolean context any longer.

I don't care about non-blessed refs because they can't be affected by "use overload". And "0" and "" are not valid names in "package" statements, so I don't care about those either -- people who bless instances into packages like that get what they deserve! :-)

So, for my purpose (determining the package of a blessed hashref), ref() and blessed() are 100% equivalent, and the latter is slower so why would I use it?

Re:Nits

jjore on 2006-10-09T01:13:07

Ooooh. Yeah. $self is already an object so you only need the class name. Oops.

As for being safe with defined(blessed(...)), I advocate that because there really is seriously weird code out there and pretending otherwise just gets us into accidents.

Also, there's a related issue that no one has come up with a good method overridable method to ask an object what class it wants to pretend to be blessed into. Maybe Moose/MOP provides a nice name. The ref() function is absolutely evil to override while at least blessed() is possible to hook without difficulty. I'd mention http://perlmonks.org/?node_id=544094 as a reference for just how evil ref() is when using mock objects.

I read https://svn.clotho.com/clotho/Class-Encapsulate/lib/Class/Encapsulate.pm and it looks good except that I'm not clear on why Class::Encapsulate has %{} overloading when you're applying that to the actual class in https://svn.clotho.com/clotho/Class-Encapsulate/lib/Class/Encapsulate/Runtime.pm . It looks like one of those is dead code. I suspect the one in Class/Encapsulate.

Re:Nits

ChrisDolan on 2006-10-09T01:39:09


I read https://svn.clotho.com/clotho/Class-Encapsulate/lib/Class/Encapsulate.pm and it looks good except that I'm not clear on why Class::Encapsulate has %{} overloading when you're applying that to the actual class in https://svn.clotho.com/clotho/Class-Encapsulate/lib/Class/Encapsulate/Runtime.pm . It looks like one of those is dead code. I suspect the one in Class/Encapsulate.


I should document that... It's so Class::Encapsulate can be a superclass and automatically install the encapsulation. That comes directly from Adrian Howard's original implementation.

Re:Nits

ChrisDolan on 2006-10-08T03:32:12

I implemented a bunch of these suggestions. Would you be willing to look at SVN again? Thanks much!