Если бы нужно было дать убедительную причину, по которой большинство разработчиков являются такими необычными существами, я бы сказал, что каждый раз, когда вы называете их ленивыми, они безумно гордятся собой и хвастаются широкой улыбкой. Хотя, по общему признанию, сам факт не идет дальше, чем просто любопытство и анекдотичность, он предполагает, что термин «ленивый» не считается оскорбительным. Конечно, будучи самим разработчиком, я бы сказал, что мы не настолько безумны (или, по крайней мере, наше безумие, как правило, довольно безобидно). Просто мы принимаем слово «ленивый» как специальный синоним «действительно СУХОГО человека, который умно использует вещи» (следовательно, выполняет меньше работы).
Хотя нет ничего плохого в том, чтобы иметь такую позицию, описанную как принцип наименьшего усилия , иногда это всего лишь блеф. Мы знаем, что использование преимуществ инфраструктуры A вместе с обширной функциональностью, предлагаемой библиотекой B, может быть чрезвычайно продуктивным, когда речь идет о сжатых сроках и получении прибыли, но в конце концов все, что мы действительно хотим, — это избежать мучительных последствий кода Дублирование и наслаждайтесь благословениями многоразовой реализации. Давайте будем честными … действительно ли мы знаем, являются ли рассматриваемые фреймворки или библиотеки такими же СУХЫМИ, как они утверждают? Хуже того, если предположить, что они есть, способны ли мы писать DRY-код поверх таких кодовых баз?
Широко распространено мнение, что повторное использование реализации, в результате чего создается код DRYer, сводится к использованию преимуществ наследования, верно? Ну, я бы хотел, чтобы это было так просто! Мало кто из практиков программирования культового груза добавит свои громкие голоса, утверждая, что Composition на самом деле король повторного использования кода и что наследование должно быть отброшено. Или, почему бы нам просто не прекратить вращаться и избавиться от обоих, взяв вместо этого простую делегацию ? Это действительно зудящая дилемма, но в конечном итоге бессмысленная.
Как и следовало ожидать, у каждого подхода есть своя зона комфорта, и то, какая из них лучше всего подходит, зависит от типа решаемой проблемы и ее окружения. Наследование долгое время было переоцененным чудовищем, особенно в мире PHP, где основные концепции ООП, такие как принцип подстановки Лискова (LSP), рассматривались в значительной степени как запутанные, неясные парадигмы, имеющие незначительное значение или не применимые в реальном мире. С другой стороны, Композиция и Делегация нашли довольно плодотворную нишу в языке, подкрепленную импульсом, полученным в результате внедрения зависимости в последние несколько лет. Это как если бы противоборствующие силы постоянно давили друг на друга, чтобы поддерживать чисто вымышленный баланс.
Если вы не знаете, по какому пути двигаться, когда дело доходит до повторного использования реализации, в этой статье я сделаю простое пошаговое руководство по трио Inheritance / Composition / Delegation в попытке продемонстрировать, бок о бок, некоторые из их самые привлекательные достоинства и неуклюжие недостатки.
Путь к наследованию — простота и проблемы с базовым типом / подтипом
Не должно быть горячих новостей о том, что первый подход к повторному использованию кода, который я планирую уделить первоочередному вниманию, является самым чрезмерным и злоупотребленным из всех, Наследование. Но я не хочу быть садистом и доводить вас до слез (и меня, конечно), объясняющих, как получить хороший подкласс Dog и запустить его, наследуя от Animal (ой!). Скорее я буду немного более реалистичным, и мы погрузим свои зубы в наивного, но функционального, Адаптер PDO. Вот так:
<?php namespace LibraryDatabase; interface DatabaseAdapterInterface { public function executeQuery($sql, array $parameters = array()); }
<?php namespace LibraryDatabase; class PdoAdapter extends PDO implements DatabaseAdapterInterface { protected $statement; public function __construct($dsn, $username = null, $password = null, array $options = array()) { // fail early if the PDO extension is not loaded if (!extension_loaded("pdo")) { throw new InvalidArgumentException( "This adapter needs the PDO extension to be loaded."); } // check if a valid DSN has been passed in if (!is_string($dsn) || empty($dsn)) { throw new InvalidArgumentException( "The DSN must be a non-empty string."); } // try to create a valid PDO object try { parent::__construct($dsn, $username, $password, $options); $this->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function executeQuery($sql, array $parameters = array()) { try { $this->statement = $this->prepare($sql); $this->statement->execute($parameters); return $this->statement->fetchAll(PDO::FETCH_CLASS, "stdClass"); } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } }
Когда дело доходит до создания быстрого и грязного адаптера, который просто добавляет некоторые дополнительные функциональные возможности к тому, который предоставляется классом PDO
, наследование действительно является довольно заманчивым путем. В этом случае адаптер выполняет некоторую базовую проверку введенных аргументов базы данных, а его executeQuery()
позволяет нам выполнять параметризованные запросы через легко потребляемый API. Следующий фрагмент демонстрирует, как вставить адаптер в клиентский код, чтобы извлечь несколько пользователей из базы данных:
<?php use LibraryLoaderAutoloader, LibraryDatabasePdoAdapter; require_once __DIR__ . "/Library/Loader/Autoloader.php"; $autoloader = new Autoloader; $autoloader->register(); $adapter = new PdoAdapter("mysql:dbname=mydatabase", "myfancyusername", "myhardtoguesspassword"); $guests = $adapter->executeQuery("SELECT * FROM users WHERE role = :role", array(":role" => "Guest")); foreach($guests as $guest) { echo $guest->name . " " . $guest->email . "<br>"; }
Нет никаких вонючих признаков каких-либо «случайных» поломок LSP, а это означает, что любой экземпляр PdoAdapter
может быть безопасно заменен во время выполнения базовым объектом PDO
без жалоб со стороны клиентского кода, и адаптер получил все наследство функциональность инкапсулирована его родителем. Можем ли мы позволить себе заключить более выгодную сделку?
По общему признанию, есть подвох. Хотя это и не явно, PdoAdapter
на самом деле представляет внешнему миру весь подробный API PDO, а также тот, который реализован сам по себе. Могут быть случаи, когда мы хотим избежать этого, даже если отношение «IS-A» между PdoAdapter
и PDO
поддерживается аккуратно.
В таких ситуациях мы могли бы обратиться к мудрости мантры «благосклонность композиции вместо наследования» и эффективно перейти к морям композиции. При этом мы будем эффективно использовать реализацию PDO
, но не будем иметь дело со всем его API. Еще лучше, поскольку не было бы отношения «IS-A», а вместо этого «HAS-A» между соответствующим адаптером и классом PDO
, мы также удовлетворили бы наши чистые инстинкты ООП. Инкапсуляция сохранит нетронутой оболочку.
Конечно, один хороший способ понять внутреннюю работу этого подхода — это подать конкретный пример. Давайте PdoAdapter
предыдущий класс PdoAdapter
для PdoAdapter
заповедей Composition.
Композиция над наследованием — адаптеров, приспешников и других забавных фактов
Вопреки распространенному мнению, до смешного легко реализовать адаптер PDO, который опирается на основы Composition. Кроме того, весь рефакторинг будет ограничен созданием простой оболочки, которая будет внедрять нативный объект PDO
в конструктор. Еще раз, практический пример — лучший учитель, когда дело доходит до понимания движущей логики процесса:
<?php namespace LibraryDatabase; interface DatabaseAdapterInterface { public function executeQuery($sql, array $parameters = array()); }
<?php namespace LibraryDatabase; class PdoAdapter implements DatabaseAdapterInterface { protected $pdo; protected $statement; public function __construct(PDO $pdo) { $this->pdo = $pdo; $this->pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this->pdo->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); } public function executeQuery($sql, array $parameters = array()) { try { $this->statement = $this->pdo->prepare($sql); $this->statement->execute($parameters); return $this->statement->fetchAll(PDO::FETCH_CLASS, "stdClass"); } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } }
Хотя реорганизованная версия адаптера в целом следует формальному определению шаблона адаптера , адаптер , передаваемый во внутренние компоненты адаптера, в данном случае является конкретной реализацией, а не абстракцией. Учитывая, что мы пытаемся спроектировать сокращенный API для объекта PDO
, не повредит быть прагматичным и внедрить объект непосредственно в вопрос без необходимости проходить через определение дополнительного интерфейса. Если вы чувствуете себя немного более комфортно, выбирая подход, основанный на интерфейсе, просто продолжайте и придерживайтесь его до конца.
Оставляя в стороне эти тонкие детали реализации, безусловно, наиболее значимым моментом является то, PdoAdapter
класс PdoAdapter
теперь является менее многословным существом. Используя Composition, он скрывает весь PDO API от внешнего мира, предоставляя клиентскому коду только свой executeQuery()
. Несмотря на наивность, этот пример поднимает несколько моментов, которые стоит отметить. Во-первых, бремя работы с потенциально опасными иерархиями классов, где подтипы могут вести себя совершенно иначе, чем их базовые типы, уносящие поток приложений, незаметно исчезло в воздухе. Во-вторых, API адаптера не только теперь менее раздутый, но и декларирует явную зависимость от реализации PDO
, что позволяет легче видеть со стороны соавтора, что ему нужно делать свое дело.