1

lets create a list of animal types:

abstract class Item
{
    public function run()
    {
        echo __FUNCTION__.'<br>';
    }
}

class Reptile extends Item
{
    public function putEgg()
    {
        echo __FUNCTION__.'<br>';
    }
}

class Mammal extends Item
{
    public function born()
    {
        echo __FUNCTION__.'<br>';
    }
}

$list = [];
for ($i = 1; $i <= 10; $i++)
{
    switch(mt_rand(1,2))
    {
        case 1 :
            $o = new Reptile();
            break;
        case 2 :
            $o = new Mammal();
            break;
    }
    $list[] = $o;
}

now at somewhere else I would want to list them:

class Test
{
    public function dump(array $list)
    {
        foreach ($list as $o)
        {
            /**
             * @var Item $o
             */
            echo '<hr>';
            echo get_class($o).':<br>';
            $o->run();
            if ($o instanceof Mammal)
            {
                $o->born();
            }
            if ($o instanceof Reptile)
            {
                $o->putEgg();
            }
        }
    }
}

(new Test())->dump($list);

now my problem is now Test class is coupled to Item and all of its descendants. If I refactor the whole like that:

abstract class Item
{
    public function run()
    {
        echo __FUNCTION__.'<br>';
    }

    public function isReptile()
    {
        return $this instanceof Reptile;
    }

    public function isMammal()
    {
        return $this instanceof Mammal;
    }
}

class Reptile extends Item
{
    public function putEgg()
    {
        echo __FUNCTION__.'<br>';
    }
}

class Mammal extends Item
{
    public function born()
    {
        echo __FUNCTION__.'<br>';
    }
}

$list = [];
for ($i = 1; $i <= 10; $i++)
{
    switch(mt_rand(1,2))
    {
        case 1 :
            $o = new Reptile();
            break;
        case 2 :
            $o = new Mammal();
            break;
    }
    $list[] = $o;
}

//
class Test
{
    public function dump(array $list)
    {
        foreach ($list as $o)
        {
            /**
             * @var Item $o
             */
            echo '<hr>';
            echo get_class($o).':<br>';
            $o->run();
            if ($o->isMammal())
            {
                $o->born();
            }
            if ($o->isReptile())
            {
                $o->putEgg();
            }
        }
    }
}

(new Test())->dump($list);

a bit looks better since now Test and Item dependencies are eliminated. Still it smells because of isMammal(), isReptile()... it means every time a new type is born, Item should be updated. Nevertheless, its a bad practice that a base class know about its descendants. What is the elegant way?

John Smith
  • 6,129
  • 12
  • 68
  • 123
  • 1
    You need some method with a __same__ name which is available in both child classes. Something like `makeChildren`. More strictly you can create an interface `makeChildren` and both classes should implement it. – u_mulder Aug 04 '17 at 08:54
  • 1
    Just remove helper methods... No? Use `instanceof` in `Test::dump()` instead. End source should only be aware of classes it's actually using. Also you may consider abstraction through interfaces. – BlitZ Aug 04 '17 at 08:54
  • 1
    If you do not like to use class coupling, then only interfaces is proper way, I believe. – BlitZ Aug 04 '17 at 09:00
  • @BlitZ why is interface coupling better? – John Smith Aug 04 '17 at 10:20
  • 1
    Because you not required to use exact classes. Interfaces is a type of abstraction that requires to match by structure and not exact particular class. Which means, that not matter how much classes you have, they all must be compatible with actions, which you require from them. It is more flexible. A lot of framework core components build on base of this feature to improve scalability. – BlitZ Aug 04 '17 at 10:43
  • 1
    If you check classes by `instanceof` - it will give `true` only in case if variable contains exact class instance. If you checking interfaces with `instanceof` - it will give `true` to any class, that implements exact interface. There might be a ton of classes, that can use one interface. So it allows greater abilities in the end. – BlitZ Aug 04 '17 at 10:50

3 Answers3

2

Use an Interface

Define an interface and make sure all animals implement that.

interface Animal {
    public function born();
}

Now all the animals have to implement this and implement the functions defined in interface.

class Reptile implement Animal {
  public function born()
  {
     return 'new baby reptile';
  }
}

class Mammal implement Animal {
   public function born()
   {
     return 'new baby mammal';
   }
}

