Будучи одним из краеугольных камней ООП, Inheritance очень похожа на двухстороннюю дверь, которая опасно качается в обе стороны. Когда он открывается в одну сторону, он предоставляет мощный механизм, который позволяет нам сразу же повторно использовать реализации, не прибегая к Composition . Однако всякий раз, когда он открывается на другую сторону, мы видим его приятную природу, быстро исчезающую в воздухе, замененную тем злым и искривленным зверем, способным создавать всевозможные гнилые иерархии, в которых подтипы ведут себя так дико, как их базовые типы, что Сказать, что между каждым из них есть отношения «Я-А», является кощунственным!
Несмотря на все подводные камни и странности, связанные с наследованием, большинство из которых можно смягчить путем рационального и умеренного использования, его очаровательному влиянию трудно противостоять. В конце концов, повторное использование кода является причиной, по которой Inheritance живет и дышит в первую очередь, и он может стать настоящим убийцей, когда речь идет о добавлении стандартных реализаций в абстракции многоуровневой системы.
Наследование предлагает простой способ легко порождать большое количество объектов, которые семантически связаны друг с другом без дублирования кода. Концепция смехотворно проста — но мощна: сначала вы отбрасываете как можно больше логики в пределах границ базового типа (обычно это абстрактный класс, но это может быть конкретный класс), а затем начинаете получать уточненные подтипы в соответствии с более конкретными требованиями. , Процесс, как правило, проводится по принципу «на слой», таким образом, каждый слой получает свой собственный набор супертипов, основные функции которых перегоняются и расширяются по очереди соответствующими подтипами.
Неудивительно, что этот повторяющийся цикл инкапсуляции / деривации основывается на формальностях шаблона проектирования, известного как Layer Supertype (да, хотя он несколько наивен, но имеет реальное академическое название), и в следующих строках я расскажу углубленно изучите его внутреннюю работу, и вы увидите, насколько просто подключить его функциональность к модели предметной области .
Необходимость супертипа уровня — определение модели раздутого домена
Можно сказать, что супертип слоя — это естественная и избирательная эволюция «общих» базовых типов, только последние живут и дышат в границах определенного слоя. Это имеет широкую нишу в многоуровневых проектах, где использование функциональности супертипа является в основном настоятельной необходимостью, а не просто легкомысленным решением.
Как обычно, наиболее эффективный способ понять прагматизм, лежащий в основе шаблона, — это несколько практических примеров. Итак, скажем, нам нужно с нуля построить простую модель предметной области, отвечающую за определение нескольких основных взаимодействий между некоторыми сообщениями в блоге и соответствующими комментариями.
Вкратце, модель может быть легко очерчена как анемичный слой, содержащий всего пару скелетных классов, моделирующих посты и комментарии.
Первый класс домена вместе с его контрактом может выглядеть так:
<?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;
}
}
Управление классом Post
Это должно быть довольно просто для понимания.
Теперь давайте сделаем модель немного толще, добавив к ней класс, который порождает комментарии, связанные с определенной записью в блоге. Это контракт и реализация выглядят так:
<?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;
}
}
Как и Post
Comment
Но теперь, когда оба класса установлены, мы можем использовать модель. Например:
<?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>";
}
Это действительно работает как шарм! Использование модели — это довольно простой процесс, требующий, чтобы вы сначала создали несколько объектов Post
Да, жизнь сладка и хороша. Ну, пока это так, но все может быть намного лучше!
Не то чтобы я хотел испортить волшебство такого прекрасного момента, но я должен признаться, что легкая дрожь пробегает по моему позвоночнику каждый раз, когда я смотрю на реализации классов Post
Comment
Хотя это не является серьезной проблемой как таковой, некоторые методы, такие как setId()
setContent()
Решение этой проблемы без неаккуратности не так интуитивно понятно, как может показаться на первый взгляд из-за нескольких логических проблем. Во-первых, хотя они разделяют семантические отношения друг с другом, каждый класс эффективно моделирует различные типы объектов. Во-вторых, они реализуют разрозненные интерфейсы, что означает, что довольно трудно абстрагировать логику, не заканчивая неуклюжей иерархией, где условие «IS-A» никогда не выполняется.
В данном случае, в частности, мы могли бы просто придерживаться более либерального подхода и думать о Post и Comment как о подтипах высокоуровневого супертипа AbstractEntity
При этом было бы довольно просто поместить разделяемую реализацию в границы абстрактного класса, следовательно, сделать определения подтипов намного тоньше. Поскольку весь процесс абстракции будет проводиться только в доменном слое, гипотетическая AbstractEntity
Просто, но приятно, а?
Перемещение общей реализации в класс одного домена — создание супертипа слоя
Процесс создания супертипа модели, по крайней мере, в этом случае, можно рассматривать как смесь нескольких методов атомарного и гранулярного рефакторинга, таких как Extract SuperClass, Pull Up Field и Pull Up Method, подробно рассмотренные Мартином Фаулером в его книга « Рефакторинг: улучшение дизайна существующего кода» (если у вас нет копии, убедитесь, что вы захватили один как можно скорее).
Давайте сделаем большой шаг вперед и проведем рефакторинг нашей неуклюжей модели предметной области, добавив в нее вышеупомянутый класс AbstractEntity
Поскольку в этой конкретной ситуации основная масса дублированного кода отображается с помощью методов классов домена, которые обрабатывают идентификаторы и выполняют некоторую базовую проверку для полей на основе строк, было бы полезно делегировать эти обязанности супертипу и позволить сфокусировать подтипы делать меньше, более узкие задачи.
Исходя из этой упрощенной концепции, «жирная» реализация вышеупомянутого модельного супертипа может выглядеть так:
<?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);
}
}
Вопреки распространенному мнению, воплощение в жизнь абстрактного супертипа, который аккуратно заключает в себе большую часть логики, разделяемой подтипами модели, под капотом, на самом деле является простым процессом, который может быть выполнен относительно быстро. В этом случае я признаю, что стал немного переусердствовать, так как супертип не только способен обрабатывать идентификаторы и проверять / дезинфицировать строки, но также разбрызгивает некоторую магию PHP за кулисами, чтобы сопоставить вызовы приватных / защищенных свойств их соответствующим мутаторам. / аксессоры, когда это возможно. Предоставление супертипа с дополнительной функциональностью, подобной этой, прямо из коробки, совершенно необязательно, поэтому не стесняйтесь включать его или опускать в своем собственном классе AbstractEntity
Следующим шагом является рефакторинг реализаций классов Post
Comment
Ниже приведены переработанные версии:
<?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;
}
}
Подтипы модели теперь намного более тонкие существа, так как большая часть дублированного кода была помещена в доминирующий супертип. Более того, из-за реализации под поверхностью __set()
__get()
<?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>";
}
Пример может быть надуманным, но он показывает, как, наконец, запустить и запустить модель, на этот раз с использованием урезанных версий классов Post
Comment
Сочная штука на самом деле происходит не за кулисами, а за кулисами, поскольку определение абстрактного объекта супертипа позволяет нам удалять большие куски дублированных реализаций без особого беспокойства в течение всего процесса рефакторинга.
Более того, единственное существование нескольких подтипов само по себе является веской причиной для преодоления трудностей с реализацией супертипа Layer. По очевидным причинам наиболее привлекательный аспект шаблона раскрывается при работе с несколькими подтипами, разбросанными по нескольким уровням приложения.
Заключительные замечания
Хотя его обычно рассматривают как переоцененного и оскорбленного зверя, я надеюсь, что теперь мало кто не согласится с тем, что Наследование является мощным механизмом, который при умном использовании в многоуровневых системах может быть эффективным средством от дублирования кода. Использование упрощенного шаблона, такого как Layer Supertype, является примером богатства привлекательных достоинств, которые Inheritance предоставляет сразу, когда дело доходит до создания подтипов, которые совместно используют обширные сегменты стандартной реализации.
Изображение через Fotolia