Search code examples
phpoopsolid-principlesdesign-principles

Is checking existence of a method inside a class a violation of SOLID principles?


I have a class called Bird that accepts array of birds in the constructor. I am trying to implement a function inside it that will check if any of the birds is currently flying, keeping in mind that all code should comply with SOLID principles.

I have the following two classes (Parrot and Ostrich)

class Parrot extends FlyingBirds{}   
class Ostrich extends BirdDetail{}

BirdDetail Class

abstract class BirdDetail {

  protected $didNotSleepLastNight;

  public function __construct(bool $didNotSleepLastNight)
  {
    $this->didNotSleepLastNight= $didNotSleepLastNight;
  }

  public function didNotSleepLastNight(): bool
  {
    return $this->didNotSleepLastNight;
  }

}

FlyingBirds (Not all birds can fly, like ostrich)

abstract class FlyingBirds extends BirdDetail{

 protected $isFlyingNow;

 public function __construct(bool $didNotSleepLastNight, bool $isFlyingNow)
 {
    parent::__construct($didNotSleepLastNight);
    $this->isFlyingNow = $isFlyingNow;
 }

 public function isFlyingNow(): bool
 {
    return $this->isFlyingNow;
 }
}   

Then I have a class called Bird

class Bird
{
  private $details;

 public function __construct(array $details)
 {
    $this->details = $details;
 }

 public function didNotSleepLastNight(): bool
 {
    foreach ($this->details as $detail) {

        if ($detail->didNotSleepLastNight()) {
            return true;
        }
    }

    return false;
 }

 public function isFlyingNow(): bool
 {
    foreach ($this->details as $detail) {

        if ($detail->isFlyingNow()) 
        {
            return true;
        }
    }
    return false;
  }
}

Now I am passing instances of Parrot and Ostrich to Bird Constructor

$bird = new Bird([new Parrot(true, false), new Ostrich(false)]);     

if($bird->isFlyingNow())
{
 echo "Yes";
}
else
{
 echo "No";
}

The problem is The above code is giving me the following error

Fatal error: Uncaught Error: Call to undefined method Ostrich::isFlyingNow()

That is because Ostrich/BirdDetail class does not have a method called isFlyingNow.

The problem can be fixed by the replacing the isFlyingNow method in Birds class with the following code:

public function isFlyingNow(): bool
{

    foreach ($this->details as $detail) {

        if (method_exists($detail, 'isFlyingNow') && $detail->isFlyingNow())             
        {
            return true;
        }
    }

    return false;

}

Could you please tell me if the above fix is some sort of violation of SOLID principles? Or can the problem be solved in a better way?


Solution

  • Not really breaking SOLID, but simply not very well designed.

    For a neater approach, use interfaces.

    Since not all birds can fly, having a public facing method isFlying() for all birds seems rather wasteful.

    Maybe you could do something like this:

    Your base Bird class, where you'd define the common properties and methods for all birds.

    class Bird {
    }
    

    Then, different interfaces to describe different possible behaviours of Bird subclasses. Some birds fly, some swim, some talk, some sing, some dive, some can run, etc, etc.

    For the flyers:

    interface FlyingBird {
    
        function isFlying():bool;
    
        function takeOff();
    
        function land();
    
        function fly($bearing, $distance);
    
    }
    

    For the talkers:

    interface TalkingBird {
    
        function say($something);
    }
    

    The singers:

    interface SingingBird {
        function sing(array $notes);
    }
    

    The swimmers (you may need to differentiate those that swim on the surface of the water and those that can dive underneath the surface).

    interface SwimmingBird {
       function isSwimming(): bool;
       // etc
    }
    

    For the runners:

    interface RunningBird {
       function isRunning(): bool;
       // etc
    }
    

    Then you could have classes like Parrot (flies and talks, but doesn't do singing, running, or swimming)

    class Parrot extends Bird implements TalkingBird, FlyingBird {
        // todo: actual implementation
    }
    

    Or Ostrich (can run, but doesn't swim, sing, talk or fly):

    class Ostrich extend Birds implements RunningBird { /* implementation */}
    

    Or even Penguin (can swim, can't fly, can't run, can't sing, can't talk):

    class Penguin extends Bird implements SwimmingBird { /* implementation */ }
    

    Etc, etc. The whole @package Ornithology is taking shape.

    Users of these classes should check if the instances are implementing the appropriate interfaces:

    if ($birdInstance instanceof FlyingBird && $birdInstance->isFlying()) {
       echo "This bird is flying!";
    }
    

    To simplify the composing of these classes you may want to create some traits:

    E.g.

    trait FlyingBirdTrait {
    
        private $flying = false;
    
        function isFlying():bool {
           return $this->flying;
        }
    
        function takeOff() {
           $this->flying = true;
        }
    
        function land() {
           $this->flying = false;
        }
    
        function fly($bearing, $altitude, $distance) {
           if (!$this->isFlying()) {
               $this->takeOff();
           }
           // calculate new position;
    
        }
    }
    

    Which then classes like Parrot could use:

    class Parrot extends Bird implements TalkingBird, FlyingBird {
        uses FlyingBirdTrait;
        // rest of the implementation, etc;
    }