Home >Backend Development >PHP Tutorial >Subtype Polymorphism - Swapping Implementation at Runtime

Subtype Polymorphism - Swapping Implementation at Runtime

尊渡假赌尊渡假赌尊渡假赌
尊渡假赌尊渡假赌尊渡假赌Original
2025-02-25 18:15:16612browse

Subtype Polymorphism - Swapping Implementation at Runtime

Core points

  • Subtype polymorphism in object-oriented design refers to the ability of a system to define a set of contracts or interfaces, and then implement them by different subtypes. This is crucial for designing scalable systems that can consume specific contracts without checking whether the implementer is in the expected type.
  • This article demonstrates the use of subtype polymorphisms by developing an insertable cache component that can be extended to suit user needs by developing additional cache drivers.
  • A key feature of the cache component is its ability to swap different cache drivers at runtime without changing any client code. This is achieved by defining a cache contract that is then followed by different implementations, thus taking advantage of the polymorphism.
  • The cache component can switch backends at runtime, highlighting the importance of polymorphism in designing highly decoupled modules. This allows easy reconnection at runtime without causing vulnerability or rigid-related issues in other parts of the system.
  • Subtype polymorphism not only makes the system more orthogonal and easier to scale, but it is also less likely to violate core paradigms such as the open/closed principle and the "interface-oriented programming" principle. It is a fundamental aspect of object-oriented programming that allows for flexibility and reusability of code.

Many people may doubt the correlation between inheritance and polymorphism in object-oriented design? Probably few, most of them may be due to ignorance or narrow thinking. But there is a small problem here that cannot be ignored. While it is simple to understand the logic of inheritance, things become more difficult when delving into the details of polymorphism. The term “polymorphism” is daunting in itself, with its academic definition full of different perspectives, which makes it even harder to understand what is actually behind it. Peripheral concepts such as parameter polymorphism and ad hoc polymorphism (usually implemented by method override/overload) do have significant applications in some programming languages, but in design, they can consume specific contracts (reads) When an abstract) extensible system, the last case should be abandoned without checking whether the implementer is of the expected type. In short, most of the time, any general reference to polymorphism in object-oriented programming is implicitly considered as a system-explicit capability used to define a set of contracts or interfaces that Or the interface is followed by different implementations. This "canonical" polymorphism is often referred to as a subtype polymorphism, because the implementor of an interface is considered to be a subtype of them, regardless of whether there is an actual hierarchy. As one might expect, understanding the nature of polymorphism is only half of the learning process; the other half is of course demonstrating how to design a polymorphic system so that it can adapt to a rather realistic situation without falling into just showing “some pretty teaching” code” (in many cases, it’s a cheap euphemism for toy code). In this article, I will show you how to take advantage of the benefits provided by polymorphism by developing an insertable cache component. The core functionality can later be extended to suit your needs by developing additional cache drivers.

Define the interface and implementation of components

