Core points
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:
<?php namespace LibraryCache; interface CacheInterface { public function set($id, $data); public function get($id); public function delete($id); public function exists($id); }
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:
<?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); } }
. 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
<?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); } }
. 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
<?php namespace LibraryCache; interface CacheInterface { public function set($id, $data); public function get($id); public function delete($id); public function exists($id); }
<?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); } }
<?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); } }
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:
<?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); }
<?php namespace LibraryCache; class FileCache extends AbstractCache { // the same implementation goes here }
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()
<?php namespace LibraryCache; class ApcCache extends AbstractCache { // the same implementation goes here }
class to the view: ApcCache
<?php namespace LibraryView; interface ViewInterface { public function setTemplate($template); public function __set($field, $value); public function __get($field); public function render(); }
<?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()); } } }
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)
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.
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.
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.
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.
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.
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.
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.
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.
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.
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!