I have a base user class which is responsible for manipulating the basic user information: name, age, location, etc. I want to extend my system with the groups functionality and later with projects and meetings. For example:
class User
{
public function getName() {
...
}
public function getAge() {
...
}
}
And then extend it with groups functionality:
class GroupableUser extends User
{
public function join($groupId) {
...
}
public function leave($groupId) {
}
public function requestGroupInvitation($groupId) {
...
}
public function acceptGroupInvitation($groupId) {
...
}
}
The name GroupableUser seems weird to me, however it does’t make sense to add join and leave methods to Group class, because its user that joins and leaves a group, not the other way around.
And later I would have classes like UserThatCanHaveProjects and UserThatCanParticipateInMeetings.
How to name these classes?
And how do you typically deal with these situations?
The capability to participate in groups/meetings and have projects is something that a user might be able to do, but it’s not something that defines what a user is. This is a pretty clear sign that modelling these options with additional classes is not a good design choice.
Static approach #1: interfaces
In a statically typed language a simplistic implementation would look something like
And the consumers of grouppable users would accept
IGrouppableUser, allowing you to craft as many classes as necessary. You can also do this in PHP, but as mentioned earlier it’s probably not a good design no matter what the language.As a footnote, I should add that with the addition of traits to the language starting from PHP 5.4 the above scenario can be implemented a bit more conveniently (classes can use a trait instead of implementing an interface, which means you don’t need to copy/paste the implementation of
joinall around the code base). But conceptually it’s the exact same approach.The main disadvantage of this approach is that it does not scale. It might be OK if you only need two or three types of users.
Static approach #2: “not supported” exceptions
If most of the users are grouppable and can have projects then it doesn’t make much sense to create a hellish hierarchy of classes; you can just add the necessary members to class
User, making it a fat interface:The main disadvantage of this approach is that it makes the class
Userappear to unconditionally support a wide range of operations when in fact it does not and as a result can make coding tedious and error-prone (lots oftry/catch). It might be OK if the vast majority of users support the vast majority of operations.Dynamic approach #1: behaviors
It would be much better to conditionally allow
Userinstances to participate in these operations. This means that you need to be able to dynamically attach “behaviors” toUserobjects, which is fortunately quite easy to do in a dynamically typed language.I suggest looking up a “behaviors” implementation from an established open-source project, but here’s a quick and dirty example:
Behavior base class and sample implementation
Composable (can use behaviors) base class and User implementation
Test driver
See it in action.
The main disadvantages of this approach are that it consumes more runtime resources and that behaviors can only access
publicmembers of the classes they are attaching to. In some cases you may find yourself forced to expose an implementation detail that should be private to enable a behavior to work.Dynamic approach #2: decorators
A variation on behaviors is the decorator pattern:
Using this pattern you can create a
UserGroupingDecoratorthat wraps anIUserat will and pass the decorator to anything that accepts either anIUseror anIGrouppableUser.The main disadvantage of this approach is that it also does not provide access to the non-public members of
User. In addition it rules out exposing bare properties fromIUseras there is no way to “forward” bare property accesses fromUserGroupingDecoratorto$realUserif the properties are also defined on the former — and you cannot implementIGrouppableUserunless they are indeed defined. This state of affairs can be sidestepped by exposing properties as distinct getter/setter methods, but that means still more code to write.