The menu of options to choose from is by no means absent when building extensible cache components (if you are skeptical about this, just look at what's behind some popular frameworks). However, here, the components I provide have the clever ability to swap different cache drivers at runtime without modifying any client code. So, how can you do this without much effort during the development process? Well, the first step should be...yes, define an isolated cache contract that will be followed by different implementations later, thereby taking advantage of the benefits of polymorphism. At its most basic level, the above contract is as follows:

<code class="language-php"><?php namespace LibraryCache;

interface CacheInterface
{
    public function set($id, $data);
    public function get($id);
    public function delete($id);
    public function exists($id);
}</code>

CacheInterface Interface is a skeleton contract that abstracts the behavior of common cache elements. With the interface, you can easily create some specific cache implementations that conform to their contracts. Since I want to keep it simple and easy to understand, the cache driver I set up will be just a lean duo: the first one uses the file system as the underlying backend for cache/get data, while the second one uses the APC extension behind the scenes. The following is a file-based cache implementation:

<code class="language-php"><?php namespace LibraryCache;

class FileCache implements CacheInterface
{
    const DEFAULT_CACHE_DIRECTORY = 'Cache/';
    private $cacheDir;

    public function __construct($cacheDir = self::DEFAULT_CACHE_DIRECTORY) {
        $this->setCacheDir($cacheDir);
    }

    public function setCacheDir($cacheDir) {
        if (!is_dir($cacheDir)) {
            if (!mkdir($cacheDir, 0644)) {
                throw InvalidArgumentException('The cache directory is invalid.');
            }
        }
        $this->cacheDir = $cacheDir;
        return $this;
    }

    public function set($id, $data) {
        if (!file_put_contents($this->cacheDir . $id, serialize($data), LOCK_EX)) {
            throw new RuntimeException("Unable to cache the data with ID '$id'.");
        }
        return $this;
    }

    public function get($id) {
        if (!$data = unserialize(@file_get_contents($this->cacheDir . $id, false))) {
            throw new RuntimeException("Unable to get the data with ID '$id'.");
        }
        return $data;
    }

    public function delete($id) {
        if (!@unlink($this->cacheDir . $id)) {
            throw new RuntimeException("Unable to delete the data with ID '$id'.");
        }
        return $this;
    }

    public function exists($id) {
        return file_exists($this->cacheDir . $id);
    }
}</code>
The driving logic of the class should be easy to understand. The most relevant thing here so far is that it exposes a neat polymorphic behavior, as it faithfully achieves the early

. While this ability is sweet and charming, for its part, I wouldn't appreciate it considering the goal here is to create a cache component that can switch backends at runtime. Let us put in extra effort for teaching purposes and bring another streamlined implementation of FileCache to life. The following implementation complies with the interface contract, but this time it is by using APC to extend the bundling method: CacheInterface CacheInterface

<code class="language-php"><?php namespace LibraryCache;

class ApcCache implements CacheInterface
{
    public function set($id, $data, $lifeTime = 0) {
        if (!apc_store($id, $data, (int) $lifeTime)) {
            throw new RuntimeException("Unable to cache the data with ID '$id'.");
        }
    }

    public function get($id) {
        if (!$data = apc_fetch($id)) {
            throw new RuntimeException("Unable to get the data with ID '$id'.");
        } 
        return $data;
    }

    public function delete($id) {
        if (!apc_delete($id)) {
            throw new RuntimeException("Unable to delete the data with ID '$id'.");
        }
    }

    public function exists($id) {
        return apc_exists($id);
    }
}</code>
Class isn't the most dazzling APC wrapper you've ever seen in your career, it packs all the features you need to save, retrieve and delete data from memory. Let's applaud ourselves, because we have successfully implemented a lightweight cache module whose specific backend is not only easy to swap at runtime due to its polymorphism, but it is also extremely easy to add more backends in the future. Just write another implementation that complies with

. However, I should emphasize that the actual subtype polymorphism is achieved by implementing contracts defined through interface construction, which is a very common approach. However, nothing can stop you from being less orthodox and get the same result by switching an interface declared as a set of abstract methods (located in an abstract class). If you feel risky and want to go that bypass, you can reconstruct the contract and the corresponding implementation as follows: ApcCache

<code class="language-php"><?php namespace LibraryCache;

interface CacheInterface
{
    public function set($id, $data);
    public function get($id);
    public function delete($id);
    public function exists($id);
}</code>
<code class="language-php"><?php namespace LibraryCache;

class FileCache implements CacheInterface
{
    const DEFAULT_CACHE_DIRECTORY = 'Cache/';
    private $cacheDir;

    public function __construct($cacheDir = self::DEFAULT_CACHE_DIRECTORY) {
        $this->setCacheDir($cacheDir);
    }

    public function setCacheDir($cacheDir) {
        if (!is_dir($cacheDir)) {
            if (!mkdir($cacheDir, 0644)) {
                throw InvalidArgumentException('The cache directory is invalid.');
            }
        }
        $this->cacheDir = $cacheDir;
        return $this;
    }

    public function set($id, $data) {
        if (!file_put_contents($this->cacheDir . $id, serialize($data), LOCK_EX)) {
            throw new RuntimeException("Unable to cache the data with ID '$id'.");
        }
        return $this;
    }

    public function get($id) {
        if (!$data = unserialize(@file_get_contents($this->cacheDir . $id, false))) {
            throw new RuntimeException("Unable to get the data with ID '$id'.");
        }
        return $data;
    }

    public function delete($id) {
        if (!@unlink($this->cacheDir . $id)) {
            throw new RuntimeException("Unable to delete the data with ID '$id'.");
        }
        return $this;
    }

    public function exists($id) {
        return file_exists($this->cacheDir . $id);
    }
}</code>
<code class="language-php"><?php namespace LibraryCache;

class ApcCache implements CacheInterface
{
    public function set($id, $data, $lifeTime = 0) {
        if (!apc_store($id, $data, (int) $lifeTime)) {
            throw new RuntimeException("Unable to cache the data with ID '$id'.");
        }
    }

    public function get($id) {
        if (!$data = apc_fetch($id)) {
            throw new RuntimeException("Unable to get the data with ID '$id'.");
        } 
        return $data;
    }

    public function delete($id) {
        if (!apc_delete($id)) {
            throw new RuntimeException("Unable to delete the data with ID '$id'.");
        }
    }

    public function exists($id) {
        return apc_exists($id);
    }
}</code>

From top to bottom, this is indeed a polymorphic approach, which is against the previously discussed method. Personally, this is just my personal statement, I prefer to use interface constructs to define contracts and use abstract classes only when encapsulating boilerplate implementations shared by several subtypes. You can choose the method that best suits your needs. At this point, I could put down the curtain, write some fancy ending comments, boast about our impressive coding skills, and brag about the flexibility of our cache components, but that would be a slouch on us. When there are client code that can consume multiple implementations, polymorphisms exhibit its most tempting aspects without checking whether these implementations are instances of some type, as long as they meet the expected contract. So let's reveal the aspect by connecting the cache component to a basic client view class, which will allow us to do some neat HTML caching effortlessly.

Put the cache driver into use

Caching HTML output via our example cache module is very simple and I will save any lengthy explanations at other times. The entire cache process can be simplified into a simple view class, similar to the following:

<code class="language-php"><?php namespace LibraryCache;

abstract class AbstractCache
{
    abstract public function set($id, $data);
    abstract public function get($id);
    abstract public function delete($id);
    abstract public function exists($id);
}</code>
<code class="language-php"><?php namespace LibraryCache;

class FileCache extends AbstractCache
{
    // the same implementation goes here
}</code>
The most dazzling guy is the class constructor, which uses the early implementers of

and the CacheInterface method. Since the last method's responsibility is to cache the view's template after it is pushed to the output buffer, it would be nice to take advantage of this capability and cache the entire HTML document. Assume that the default template of the view has the following structure: render()

<code class="language-php"><?php namespace LibraryCache;

class ApcCache extends AbstractCache
{
    // the same implementation goes here 
}</code>
Now, let's have a little fun and cache the document by providing an instance of the

class to the view: ApcCache

<code class="language-php"><?php namespace LibraryView;

interface ViewInterface
{
    public function setTemplate($template);
    public function __set($field, $value);
    public function __get($field);
    public function render();
}</code>
It's very good, right? But wait! I was so excited that I forgot to mention that the above code snippet would explode on any system that does not have the APC extension installed (naughty system administrator!). Does this mean that the carefully crafted cache module is no longer reusable? This is exactly where the file-based driver comes into play, which can be put into the client code without receiving any complaints:

<code class="language-php"><?php namespace LibraryView;
use LibraryCacheCacheInterface;

class View implements ViewInterface
{
    const DEFAULT_TEMPLATE = 'default';    
    private $template;
    private $fields = array();
    private $cache;

    public function __construct(CacheInterface $cache, $template = self::DEFAULT_TEMPLATE) {
        $this->cache = $cache;
        $this->setTemplate($template);
    }

    public function setTemplate($template) {
        $template = $template . '.php';
        if (!is_file($template) || !is_readable($template)) {
            throw new InvalidArgumentException(
                "The template '$template' is invalid.");   
        }
        $this->template = $template;
        return $this;
    }

    public function __set($name, $value) {
        $this->fields[$name] = $value;
        return $this;
    }

    public function __get($name) {
        if (!isset($this->fields[$name])) {
            throw new InvalidArgumentException(
                "Unable to get the field '$field'.");
        }
        return $this->fields[$name];
    }

    public function render() {
        try {
            if (!$this->cache->exists($this->template)) {
                extract($this->fields);
                ob_start();
                include $this->template;
                $this->cache->set($this->template, ob_get_clean());
            }
            return $this->cache->get($this->template);
        }
        catch (RuntimeException $e) {
            throw new Exception($e->getMessage());
        } 
    }
}</code>
The above single line of code explicitly states that the view will use the file system instead of shared memory to cache its output. This dynamic switching cache backend briefly illustrates why polymorphism is so important when designing highly decoupled modules. It allows us to easily reconnect things at runtime without spreading vulnerability/rigidity-related artifacts to other parts of our system.

Conclusion

Polymorphism is indeed one of those good things in life, and once you understand it, it makes you wonder how you can do without Its case continues for so long. Polymorphic systems are inherently more orthogonal, easier to scale, and less prone to violating core paradigms such as the open/closed principle and the wise “interface-oriented programming” principle. Although rather primitive, our cache module is a prominent example of these advantages. If you haven't refactored your application to take advantage of the benefits of polymorphism, you'd better hurry up because you missed the jackpot! Pictures from Fotolia

FAQs about subtype polymorphisms (FAQ)

What are the main differences between subtype polymorphisms and parameter polymorphisms?

Subtype polymorphism, also known as inclusion polymorphism, is a form of polymorphism in which a name represents instances of many different categories that are associated by a public superclass. Parameter polymorphism, on the other hand, allows a function or data type to process a value in the same way without relying on its type. Parameter polymorphism is a way to make a language more expressive while maintaining full static type safety.

How does subtype polymorphism work in Java?

In Java, subtype polymorphism is achieved by using inheritance and interfaces. Superclass reference variables can point to subclass objects. This allows Java to decide which method to call at runtime, which is called dynamic method scheduling. It is one of the powerful features of Java that enables it to support dynamic polymorphism.

Can you provide an example of subtype polymorphism?

Of course, let's consider a simple example in Java. Suppose we have a superclass called "Animal" and two subclasses "Dog" and "Cat". Both the "Dog" and "Cat" classes rewrite the "sound" method of the "Animal" class. Now, if we create an "Animal" reference pointing to a "Dog" or "Cat" object and call the "sound" method, Java will decide at runtime which class's "sound" method to call. This is an example of subtype polymorphism.

What is the significance of subtype polymorphism in programming?

Subtype polymorphism is a fundamental aspect of object-oriented programming. It allows for flexibility and reusability of the code. Using subtype polymorphism, you can design a common interface for a set of classes and then use this interface to interact with the objects of those classes in a unified way. This will result in cleaner, more intuitive and easier to maintain code.

What is the relationship between subtype polymorphism and Liskov replacement principle?

Liskov Substitution Principle (LSP) is a principle of object-oriented design that states that if a program is using a base class, it should be able to use any of its subclasses without the program knowing it. In other words, objects of superclasses should be able to be replaced by objects of subclasses without affecting the correctness of the program. Subtype polymorphism is a direct application of LSP.

Does all programming languages ​​support subtype polymorphism?

No, not all programming languages ​​support subtype polymorphism. It is mainly a feature of statically typed object-oriented programming languages ​​such as Java, C, and C#. Dynamically typed languages ​​like Python and JavaScript have different forms of polymorphism, called duck types.

What is the difference between static polymorphism and dynamic polymorphism?

Static polymorphism, also known as compile-time polymorphism, is achieved through method overloading. The decision about which method to call is made at compile time. On the other hand, dynamic polymorphism, also known as runtime polymorphism, is implemented through method rewriting. The decision about which method to call is made at runtime. Subtype polymorphism is a dynamic polymorphism.

Can you explain the concept of up-conversion in subtype polymorphism?

Upconversion is a process of treating derived class objects as base class objects. It is a key aspect of subtype polymorphism. When you upconvert a derived class object, you can call any method defined in the base class. However, if the method is rewritten in the derived class, the rewrite version will be called.

What is down-conversion in the context of subtype polymorphism?

Down conversion is the opposite of up conversion. It is the process of converting superclass objects into subclasses. When you need to access methods that only exist in subclasses, you can use down conversion. However, downconversion can be dangerous because it can cause a ClassCastException if the object being converted does not actually have the type you are converting to.

How does subtype polymorphism promote reusability of code?

Subtype polymorphism allows us to write more general and reusable code. By using superclass references to interact with subclass objects, we can write code for various objects as long as they all belong to subclasses of the same superclass. This means we can add new subclasses without changing the code that uses superclasses, which makes our code more flexible and easier to maintain.

The above is the detailed content of Subtype Polymorphism - Swapping Implementation at Runtime. For more information, please follow other related articles on the PHP Chinese website!

Statement:
The content of this article is voluntarily contributed by netizens, and the copyright belongs to the original author. This site does not assume corresponding legal responsibility. If you find any content suspected of plagiarism or infringement, please contact admin@php.cn