6
\$\begingroup\$

I've been reading up on OOP concepts such as: polymorphism, inheritance and encapsulation and would like to know if I am even close to using these concepts correctly.

interface Canine
{
    public function bark();
    public function howl();
}

interface Feline
{
    public function chase();
}

interface Mammal
{
    const GENDER_MALE = 'male';
    const GENDER_FEMALE = 'female';

    public function isMale();
    public function isFemale();
}

abstract class Animal
{
    public function breath()
    {
        return 'I breath oxygen found in the air using my nose.';
    }
}

class Dog extends Animal implements Canine, Mammal
{
    private $name;
    private $gender;

    public function __construct($name, $gender)
    {
        $this->setName($name);
        $this->setGender($gender);
    }

    private function setName($name)
    {
        $this->name = (string) $name;
    }

    private function setGender($gender)
    {
        if ($gender === self::GENDER_MALE || $gender === self::GENDER_FEMALE) {
            $this->gender = $gender;
        } else {
            throw new InvalidArgumentException('Mammals can only be male or female');
        }
    }

    public function getGender()
    {
        return $this->gender();
    }

    public function eat()
    {
        return 'I eat meat.';
    }

    public function bark()
    {
        return 'Woof, woof!';
    }

    public function howl()
    {
        return 'AHROOOOOOO WOO WOOO!';
    }

    public function isMale()
    {
        return $this->gender === self::GENDER_MALE;
    }

    public function isFemale()
    {
        return $this->gender === self::GENDER_FEMALE;
    }
}

class Lion extends Animal implements Mammal, Feline
{
    private $gender = self::GENDER_MALE;

    public function chase()
    {
        return 'I am chasing a Gazelle!';
    }

    public function isMale()
    {
        return $this->gender === self::GENDER_MALE;
    }

    public function isFemale()
    {
        return $this->gender === self::GENDER_FEMALE;
    }

    public function eat()
    {
        return 'I am now eating the gazelle I just caught.';
    }
}

class Fish extends Animal
{
    final public function breath()
    {
        return 'I breath oxygen found in the water using my gills.';
    }
}

class Client
{
    public function __construct()
    {
        $dog = new Dog('Sparky', Mammal::GENDER_MALE);
        $cat = new Lion;
        $fish = new Fish;

        $this->setupFeline($cat);
        $this->setupCanine($dog);

        $this->setupAnimal($cat);
        $this->setupAnimal($dog);
        $this->setupAnimal($fish);
    }

    public function setupFeline(Feline $cat)
    {
        echo 'Feline: ' . $this->iAmA($cat), ' and ',$cat->chase(), '&nbsp;', $cat->eat() . '<br>';
    }

    public function setupCanine(Canine $dog)
    {
        echo 'Canine: ' . $this->iAmA($dog), '&nbsp;', $dog->howl() . '<br>';
    }

    public function setupAnimal(Animal $animal)
    {
        echo $this->iAmA($animal) . ' and ' . $animal->breath() . '<br>';
    }

    private function iAmA(Animal $animal)
    {
        return 'I am a ' . get_class($animal);
    }
}

$client = new Client;
\$\endgroup\$

migrated from programmers.stackexchange.com Apr 24 '15 at 13:42

This question came from our site for professionals, academics, and students working within the systems development life cycle.

3
\$\begingroup\$

I agree with Gerard a little, you have made (in my eyes) a common error with interfaces where you've tried to make them specific to a certain type rather than what they actually do, I see this a lot in the wild (excuse the pun). This isn't to say that interfaces based on type are bad, they have their place but in general an abstract would be better suited for types as types will have unique properties that you could not declare in an interface (nor would you want to).

As an example I'll elaborate on the "Dog is also capable of chasing" example. Declaring a Dog as Feline makes no sense when trying to describe what a Dog is, instead, make it capable of doing something.

/**
 * Any animal that chases after things should implement
 * this interface.
 */
interface Chaser {
    /**
     * GET THE RABBIT!
     * @return void
     */
    public function chase();
}

On the flipside, you could easily have an interface such as:

/**
 * Any animal that is chased by another implements this
 */
interface Chasable {
    /**
     * RUN RABBIT!
     * @return void
     */
    public function beingChased();
}

So you now have interfaces that describe what they do. It now makes sense that both a Dog and a Lion can implement Chaser. Not only this, any animal class you create are capable of using it without semantic issues.

Your Canine interface suffers from the same problem (to a lesser degree). Having a canine interface is probably not a bad thing, declaring that canines only bark or howl is true. Is it true to say that no other animal species can bark or howl? what happens when this cat turns up on your doorstep? (stupid example but I found it amusing)

