Key Takeaways
- The Layer Supertype Pattern is pivotal in multi-tiered systems for encapsulating common implementation across different classes, thereby promoting code reuse and reducing duplication.
- Implementing the Layer Supertype involves creating a shared base class that abstracts common logic and properties, which are then extended by more specific subclasses.
- This pattern aids in maintaining cleaner code architecture by allowing shared functionalities to be modified in a single place, thus enhancing maintainability and scalability.
- The Layer Supertype Pattern not only simplifies the codebase but also aligns well with the Single Responsibility Principle by segregating common behavior from class-specific behavior.
- While the pattern offers numerous benefits in reducing boilerplate and redundant code, it must be applied judiciously to avoid creating overly complex or large superclass structures that can be difficult to manage.
The Need for a Layer Supertype – Defining a Bloated Domain Model
It could be said that a Layer Supertype is the natural and selective evolution of “common” base types, only that the latter live and breathe inside the confines of a specific layer. This has a prolific niche in multi-tiered designs, where exploiting the functionality of a supertype is mostly an imperative need, not just a flippant decision. As usual, the most effective way to understand the pragmatism behind the pattern is through a few hands-on examples. So, say we need to build a simple Domain Model from scratch, responsible for defining a few basic interactions between some blog posts and their corresponding comments. In a cursory fashion, the model could be easily outlined as an anemic layer containing just a couple of skeletal classes modeling the posts and comments. The first domain class, along with its contract, could look like this:<?php
namespace Model;
interface PostInterface
{
public function setId($id);
public function getId();
public function setTitle($title);
public function getTitle();
public function setContent($content);
public function getContent();
public function setComment(CommentInterface $comment);
public function setComments(array $comments);
public function getComments();
}
<?php
namespace Model;
class Post implements PostInterface
{
protected $id;
protected $title;
protected $content;
protected $comments = array();
public function __construct($title, $content, array $comments = array()) {
$this->setTitle($title);
$this->setContent($content);
if (!empty($comments)) {
$this->setComments($comments);
}
}
public function setId($id) {
if ($this->id !== null) {
throw new BadMethodCallException(
"The ID for this post has been set already.");
}
if (!is_int($id) || $id < 1) {
throw new InvalidArgumentException(
"The post ID is invalid.");
}
$this->id = $id;
return $this;
}
public function getId() {
return $this->id;
}
public function setTitle($title) {
if (!is_string($title)
|| strlen($title) < 2
|| strlen($title) > 100) {
throw new InvalidArgumentException(
"The post title is invalid.");
}
$this->title = htmlspecialchars(trim($title),
ENT_QUOTES);
return $this;
}
public function getTitle() {
return $this->title;
}
public function setContent($content) {
if (!is_string($content) || strlen($content) < 2) {
throw new InvalidArgumentException(
"The post content is invalid.");
}
$this->content = htmlspecialchars(trim($content),
ENT_QUOTES);
return $this;
}
public function getContent() {
return $this->content;
}
public function setComment(CommentInterface $comment) {
$this->comments[] = $comment;
return $this;
}
public function setComments(array $comments) {
foreach ($comments as $comment) {
$this->setComment($comment);
}
return $this;
}
public function getComments() {
return $this->comments;
}
}
Driving the Post
class is banal logic that boils down to defining the data and behavior of a few basic post entries. It should be pretty simple to understand.
Now let’s make the model a little fatter by adding to it a class that spawns the comments associated with a particular blog entry. It’s contract and implementation look like this:
<?php
namespace Model;
interface CommentInterface
{
public function setId($id);
public function getId();
public function setContent($content);
public function getContent();
public function setAuthor($author);
public function getAuthor();
}
<?php
namespace Model;
class Comment implements CommentInterface
{
protected $id;
protected $content;
protected $author;
public function __construct($content, $author) {
$this->setContent($content);
$this->setAuthor($author);
}
public function setId($id) {
if ($this->id !== null) {
throw new BadMethodCallException(
"The ID for this comment has been set already.");
}
if (!is_int($id) || $id < 1) {
throw new InvalidArgumentException(
"The comment ID is invalid.");
}
$this->id = $id;
return $this;
}
public function getId() {
return $this->id;
}
public function setContent($content) {
if (!is_string($content) || strlen($content) < 2) {
throw new InvalidArgumentException(
"The content of the comment is invalid.");
}
$this->content = htmlspecialchars(trim($content),
ENT_QUOTES);
return $this;
}
public function getContent() {
return $this->content;
}
public function setAuthor($author) {
if (!is_string($author) || strlen($author) < 2) {
throw new InvalidArgumentException(
"The author is invalid.");
}
$this->author = $author;
return $this;
}
public function getAuthor() {
return $this->author;
}
}
Like Post
, the Comment
class is pretty naïve as well. But now with both classes in place, we can put the model to use. For example:
<?php
use LibraryLoaderAutoloader,
ModelPost,
ModelComment;
require_once __DIR__ . "/Library/Loader/Autoloader.php";
$autoloader = new Autoloader;
$autoloader->register();
$post = new Post(
"A sample post.",
"This is the content of the post."
);
$post->setComments(array(
new Comment(
"One banal comment for the previous post.",
"A fictional commenter"),
new Comment(
"Yet another banal comment for the previous post.",
"A fictional commenter")
));
echo $post->getTitle() . " " . $post->getContent() . "<br>";
foreach ($post->getComments() as $comment) {
echo $comment->getContent() . " " . $comment->getAuthor() .
"<br>";
}
That worked like a charm indeed! Consuming the model is a fairly straightforward process, requiring that you first create a few Post
objects and then hydrating them with the related comments. Yes, life is sweet and good. Well, so far it is, but things could definitely be much better!
Not that I want to spoil the magic of such a beautiful moment, but I must confess a slight shiver run down my spine each time I look at the implementations of the Post
and Comment
classes. While it isn’t a serious issue per se, some methods, such as setId()
and setContent()
, exhibit the typical symptoms of code duplication.
Solving this problem without sloppy isn’t so intuitive as it might seem at first glance because of a few logical concerns. First, although they share a semantic relationship with each other, each class effectively models different types of objects. Second, they implement disparate interfaces which means it’s somewhat hard to abstract away the logic without ending up with a clumsy hierarchy where the “IS-A” condition never holds true.
In this case in particular, we could just take a more liberal approach and think of Post and Comment as subtypes of a highly-generic AbstractEntity
supertype. In doing so, it would be pretty simple to put shared implementation inside the boundaries of an abstract class, hence making the definitions of the subtypes a lot thinner. Since the whole abstraction process would be only conducted in the domain layer, the hypothetical AbstractEntity
would be considered… yes, you guessed right, a Layer Supertype. Simple but nice, huh?
Moving Shared Implementation to a Single Domain Class – Creating a Layer Supertype
The process of creating a model supertype, at least in this case, can be viewed as a mixture of a few atomic and granular refactoring techniques, such as Extract SuperClass, Pull Up Field, and Pull Up Method all covered in depth by Martin Fowler in his book Refactoring: Improving the Design of Existing Code (if you don’t own a copy already, make sure to grab one ASAP). Let’s take the big plunge and refactor our clunky domain model by dropping into it the aforementionedAbstractEntity
class. Since in this specific situation the bulk of duplicated code appears through the methods of the domain classes that handle IDs and perform some basic validation on string-based fields, it’d be useful to delegate these responsibilities to the supertype and let the subtypes be focused on doing fewer, more narrowed tasks.
Based on this simplistic concept, a “fat” implementation of the aforementioned model supertype might look like this:
<?php
namespace Model;
class AbstractEntity
{
protected $id;
// Map calls to protected/private fields to mutators when
// defined. Otherwise, map them to the fields.
public function __set($field, $value) {
$this->checkField($field);
$mutator = "set" . ucfirst(strtolower($field));
method_exists($this, $mutator) && is_callable(array($this, $mutator))
? $this->$mutator($value)
: $this->$field = $value;
return $this;
}
// Map calls to protected/private fields to accessors when
// defined. Otherwise, map them to the fields.
public function __get($field) {
$this->checkField($field);
$accessor = "get" . ucfirst(strtolower($field));
return method_exists($this, $accessor) && is_callable(array($this, $accessor))
? $this->$accessor()
: $this->$field;
}
// Map calls to undefined mutators/accessors to the corresponding
// fields
public function __call($method, $arguments) {
if (strlen($method) < 3) {
throw new BadMethodCallException(
"The mutator or accessor '$method' is not valid for this entity.");
}
$field = lcfirst(substr($method, 3));
$this->checkField($field);
if (strpos($method, "set") === 0) {
$this->$field = array_shift($arguments);
return $this;
}
if (strpos($method, "get") === 0) {
return $this->$field;
}
}
// Make sure IDs are positive integers and assigned only once
public function setId($id) {
if ($this->id !== null) {
throw new BadMethodCallException(
"The ID for this entity has been set already.");
}
if (!is_int($id) || $id < 1) {
throw new InvalidArgumentException(
"The ID for this entity is invalid.");
}
$this->id = $id;
return $this;
}
public function getId() {
return $this->id;
}
// Get the entity fields as an array
public function toArray() {
return get_object_vars($this);
}
// Check if the given field exists in the entity
protected function checkField($field) {
if (!property_exists($this, $field)) {
throw new InvalidArgumentException(
"Setting or getting the field '$field' is not valid for this entity.");
}
}
// Validate and sanitize a string
protected function sanitizeString($value, $min = 2, $max = null) {
if (!is_string($value) || empty($value)) {
throw new InvalidArgumentException(
"The value of the current field must be a non-empty string.");
}
if (strlen($value) < (integer) $min || $max ? strlen($value) > (integer) $max : false) {
throw new InvalidArgumentException(
"Trying to assign an invalid string to the current field.");
}
return htmlspecialchars(trim($value), ENT_QUOTES);
}
}
Contrary to popular belief, bringing to life an abstract supertype which neatly encapsulates most of the logic shared by the model’ subtypes under its hood is actually an easy process that can be accomplished relatively quickly. In this case, I admit I became a bit overzealous as the supertype not only is capable of handling IDs and validating/sanitizing strings, but is also sprinkles in some PHP magic behind the scenes to map calls to private/protected properties to their corresponding mutators/accessors whenever possible. Providing the supertype with extra functionality like this right out of the box is entirely optional, so feel free include it or omit it in your own AbstractEntity
class as you see fit.
The next step is to refactor the implementations of the Post
and Comment
classes so they can accommodate the goodies their abstract parent now offers. Below are the refactored versions:
<?php
namespace Model;
class Post extends AbstractEntity implements PostInterface
{
protected $title;
protected $content;
protected $comments = array();
public function __construct($title, $content, array $comments = array()) {
$this->setTitle($title);
$this->setContent($content);
if (!empty($comments)) {
$this->setComments($comments);
}
}
public function setTitle($title) {
try {
$this->title = $this->sanitizeString($title);
return $this;
}
catch (InvalidArgumentException $e) {
throw new $e("Error setting the post title: " .
$e->getMessage());
}
}
public function getTitle() {
return $this->title;
}
public function setContent($content) {
try {
$this->content = $this->sanitizeString($content);
return $this;
}
catch (InvalidArgumentException $e) {
throw new $e("Error setting the post content: " .
$e->getMessage());
}
}
public function getContent() {
return $this->content;
}
public function setComment(CommentInterface $comment) {
$this->comments[] = $comment;
return $this;
}
public function setComments(array $comments) {
foreach ($comments as $comment) {
$this->setComment($comment);
}
return $this;
}
public function getComments() {
return $this->comments;
}
}
<?php
namespace Model;
class Comment extends AbstractEntity implements CommentInterface
{
protected $content;
protected $author;
public function __construct($content, $author) {
$this->setContent($content);
$this->setAuthor($author);
}
public function setContent($content) {
try {
$this->content = $this->sanitizeString($content);
return $this;
}
catch (InvalidArgumentException $e) {
throw new $e("Error setting the comment: " .
$e->getMessage());
}
}
public function getContent() {
return $this->content;
}
public function setAuthor($author) {
try {
$this->author = $this->sanitizeString($author);
return $this;
}
catch (InvalidArgumentException $e) {
throw new $e("Error setting the author : " .
$e->getMessage());
}
}
public function getAuthor() {
return $this->author;
}
}
The model’s subtypes are now a lot slimmer creatures, as most of the duplicated code has been placed within the dominant supertype. What’s more, because of the implementation under the surface of the __set()
/__get()
duet, consuming the model is even an easier task, reduced to just coding the following snippet:
<?php
$post = new Post("A sample post.", "This is the content of the post.");
$post->setComments(array(
new Comment(
"One banal comment for the previous post.",
"A fictional commenter"),
new Comment(
"Yet another banal comment for the previous post.",
"A fictional commenter")
));
echo $post->title . " " . $post->content . "<br>";
foreach ($post->comments as $comment) {
echo $comment->content . " " . $comment->author . "<br>";
}
The example might be contrived, but it does highlight how to get the model finally up and running, this time using the trimmed versions of the Post
and Comment
classes. The juicy stuff actually happens behind the scenes rather than on the flashy front line, since the definition of an abstract supertype entity allows us to remove large chunks of duplicated implementations without much worry during the whole refactoring process.
Moreover, the sole existence of a few subtypes is, on its own, a valid reason for going through the hassle of implementing a Layer Supertype. For obvious reasons, the pattern’s most appealing facet is exposed when dealing with multiple subtypes scattered through several application layers.
Closing Remarks
Although its commonly viewed as an overrated and abused beast, I hope now few will disagree that Inheritance is a powerful mechanism which, when cleverly employed in multi-tiered systems, can be an effective repellent of code duplication. The use of a simplistic pattern such as Layer Supertype is an example of the wealth of engaging virtues that Inheritance provides right out of the box when it comes to creating subtypes that share extensive segments of boilerplate implementation with each other. Image via FotoliaFrequently Asked Questions about the Layer Supertype Pattern
What is the main purpose of the Layer Supertype Pattern?
The Layer Supertype Pattern is a design pattern used in object-oriented programming. Its main purpose is to encapsulate common behavior across classes in a certain layer of an application. By doing so, it reduces code duplication and promotes code reuse, making the system more maintainable and easier to understand.
How does the Layer Supertype Pattern differ from other design patterns?
Unlike other design patterns that focus on communication between objects, the Layer Supertype Pattern focuses on the organization of code within a specific layer of an application. It provides a way to group common behavior in a superclass, which can then be extended by other classes in the same layer.
Can you provide an example of when to use the Layer Supertype Pattern?
Let’s say you’re developing a web application with multiple entities like User, Product, and Order. These entities might have common operations like save, delete, and update. Instead of writing these operations in each entity class, you can create a superclass (a layer supertype) that encapsulates these common operations. The entity classes can then extend this superclass.
What are the benefits of using the Layer Supertype Pattern?
The Layer Supertype Pattern promotes code reuse and reduces code duplication. It makes the code easier to maintain and understand. It also provides a clear structure for the layer, making it easier to add new classes or modify existing ones.
Are there any drawbacks to using the Layer Supertype Pattern?
While the Layer Supertype Pattern has many benefits, it’s not without its drawbacks. One potential issue is that it can lead to a large superclass that becomes difficult to manage. It can also make the system more complex if not used properly.
How does the Layer Supertype Pattern relate to the Single Responsibility Principle?
The Layer Supertype Pattern aligns well with the Single Responsibility Principle. Each class in the layer has a single responsibility and all the common behavior is encapsulated in the superclass. This makes the system easier to understand and maintain.
Can the Layer Supertype Pattern be used in conjunction with other design patterns?
Yes, the Layer Supertype Pattern can be used in conjunction with other design patterns. For example, it can be used with the Factory Pattern to create instances of classes in the layer.
Is the Layer Supertype Pattern specific to a particular programming language?
No, the Layer Supertype Pattern is not specific to any programming language. It can be implemented in any object-oriented programming language that supports inheritance.
How does the Layer Supertype Pattern affect testing?
The Layer Supertype Pattern can make testing easier. Since the common behavior is encapsulated in a superclass, you can test this behavior once in the superclass instead of testing it in each subclass.
Can the Layer Supertype Pattern be used in multi-tiered systems?
Yes, the Layer Supertype Pattern can be used in multi-tiered systems. It can be used to encapsulate common behavior in a specific layer of the system, making the system more organized and easier to maintain.
Alejandro Gervasio is a senior System Analyst from Argentina who has been involved in software development since the mid-80's. He has more than 12 years of experience in PHP development, 10 years in Java Programming, Object-Oriented Design, and most of the client-side technologies available out there.