0

I would like to combine 2 objects through composition with one optional object and keep a generic and clean API. To provide an example:

class Composition implements Object1Interface, Object2Interface
{
    private $object1;

    private $object2;

    public function __construct($object1, $object2 = null)
    {
        $this->$object1 = $object1;
        $this->$object2 = $object2;
    }

    // This will always work since the object is mandatory.
    public function getId() {
        return $this->object1->getId();
    }

    // This may fail because the object2 can be null.
    public function getName() {
        return $this->object2->getName();
    }
}

You could see that this starts to fail pretty fast since I could be proxying on a null object here. What would be a good way to solve this?

If I were to make $object2 mandatory while I might not have all the constructor data at all times to fill out $object2, I need to make all arguments for the constructor optional. This seems like a big no-no since it would no longer have required arguments.

An option would be to create a method to return $object2 but this requires chaining by a user like so:

$composition->getObject2()->getName()

This could be acceptable but I am destroying the clean API that I have and am now chaining methods.

Is there anything that can be done here to fix this or should I simply go with the mentioned chaining of methods solution?

Stephan-v
  • 19,255
  • 31
  • 115
  • 201
  • What would be the expected behavior when `$object2` is accessed even so it wasn't supplied? Also the chaining doesn't help much, it would just fail at a different point in the code. – Yoshi Oct 24 '17 at 10:21
  • `public function getName() { if($this->object2) return $this->object2->getName(); throw new \Exception('...')}` – Alex Blex Oct 24 '17 at 10:23

2 Answers2

1

I guess you mean something like null-safe operator.

Unlike Hack, the PHP does not have this operator (the relevant RFC), so you need explicitly check the values for null. Also you can use something like Option Type. At least you can dynamically create "default" versions of dependent objects:

class Composition implements Object1Interface, Object2Interface
{
    private $object1;

    // ...

    public function __construct(Object1Interface $object1 = null, Object2Interface $object2 = null)
    {
        if ($object1 !== null) {
            $this->object1 = $object1;
        } else {
            $this->object1 = new class implements Object1Interface
            {
                public function getId()
                {
                    return null;
                }
            };
        }

        // ...
    }

    public function getId() {
        return $this->object1->getId(); // Always works.
    }
}
Timurib
  • 2,735
  • 16
  • 29
1

You have two options: Either always check if the object is set before returning anything from it, or use a "dummy" object in place of a user-supplied one if one isn't supplied.

The former can become quite messy as you have to add guards around everything, so I'd personally prefer the latter.

One thing that will make implementing the latter a lot simpler is using an interface to specify the specifications of the expected object and having your constructor instantiate the dummy object if no real object is supplied.

interface AThingThatDoesSomething
{
    public function getValue() : integer
}

class RealClass implements AThingThatDoesSomething
{
    public function getValue() : integer
    {
        return mt_rand();
    }
}

class DummyClass implements AThingThatDoesSomething
{
    public function getValue() : integer
    {
        // You could return a dummy value here, or throw an exception, or whatever you deem to be the correct behaviour for the dummy class.  This implementation just returns 0 for simplicity
        return 0;
    }
}

class ConsumingClass
{
    private $requiredInstance = null;
    private $optionalInstance = null;

    public function __construct(AThingThatDoesSomething $requiredInstance, AThingThatDoesSomething $optionalInstance = null)
    {
        if (null === $optionalInstance)
        {
            $optionalInstance = new DummyClass();
        }

        $this->requiredInstance = $requiredInstance;
        $this->optionalInstance = $optionalInstance;
    }

    public function getRequiredVal() : integer
    {
        return $this->requiredInstance->getValue();
    }

    // You don't need to worry if the user supplied an optional instance here because if they didn't then the dummy instance will have been instantiated instead
    public function getOptionalVal() : integer
    {
        return $this->optionalInstance->getValue();
    }
}

This might seem like a contrived example, and of course you'd be right, but it also demonstrates one of the benefits of a pattern called Design By Contract. As long as an object promises to meet certain criteria (in this case by implementing an interface) then you can substitute any object that meets those criteria, even if the object doesn't actually do anything.

In real life I use this for classes where I need logging. I use the psr\log package and set a new NullLogger in the constructor. If I need actual logging I then use setLogger() to pass a logger in, but if I don't then I don't need to worry about $this->logger failing because it's always set.

GordonM
  • 31,179
  • 15
  • 87
  • 129