Stories
Slash Boxes
Comments
NOTE: use Perl; is on undef hiatus. You can read content, but you can't post it. More info will be forthcoming forthcomingly.

All the Perl that's Practical to Extract and Report

use Perl Log In

Log In

[ Create a new account ]

Ovid (2709)

Ovid
  (email not shown publicly)
http://publius-ovidius.livejournal.com/
AOL IM: ovidperl (Add Buddy, Send Message)

Stuff with the Perl Foundation. A couple of patches in the Perl core. A few CPAN modules. That about sums it up.

Journal of Ovid (2709)

Wednesday October 07, 2009
08:16 AM

Object Responsibilities versus Object Behavior

[ #39728 ]

There seems to be some confusion about some of the issues which roles are designed to deal with. I'm going to take another stab at this. Everyone who uses roles (that I've talked to) agrees that they're ridiculously easy to use. What they don't understand is why. I want to explain a core concept in objected-oriented programming (OOP) which seems to be at the heart of much of this confusion. This can help you regardless of whether or not you adopt roles.

Imagine that you have a Server class. This class might look like this Perl 6 class definition:

class Server {
    has $.ip_address;
    has $.name is rw;

    method restart(Bool $nice=True) {
        say $nice ?? 'yes' !! 'no';
    }
}

my Server $s .= new( name => 'localhost', ip_address => '192.168.0.1' );
say $s.name;
$s.restart;

That will output (well, with a recent version of Rakudo):

localhost
yes

What's important here is that this class handles the responsibilities that a server needs (well, some of them).

Now let's say that servers are uniquely identified by name and you need to find "Plato" and change its name to "Pluto" since you identify servers as celestial bodies. Here's one hypothetical interface.

my Server $server .= find( name => 'Plato' );
$server.name = 'Pluto';
$server.save;

Where did the find and save method come from? Here's where they should not come from:

class Server isa SomeORM { ... } # wrong!

Why is that? Well, SomeORM theoretically is an Object-Relational Mapper (ORM) akin to DBIx::Class, Hibernate and similar technologies. The problem with inheritance here is that your Server class isn't an ORM, It's a server. Maybe it could subclass off of Computer on the theory that it's a specialized type of Computer, but it's not an ORM.

That theoretical point leaves us with an interesting conundrum. The Server class has clear responsibilities and the SomeORM class should provide behaviors which various classes -- which may or may not be related by inheritance -- such as Customer, DataCenter, Office need to implement. To say SomeClass isa SomeOtherClass is to promise that the child has the same responsibilities of the parent. A customer class is responsible for providing the customer's name, the customer's age, the customer's balance, etc. No other class should share this responsibility. Instead, other classes should be asking the customer for this information.

However, the find and save behaviors are shared by multiple classes and are not intrinsically part of a class' responsibilities. They're artifacts of how we're using the classes (this gets a bit dodgy here as any subset of responsibilities for what you're modeling are artifacts of your business needs).

Got that? Share behaviors, not responsibilities (admittedly some responsibilities are implemented by collections of classes which act in concert, but each class encapsulates its responsibilities in the same way that the collection of classes should be solely responsible for the responsibilities it manifests). Why is this important? Here's some Perl 5:

package Book;
use parent 'DBIx::Class';

In DBIx::Class, you have a resultset class for every resultsource (I really wish they called the latter "resultitem"). What have you inherited from DBIx::Class::ResultSet and its parent classes?

