Jay Bienvenu
Software developer.
Jay Bienvenu
NetShapers
October 2018
This description of the Option pattern (or what I am calling the Option pattern) is my own. Descriptions of this pattern may differ in literature.
Code shown herein is intentionally simplified to facilitate this presentation. Real-world code may have additional complexity not shown in this presentation.
Isolate operations, especially unstable ones,
into small objects backed by robust testing.
interface Option
{
public function get();
}
class ScalarOption implements Option {
private $_value;
public function __construct($value) { $this->_value = $value; }
public function get() { return $this->_value; }
}
class NullOption implements Option {
public function get() { throw new \Exception('No value.'); }
}
abstract class OptionFactory {
public static function fromValue($value) {
return is_null($value) ? new NullOption() : new ScalarOption($value);
}
}
interface Option
{
public function get();
public function getOrElse($fallback);
public function getOrCall($callable);
public function getOrThrow($exception);
}
class ScalarOption implements Option {
// ...
public function get() { return $this->_value; }
public function getOrElse() { return $this->_value; }
public function getOrCall($callable) { return $this->_value; }
public function getOrThrow() { return $this->_value; }
}
class NullOption implements Option {
// ...
public function get() { throw new \Exception('No value.'); }
public function getOrElse($fallback) { return $fallback; }
public function getOrCall($callable) { return $callable(); }
public function getOrThrow($exception) { throw $exception; }
}
// Before:
if (!is_null($value)) {
return $value;
} else {
return do_something_else();
}
// After:
return $value_option->getOrCall(do_something_else);
interface Option
{
// ...
public function hasValue();
}
class ScalarOption implements Option {
// ...
public function hasValue() { return true; }
}
class NullOption implements Option {
// ...
public function hasValue() { return false; }
}
interface Option
{
public function orElse(Option $else);
}
class ScalarOption implements Option {
public function orElse(Option $else) { return $this; }
}
class NullOption implements Option {
public function orElse(Option $else) { return $else; }
}
// Before:
$x = $repo->findSomething();
if (is_null($x)) {
$x = $repo->findSomethingElse();
if (is_null($x)) {
$x = $repo->createSomething();
}
}
// After: (bad idea; we'll fix it shortly)
$x = $repo->findSomethingReturnOption()
->orElse($repo->findSomethingElseReturnOption())
->orElse($repo->createSomethingReturnOption())
->get();
An Option that specifies a deferred action.
class LazyOption implements Option {
private $_callback;
private $_arguments;
public function __construct($callback, array $arguments = []) {
$this->_callback = $callback;
$this->_arguments = $arguments;
}
private function option()
{
$option = call_user_func_array($this->callback, $this->arguments);
if ($option instanceof Option) return $option;
return new NullOption();
}
public function get() { return $this->option()->get(); }
public function getOrCall($callable) { return $this->option()->getOrCall($callable); }
public function getOrElse($fallback) { return $this->option()->getOrElse($fallback); }
public function getOrThrow($exception) { return $this->option()->getOrThrow($exception); }
public function hasValue() { return $this->option()->hasValue(); }
public function orElse(Option $else) { return $this->option()->orElse($else); }
}
// Bad:
$x = $repo->findSomethingReturnOption()
->orElse($repo->findSomethingElseReturnOption())
->orElse($repo->createSomethingReturnOption())
->get();
// Better:
$x = $repo->findSomethingReturnOption()
->orElse(new LazyOption([$repo,'findSomethingElseReturnOption']))
->orElse(new LazyOption([$repo,'createSomethingReturnOption']))
->get();
interface Option
{
// ...
/**
* Applies callable to non-empty value.
* Returns return value of callable wrapped in Option().
* If option is empty, then callable is not applied.
*/
public function map($callable) : Option;
/**
* Applies callable to non-empty value; returns return value of callable directly.
* Expects callable to return an Option.
*/
public function flatMap($callable) : Option;
/**
* Applies callable to non-empty value, but does not return value.
*/
public function forAll($callable) : Option;
}
class ScalarOption implements Option {
// ...
public function map($callable)
{
return OptionFactory::fromValue(call_user_func($callable, $this->value));
}
public function flatMap($callable)
{
return call_user_func($callable, $this->value); // must return Option
}
public function forAll($callable)
{
return call_user_func($callable, $this->value); return $this;
}
}
class NullOption implements Option {
// ...
public function map($callable) { return $this; }
public function flatMap($callable) { return $this; }
public function forAll($callable) { return $this; }
}
By Jay Bienvenu