Note that there are some explanatory texts on larger screens.

plurals
  1. PO
    primarykey
    data
    text
    <p>The capability to participate in groups/meetings and have projects is something that a user might be able to <em>do</em>, but it's not something that defines what a user <em>is</em>. This is a pretty clear sign that modelling these options with additional classes is not a good design choice.</p> <h2>Static approach #1: interfaces</h2> <p>In a statically typed language a simplistic implementation would look something like </p> <pre><code>interface IGrouppableUser { public function join(...); } class GroupableUser implements IGrouppableUser { public function join(...) { /* implementation */ } } </code></pre> <p>And the consumers of grouppable users would accept <code>IGrouppableUser</code>, 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.</p> <p>As a footnote, I should add that with the addition of <a href="http://php.net/manual/en/language.oop5.traits.php" rel="nofollow">traits</a> 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 <code>join</code> all around the code base). But conceptually it's the exact same approach.</p> <p>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.</p> <h2>Static approach #2: "not supported" exceptions</h2> <p>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 <code>User</code>, making it a <a href="http://www.oodesign.com/interface-segregation-principle.html" rel="nofollow">fat interface</a>:</p> <pre><code>class GroupableUser implements IGrouppableUser { private $isGrouppable = true; // default, can be changed at runtime public function join(...) { if (!$this-&gt;isGrouppable) throw new Exception("User is not grouppable!"); // real implementation } } </code></pre> <p>The main disadvantage of this approach is that it makes the class <code>User</code> appear 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 of <code>try</code>/<code>catch</code>). It might be OK if the vast majority of users support the vast majority of operations.</p> <h2>Dynamic approach #1: behaviors</h2> <p>It would be much better to conditionally allow <code>User</code> instances to participate in these operations. This means that you need to be able to dynamically attach "behaviors" to <code>User</code> objects, which is fortunately quite easy to do in a dynamically typed language.</p> <p>I suggest looking up a "behaviors" implementation from an established open-source project, but here's a quick and dirty example:</p> <p><strong>Behavior base class and sample implementation</strong></p> <pre><code>abstract class Behavior { public function provides($name) { return method_exists($this, $name); } public function invoke($target, $name, $arguments) { array_unshift($arguments, $target); return call_user_func_array(array($this, $name), $arguments); } } class GrouppableBehavior extends Behavior { public function join(User $user, $groupName) { echo "The user has joined group $groupName."; } } </code></pre> <p><strong>Composable (can use behaviors) base class and User implementation</strong></p> <pre><code>class Composable { private $behaviors = array(); public function __call($name, $arguments) { foreach ($this-&gt;behaviors as $behavior) { if ($behavior-&gt;provides($name)) { return $behavior-&gt;invoke($this, $name, $arguments); } } throw new Exception("No method $name and no behavior that implements it"); } public function attach($behavior) { $this-&gt;behaviors[] = $behavior; } } class User extends Composable {} </code></pre> <p><strong>Test driver</strong></p> <pre><code>$user1 = new User; $user2 = new User; $user1-&gt;attach(new GrouppableBehavior); $user1-&gt;join('Test Group'); // works $user2-&gt;join('Test Group'); // throws </code></pre> <p><strong><a href="http://ideone.com/Y8NqKo" rel="nofollow">See it in action</a></strong>.</p> <p>The main disadvantages of this approach are that it consumes more runtime resources and that behaviors can only access <code>public</code> members 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.</p> <h2>Dynamic approach #2: decorators</h2> <p>A variation on behaviors is the <a href="http://www.oodesign.com/decorator-pattern.html" rel="nofollow">decorator pattern</a>:</p> <pre><code>interface IUser {} interface IGrouppableUser extends IUser { public function join(...); } class User implements IUser {} class UserGroupingDecorator implements IGrouppableUser { private $realUser; public function __construct(IUser $realUser) { $this-&gt;realUser = $realUser; } public function join(...) { /* implementation */ } /* now you need to implement all IUser methods and forward the calls to $this-&gt;realUser */ /* if IUser exposes bare properties we have a problem! */ } </code></pre> <p>Using this pattern you can create a <code>UserGroupingDecorator</code> that wraps an <code>IUser</code> at will and pass the decorator to anything that accepts either an <code>IUser</code> or an <code>IGrouppableUser</code>. </p> <p>The main disadvantage of this approach is that it also does not provide access to the non-public members of <code>User</code>. In addition it rules out exposing bare properties from <code>IUser</code> as there is no way to "forward" bare property accesses from <code>UserGroupingDecorator</code> to <code>$realUser</code> if the properties are also defined on the former -- and you cannot implement <code>IGrouppableUser</code> unless 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.</p>
    singulars
    1. This table or related slice is empty.
    plurals
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. This table or related slice is empty.
    1. VO
      singulars
      1. This table or related slice is empty.
    2. VO
      singulars
      1. This table or related slice is empty.
    3. VO
      singulars
      1. This table or related slice is empty.
 

Querying!

 
Guidance

SQuiL has stopped working due to an internal error.

If you are curious you may find further information in the browser console, which is accessible through the devtools (F12).

Reload