DEV Community

Cover image for Interfaces and Abstract Classes
Rodrigo Vieira
Rodrigo Vieira

Posted on

Interfaces and Abstract Classes

For most people I've talked on my journey through software development, abstraction always brings the idea of code reuse. You might think about company and person objects, both have names, addresses, phone contacts, and so on. They both share common attributes and maybe even common behavior. You can define an abstract base class named Entity and then create two concrete classes, Person and Company, extended from Entity.

Let's take a step back, imagine basic PHP types, boolean, integer, float, objects and all the ways you are able to work with them. Everyone knows or at least should know, the expected behavior of those types. I mean, if you create an integer variable, you know what kind of value to expect and what kind of operations you can perform with it.

Every type has its own stable behavior.

A programming language cares itself about the unchangeable behavior of its essential types. The same idea applies when we are defining abstract classes and their concrete implementations.

Abstract Classes

When extending from other classes, we are creating new data types. The code snippets below define a new child class ArrayCache, which is a AbstractCache class. Thus, the child class is a specification that will inherit all of the parent's non-private properties and methods. The client code still expects them to behave exactly as their parent does:

abstract class AbstractCache
{
    abstract public function get(string $key)
    { }

    abstract public function set(string $key, $value)
    { }
}
class ArrayCache extends AbstractCache
{
    protected $items;

    public function get(string $key)
    {
        return $this->item[$key];
    }

    public function set(string $key, $value)
    {
        $this->item[$key] = $value;
    }
}

According to Liskov Substitution Principle, we can use these sub-types, without any special knowledge, only by reading the parent class public interface.

All concrete implementations from the same abstract base class should obey the abstract class interfaces. When using inheritance, it is our responsibility to respect the parent class interface. When working with inheritance, try to keep the hierarchy short, no more than three levels. Also, remember that if there is some code in the parent class that you are using from the client one as parent::method(), you must care about the returned values as well.

When we provide public methods we are saying: "With this data type you can do this and it will behave in this particular way."

You can think of abstraction as taking out unnecessary details to describe something in simpler terms in order to focus only on important aspects of the current context. Encapsulation plays the main role here, as a technique used to hide some state and behavior from the outside world. Client code can interact with it through its methods, but cannot access its state directly.

In other words:

A public interface is a set of rules, that defines the way in which we can interact with an object.

Interfaces

When you are trying to enforce the same behavior for different types, you may want to take advantage of Interfaces.

An interface in PHP can contain methods and constants, but can't contain any variables. All methods must be public and have no implementation. Also, an interface can be inherited from another by extends keyword. Even though classes can inherit from multiple interfaces, that can't implement two interfaces that share the same method names, it would cause ambiguity.

Hence, we usually use abstraction for code reuse when defining a hierarchy of data types. As for interfaces, its common use is to enforce certain behavior.

For instance, we want to make sure that those two types, Company and Person can be cached by providing its own key and its own value:

interface Cacheable
{
    public function getCacheKey();
    public function getCacheValue();
}

We have to create the Company class and the methods imposed by Cacheable interface:

class Company extends Entity implements Cacheable
{
    public function getCacheKey()
    {
        return 'company_' . $this->id;
    }

    public function getCacheValue()
    {
       return [
           'name' => $this->name,
           'phone' => $this->phone,
           'address' => $this->address,
           'foundation_date' => $this->foundation_date
       ];
    }
}

The same for Person class:

class Person extends Entity implements Cacheable
{
    public function getCacheKey()
    {
        return 'person_' . $this->id;
    }

    public function getCacheValue()
    {
       return [
           'name' => $this->name,
           'phone' => $this->phone,
           'address' => $this->address,
           'birth_date' => $this->birth_date
       ];
    }
}

With this inverted approach, all caching logic is encapsulated in a cacheable class. Every class must define its own rules, how it should be stored in the cache. The Cache class knows nothing about these details, it trusts Cacheable objects and calls their getCacheKey and getCacheData methods.

This blind trust is the main idea of using interfaces. We are establishing a contract that must be honored by both sides.

We can now redefine our ArrayCache like these:

class AbstractCache extends AbstractCache
{
    protected $items;

    abstract public function get(Cacheable $entity)
    {
        $key = $entity->getCacheKey();
        return $this->item[$key];
    }

    abstract public function set(Cacheable $entity, $value)
    {
        $key = $entity->getCacheKey();
        $data = $entity->getCacheValue();

        $this->item[$key] = $value;
    }
}

Keep in mind that interfaces don't define a new type, they describe an aspect of a type.

References

Top comments (0)