Object Responsibilities versus Object Behavior

Ovid on 2009-10-07T13:16:43

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_cache
_build_unique_query
_calculate_score
_collapse_cond
_collapse_query
_collapse_result
_cond_for_update_delete
_construct_object
_count
_is_unique_query
_load_components
_merge_attr
_mk_group_accessors
_remove_alias
_resolve_from
_resolved_attrs
_result_class_accessor
_rollout_array
_rollout_attr
_rollout_hash
_source_handle
_unique_queries
all
can
carp
clear_cache
cluck
component_base_class
count
count_literal
create
croak
cursor
delete
delete_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
get_simple
get_super_paths
import
inject_base
isa
load_components
load_optional_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_result
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.


interface

niceperl on 2009-10-07T19:39:08

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.

Re:interface

perigrin on 2009-10-07T22:21:41

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.

don't mix responsibilities

dami on 2009-10-07T21:06:14

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 agree that instances of DBIx::Class have too many methods, but that's not a consequence of using inheritance, it's a consequence of the particular DBIx::Class design, which is another mix of responsabilities : datasources and resultsets are merged into one single concept.

You're hiding the details ...

perigrin on 2009-10-07T22:41:06

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',
        traits => ['Persisted']
    );

    has name => (
        isa => 'Str',
        is => 'ro',
        traits => ['Persisted']
    );

    sub restart {
        my ( $self, $nice ) = @_;
        say $nice ? 'yes' : 'no';
    }

Where Persistent is a parameterized role applied to the metaclass, and SomeORM is a parameter to define what storage engine to use. Effectively your Class's instance has-a ORM and that ORM is used to persist the attributes you care about.

To be more blunt, dami is correct ... the relationship between between SomeORM and Server is 'has' not 'does' or 'is'. Your use of roles there doesn't change that fact it simply hides the implementation you're complaining about somewhere useful for the sake of your argument.

A meet-in-the-middle solution: Delegation.

hobbs on 2009-10-08T05:41:44

(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)],
);

Representation, not delegation or inheritance

moritz on 2009-10-08T09:48:04

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

A verbose response

nothingmuch on 2009-10-08T15:53:03

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