class Test {
   public function makeBaby(Animal $animal)
   {
      echo $animal->born();
   }
}

(new Test())->makeBaby(new Reptile());
(new Test())->makeBaby(new Mammal());
PayamB
  • 123
  • 1
  • 6
1

I believe, you don't wish end method to grow in vertical size. I suggest to use interfaces to analyze Item structure, rather than it's class.

<?php
/*
 * Interfaces first.
 * They will allow us to build a "contract" between calling class and
 * actual implementations. Also, only interfaces MUST be used in end class.
 */
interface ViviparousInterface
{
    public function giveBirth();
}

interface OviparousInterface
{
    public function layEgg();
}

interface SpawningInterface
{
    public function layCaviar();
}

/*
 * Now implemetation classes:
 */
abstract class Item
{
    public function run()
    {
        echo __FUNCTION__ . '<br>';
    }
}

class Reptile extends Item implements OviparousInterface
{
    public function layEgg()
    {
        echo __FUNCTION__ . '<br>';
    }
}

class Mammal extends Item implements ViviparousInterface
{
    public function giveBirth()
    {
        echo __FUNCTION__ . '<br>';
    }
}

class Fish extends Item implements SpawningInterface
{
    public function layCaviar()
    {
        echo __FUNCTION__ . '<br>';
    }
}

class ShomethingElse extends Item implements ViviparousInterface
{
    public function giveBirth()
    {
        echo __FUNCTION__ . '<br>';
    }
}

/**
 * Test class:
 */
class Test
{
    public function dump(array $list)
    {
        foreach ($list as $o)
        {
            /**
             * @var Item $o
             */
            echo '<hr>', get_class($o) . ':<br>';

            $o->run();

            /*
             * Here we do not care about actual classes.
             * We do know, that if they implement one of the interfaces,
             * then they will have required methods.
             */
            if ($o instanceof ViviparousInterface) {
                $o->giveBirth();
            } elseif ($o instanceof OviparousInterface) {
                $o->layEgg();
            } elseif ($o instanceof SpawningInterface) {
                $o->layCaviar();
            }
        }
    }
}

/*
 * Test case:
 */
$list = [];

for ($i = 1; $i <= 10; $i++)
{
    switch(mt_rand(1, 4))
    {
        case 1:
            $o = new Reptile();
            break;

        case 2:
            $o = new Mammal();
            break;

        case 3:
            $o = new Fish();
            break;

        case 4:
            $o = new ShomethingElse();
            break;
    }

    $list[] = $o;
}

(new Test())->dump($list);

In the end, no matter how much actual Item descendants you will have in the future, your Test:dump() method will use only class structure analysis. It will dramatically reduce further size grow.

Further reading:

  1. What is the point of interfaces in PHP?
  2. Build seven good object-oriented habits in PHP
BlitZ
  • 12,038
  • 3
  • 49
  • 68
  • but it still have dependencies (instanceof Interface) – John Smith Aug 04 '17 at 10:16
  • 1
    @JohnSmith Well, you will have them anyway here, because you executing some kind of actions, according to class or it's structure. In this case, you will have less of them. You can move this part to other class method to improve transparency, but you still will have them. Interfaces is not a classes, so you will not have to increase size of the method, when new `Item` descendant will show up. So I think it is more suitable and acceptable. No ? – BlitZ Aug 04 '17 at 10:24
0

I found a good workaround. I will have homogen lists:

$listReptiles = [];
$listMammals = [];
for ($i = 1; $i <= 10; $i++)
{
    switch(mt_rand(1,2))
    {
        case 1 :
            $listReptiles[] = new Reptile();
            break;
        case 2 :
            $listMammals[] = new Mammal();
            break;
    }
}

what do you think?

John Smith
  • 6,129
  • 12
  • 68
  • 123
  • 1
    I think you overreacting necessity to remove dependencies. It is good to have less dependencies, but in the end code is depending on other code. So it is clear, that dependencies will be anyway. If this solution fits your requirements, then you should do as your developer sense suggests. Still, interfaces in your case is one of the proper decisions and only you can choose decision, which really required by situation. Good luck with it. Also, in any case be ready to accept consequences. – BlitZ Aug 04 '17 at 19:37