Cat videos aside, we now have a strange edge case where a cat is displaying canine behaviour, but it is not a canine. Enter the Barker interface (that I find both funny and embarrassing).

/**
 * Any animal that barks can extend this interface
 */
interface Barker {
    /**
     * WOOOF!!
     * @return void
     */
    public function bark();
}

Going into the nuances of having a single Cat that can bark out of a set of possibly hundreds of Cat instances is beyond what you're trying to learn but I'm hoping you get the gist of where I'm going with this, behaviour as interfaces generally work better than types and give you a lot more flexibility when actually determining what your classes do.

I'm making general statements a lot here, but you will see that your interfaces become very specific to actions. As such they are smaller and make more sense. I have found it much easier to have a class that implements 5 smaller interfaces than 1 or 2 monolithic interfaces due to the increased flexibility available and it keeps things DRY.

class Canine extends Mammal implements Chaser, Barker, Howler
{
    // I am a Canine. I am a mammal. I do things that all mammals do
    // (that also includes being an animal). 
    // I can chase, bark and howl. I know this before I've written
    // any methods because my parent and interfaces say so.
}

Some tips for you moving forward:

  • Getters and setters are good, but can clog up your class with menial methods that achieve very little when you are not modifying data passed in or out.
  • Interfaces work best when they describe what something should do.
  • Don't rely on just interfaces to achieve what you need. I'd recommend getting stuck into common OOP design patterns sooner rather than later.

Example of the chaser and chasable interfaces in action:

First lets make our interfaces a little more rounded

/**
 * Any animal that chases after things should implement
 * this interface.
 */
interface Chaser {
    /**
     * GET THE CHASABLE ANIMAL!
     * @param Chasable
     * @return string
     */
    public function chase(Chasable $chasable);
}

/**
 * Any animal that chases after things should implement
 * this interface.
 */
interface Chasable {
    /**
     * RUN FROM THE CHASER!
     * @param Chaser
     * @return string
     */
    public function beingChased(Chaser $chaser);
}

Now let's create the chaser and the chasable animals.

/**
 * A chaser animal
 */
class Dog implements Chaser {
    /**
     * GET THE CHASABLE ANIMAL!
     * @param Chasable
     * @return string
     */
    public function chase(Chasable $chasable)
    {
        return "I'm chasing a {get_class($chasable)}!"
    }
}

class Rabbit implements Chasable {
    /**
     * RUN FROM THE CHASER!
     * @param Chaser
     * @return string
     */
    public function beingChased(Chaser $chaser)
    {
        return "Help! I'm being chased by a {get_class($chaser)}"
    }
}

Start the chase!

$dog = new Dog();
$rabbit = new Rabbit();

//We know the dog is a chaser and the rabbit is a chasable
//So we know that this will work
echo $dog->chase($rabbit);

//We know the rabbit is a chasable and the dog is a chaser
//So we know that this will work
echo $rabbit->beingChased($dog);
\$\endgroup\$
  • \$\begingroup\$ Truly loved your answer! I was able to understand instantly what I had done wrong and the umm... barking cat... was interesting too. \$\endgroup\$ – Nathan Bishop Apr 26 '15 at 11:20
  • \$\begingroup\$ Glad you found it useful, I didn't put the points in mentioned by @Gerard. They are pertinent to this as well. The cat... yes... who knew! \$\endgroup\$ – David Barker Apr 26 '15 at 11:41
1
\$\begingroup\$
  1. Specify one method in terms of another. For example, you can define isMale() as !$this->isFemale(). Strickly speaking, I acknowledge that in this case an animal may have 3 options (hermaphrodite).
  2. Move the gender property to your abstract Animal class.
  3. setupAnimal is a misleading name. Consider an appropriate verb+noun name, e.g., printAnimalname
  4. In the line $this->name = (string) $name; I'm unsure why you would need a string cast.
  5. Instead of all your setupXXX functions, consider overloading the __toString method, or some other method shared by all Animal instances.

I've seen dogs chase things, too. By your PHP logic, I should declare a dog as feline. My point is: perhaps you can find a less contrived example to practice with. The "animal" thing quickly turns into a study of biology instead of well applied OOP.

\$\endgroup\$
  • \$\begingroup\$ Sorry, I was using the Client class for something else on my local webserver, I must have included it by accident. Thanks for the pointers. \$\endgroup\$ – Nathan Bishop Apr 25 '15 at 0:10
  • \$\begingroup\$ I am removing this as the answer. While this helped me out a bit, the answer given by @David explained in far greater detail exactly what I did wrong and how to implement a more "SOLID" foundation for my animal classes. \$\endgroup\$ – Nathan Bishop Apr 26 '15 at 11:13

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.