0

I am trying to come up with a way to be able to make any (or at least a group of) class have the ability to have options.

This is the generic interface that I would like to have

interface OptionableInterface
{
    public function getOption($key);
    public function setOption($key, $value);
    public function getOptions();
    public function setOptions($options, $merge);
    public function removeOption($key);
}

I've thought about either implementing a concrete class with the above interface then extending it as needed, but since PHP has no multiple inheritance, this could be a problem.

The other way would be to use the decorator pattern. But I'm uncertain if this is the correct usage of decorator pattern.

Any ideas? I'm stuck using PHP 5.2 for now (maybe able to change to 5.3 later in which case I can use traits).

Populus
  • 7,470
  • 3
  • 38
  • 54
  • This is called `mixin` - you include your extension in class without actually inheriting it: http://stackoverflow.com/questions/6876925/is-it-possible-to-use-mixins-in-php – Karel Frajták Sep 12 '13 at 18:42
  • 1
    traits were released first in PHP 5.4. So you will not be able to use them if you upgrade only to PHP 5.3 – catalin.costache Sep 12 '13 at 18:42
  • Why not just make the Options explicit so I don't have to lookup your documentation each time I want to use an object that is Optionable to find out what options it can take? Would make for much more readable code. Scrap that interface. – Gordon Sep 12 '13 at 18:45
  • yea sorry about that, I remembered wrong 5.4 it is :P I guess no traits for me as my company won't upgrade to 5.4 any time soon – Populus Sep 12 '13 at 18:45
  • @Gordon I'm trying to have a more complicate set of options than just an array, e.g. `public function setOptions($options, $merge)` would allow overwriting all options, or merging. If I do this for all classes that have this functionality, that would be alot of duplicate code. – Populus Sep 12 '13 at 18:50
  • 1
    All these classes are hard to figure out then. No one can know from looking at the API of your classes what options they can take. So instead of having one generic but non-speaking OptionableInterface give your classes meaningful setters, like `setFoo(42)` instead of doing `setOption('foo', 42);`. – Gordon Sep 12 '13 at 18:55
  • I agree with @Gordon. Having explicit setters and getters would make a better API then fuzzy options. You say it would create a lot of duplicate code but it really doesn't. Just setting a property internally the same way in some classes is not really code duplication. – Bart Sep 12 '13 at 19:26
  • I also see this as an anti-pattern. One that causes much of the API documentation to be incomplete. – Louis-Philippe Huberdeau Sep 12 '13 at 20:07
  • @Gordon I understand that, thanks for bringing that up. I guess I'm trying to be lazy (in the long run), and there is alot of respectable code out there that already uses options, but I wanted to take it further and be able to attach that functionality to any class. – Populus Sep 13 '13 at 14:41

2 Answers2

2

First of all, you have to be careful in order not to violate the Single-Responsibility Principle. An object that does something and have options in mind, should take those options as an argument.

interface CookieOptionsInterface
{
    public function setPath($path);
    public function getPath();
    public function setTtl();
    public function getTtl();

    //...The rest if needed 
}


class Cookie
{
    protected $cookieOptions;
    
    public function __construct(CookieOptionsInterface $cookieOptions)
    {
       $this->cookieOptions = $cookieOptions;
    }

    public function write(array $pair)
    {
        foreach ($pair as $key => $value) {
              setcookie($key, $value, $this->cookieOptions->getTtl() + time(), $this->cookieOptions->getPath());
        }
    }

    // .. The rest
}

The core points to note:

  • A Cookie should be completely unaware of default options. It does only one thing - Cookie CRUD operations

  • CookieOptionsInterface should be completely unaware of Cookie class. It does only one thing - it abstracts options access

  • It does not break encapsulation

  • It adheres to Dependency Injection (Thus you can easily mock $cookieOptions)

  • It adheres to the Single-Responsibility Principle

  • Since CookieOptionsInterface is completely decoupled, you can inherit from (Thus reducing code duplication for another options, like CookieBar)

Community
  • 1
  • 1
Yang
  • 8,580
  • 8
  • 33
  • 58
  • This looks good, let me try it out with my code and see if I have anymore questions – Populus Sep 12 '13 at 19:44
  • "A Cookie should be completely unaware of default options": no, it shouldn't because they are an integral part of a cookie. Hence, providing the API to set these values on the Cookie is perfectly fine. There is no SRP violation when the cookie has write() and setName(), etc because they belong together. They are part of one responsibility. You can easily see that the Cookie has high cohesion by noticing that the write method uses these options. The only benefit of having the CookieOptions is to share default options between Cookies. – Gordon Sep 12 '13 at 20:07
  • I hear what you're saying, because I used to think exactly the way you do in the past. If you have `setName()` then you should also implement `getName()` in order to satisfy the `POLS`. The point? You end up creating a bunch of setters and geters in `Cookie` class. Since setters and getters serve *data container* responsibility, and since Cookie basically serves CRUD operations, they should be decoupled. By the way, this idea is not mine, it comes from here : http://www.sitepoint.com/the-single-responsibility-principle/ – Yang Sep 12 '13 at 20:18
  • I am not saying that you should have a Getter when you have a Setter. Not at all. You want to avoid both most of the time in favor of *Tell Don't Ask*. What I am saying is that the data in the Options is what makes up the identity of the Cookie as a cohesive whole (plus the name/value pairs). They are not some isolated data part. The Cookie does not merely *use* them. It *is* them. Having Setters for these would be okay, because these would then be part of the "set of operations" that make up the *one reason to change*. They change together. – Gordon Sep 13 '13 at 06:31
  • So your CookieOptions really divides a single responsibility into two. That's okay if you want to reuse the CookieOptions in other Cookies (*Flyweight*), but it's not neccessary to follow SRP. We could argue that writing the Cookie to a header is a separate responsibility. That would warrant a CookieWriter then though instead of splitting of the cohesive Cookie data. But by virtue of *InformationExpert* the writer method *does* belong on the Cookie. On a side note, http://butunclebob.com/ArticleS.UncleBob.PrinciplesOfOod is likely better than anything you'll ever find on sitepoint about SOLID – Gordon Sep 13 '13 at 06:49
1

You could use composition instead of inheritance or interfaces. Maybe something like this:

class SomethingThatHasOptions {
  public $options;
  public function __construct () {
    $this->options = new OptionProvider ();
  }
}

class OptionProvider {
    public function getOption($key) { ... }
    public function setOption($key, $value) { ... }
    public function getOptions() { ... }
    public function setOptions($options, $merge) { ... }
    public function removeOption($key) { ... }
}

And then you can use it like this:

$optionable = new SomethingThatHasOptions;
$optionable->options->setOption('foo', 42);
Gordon
  • 312,688
  • 75
  • 539
  • 559
grossvogel
  • 6,694
  • 1
  • 25
  • 36
  • Thanks! This works, but I'm trying to find a more elegant way of doing it :) Will probably fall back to your answer if nothing better is found. – Populus Sep 12 '13 at 18:47
  • 1
    `$optionable->options->...` <- This breaks encapsulation, which states *An object should be in complete control of its state and implementation* – Yang Sep 12 '13 at 18:58
  • 1
    @DaveJust: True. It's better to inject the options as a dependency. – grossvogel Sep 12 '13 at 19:55