MODIFY_CODE_ATTRIBUTES
VERSION
__source_handle_accessor
_add_alias
_attr_cac he
_build_unique_query
_calculate_score
_collapse_cond
_collapse_query
_col lapse_result
_cond_for_update_delete
_construct_object
_count
_is_unique_que ry
_load_components
_merge_attr
_mk_group_accessors
_remove_alias
_resolve_ from
_resolved_attrs
_result_class_accessor
_rollout_array
_rollout_attr
_r ollout_hash
_source_handle
_unique_queries
all
can
carp
clear_cache
cluck
component_base_class
count
count_literal
create
croak
cursor
delete
del ete_all
ensure_class_found
ensure_class_loaded
exists
find
find_or_create
find_or_new
first
get_cache
get_column
get_component_class
get_inherited
g et_simple
get_super_paths
import
inject_base
isa
load_components
load_opti onal_class
load_optional_components
load_own_components
make_group_accessor
make_group_ro_accessor
make_group_wo_accessor
mk_classaccessor
mk_classdata
mk_group_accessors
mk_group_ro_accessors
mk_group_wo_accessors
new
new_resul t
next
page
pager
populate
related_resultset
require
reset
result_class
result_source
search
search_like
search_literal
search_related
search_rs
set_cache
set_component_class
set_inherited
set_simple
single
slice
throw_ exception
update
update_all
update_or_create
use

Well, that's interesting. You've inherited close to 100 methods! You'll discover that the resultsource classes also inherit a bunch of methods you possibly didn't want. Do you know what they are? Do you know if you're going to override them in your class?

That's what's wrong with inheritance. You're pulling in a bunch of behavior you do not want or need. I'd much rather see this:

class Server does SomeORM {
    has IPAddress $.ip_address is persisted;
    has Str       $.name       is persisted;

    method restart(Bool $nice=True) {
        say $nice ?? 'yes' !! 'no';
    }
}

Separating behaviors out into roles, classes return to their original purpose of handling their responsibilities and shared behaviors are broken out into roles which implement those behaviors. It's much cleaner and if one role's behavior conflicts with another role's behavior, you find out at composition time. You won't get that with inheritance (unless you're programming in Eiffel).

There is one fly in this ointment, though. What happens if SomeORM implements a restart method? Currently, both Moose roles and the Perl 6 spec say that the class wins and silently ignores the role's method. This is as bad as the inheritance behavior. I would like to at least see an overrideable warning.

The Fine Print: The following comments are owned by whoever posted them. We are not responsible for them in any way.
 Full
 Abbreviated
 Hidden
More | Login | Reply
Loading... please wait.
  • the first time I heard about this concept was in 1996, studying the Java interface keyword. And the next step is "programming oriented to the interface", thinking in classes is bad. You must design witn roles in mind.
    • Interfaces are a special case of Roles actually. They are effectively pure abstract Roles. Roles however can provide a default implementation of a contracted (this is also called Design By Contract) method. This is why the override warning was removed from Moose (it did have this behavior for a very short period of time). The warning was penalizing a valid idiom.

  • Hi Ovid,

    Your example doesn't sound right to me. The job of a server is to serve requests; the job of an ORM is to give access to some persistence methods; merging both responsibilities into a single class, be it through inheritance or through roles, is confusing. I would rather have a ServerInfo class that keeps the persistent data, and a Server class that deals with requests, with a "has-a" relationship between them (and then it doesn't matter through which mechanism such classes are composed).

    Besides, I a

  • Role composition doesn't solve the method-explosion problem. Suggesting it does actually hides the obvious solution to the problem because to properly implement your role you need to create an ORM delegate anyway. The proper solution to this in Moose terms is:

        package Server;
        use Moose -traits Persistent( storage => SomeORM );

        has ip_address => ( i
            sa => 'IPAddress',
            is => 'ro',
         

  • (in Moose syntax because I haven't boned up on this part of the Perl 6 syntax yet)


    has '_orm' => (
        isa => 'SomeORM',
        is => 'ro',
        lazy_build => 1,
        builder => '_connect_to_orm',
        handles => [qw(find)],
    );

  • In a perfect Perl 6 world you'd implement an ORM not as a inheritance or delegation, but by providing a different low-level representation, which manages storage to a db.

    The object doesn't have to be aware that it's stored, you just have to provide a constructor that allows injecting of a different representation.

    Sadly Rakudo doesn't implement representation polymorphism yet, SMOP might do it already

  • I started responding in comments yesterday, but moved it to a blog post when it grew long. Then it grew longer ;-)

    Short story: I think moritz has hit the nail on the head, but I go into details about the ideas that dami, perigrin and hobbs commented about.

    http://blog.woobling.org/2009/10/roles-and-delegates-and-refactoring.html [woobling.org]