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.
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.
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.
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
(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