Статьи

Повторное использование реализации — обзор наследования, композиции и делегирования

Если бы нужно было дать убедительную причину, по которой большинство разработчиков являются такими необычными существами, я бы сказал, что каждый раз, когда вы называете их ленивыми, они безумно гордятся собой и хвастаются широкой улыбкой. Хотя, по общему признанию, сам факт не идет дальше, чем просто любопытство и анекдотичность, он предполагает, что термин «ленивый» не считается оскорбительным. Конечно, будучи самим разработчиком, я бы сказал, что мы не настолько безумны (или, по крайней мере, наше безумие, как правило, довольно безобидно). Просто мы принимаем слово «ленивый» как специальный синоним «действительно СУХОГО человека, который умно использует вещи» (следовательно, выполняет меньше работы).

Хотя нет ничего плохого в том, чтобы иметь такую ​​позицию, описанную как принцип наименьшего усилия , иногда это всего лишь блеф. Мы знаем, что использование преимуществ инфраструктуры 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 , что позволяет легче видеть со стороны соавтора, что ему нужно делать свое дело.

И все же это множество преимуществ идет с не столь скрытой стоимостью: в более реалистичных ситуациях может быть сложнее создать «составные» адаптеры, чем создать «унаследованные». Правильный путь протектора будет варьироваться от случая к случаю; будьте рациональны и выберите тот, который наилучшим образом соответствует вашим требованиям.

Если вам интересно, как добиться PdoAdapter с помощью PdoAdapter класса PdoAdapter , следующий пример должен быть поучительным:

 <?php $pdo = new PDO("mysql:dbname=mydatabase", "myfancyusername", "myhardtoguesspassword"); $adapter = new PdoAdapter($pdo); $guests = $adapter->executeQuery("SELECT * FROM users WHERE role = :role", array(":role" => "Guest")); foreach($guests as $guest) { echo $guest->name . " " . $guest->email . "<br>"; } 

К настоящему времени мы увидели на очень широком уровне два общих подхода, которые можно использовать для повторного использования реализации. Итак, что будет дальше? Что ж, я тоже пообещал углубиться в тонкости делегации. Хотя по общему признанию Composition является неявной формой делегирования, в более раннем примере ссылка на делегирование была установлена ​​путем объявления зависимости от реализации PDO в конструкторе.

Тем не менее, возможно запустить и запустить приличный механизм делегирования, используя один или несколько заводских методов, даже если это не позволяет использовать преимущества отделения интерфейса от реализации. Несмотря на это, стоит взглянуть на этот подход, так как он полезен для создания адаптеров PDO, способных к медленным соединениям с базой данных.

Отсрочка отключения базы данных через делегирование

Может ли быть что-то более восхитительное, чем задержка дорогой поездки в базу данных до последней минуты? Конечно, лучше избегать его полностью, что является основной причиной, по которой система кеша живет и дышит. Но, к сожалению, иногда у нас просто не может быть всего этого, и нам нужно разобраться с этим, разрабатывая адаптеры PDO, которые инкапсулируют эту изящную способность внутри границ фабричного метода.

Делегирование — это простой, но мощный шаблон, который позволяет реализовать эту функцию без особых хлопот. Если бы мы обратили наше внимание на предыдущий класс PdoAdapter и заставили его использовать преимущества делегирования, это выглядело бы следующим образом:

 <?php namespace LibraryDatabase; interface DatabaseAdapterInterface { public function connect(); public function disconnect(); public function executeQuery($sql, array $parameters = array()); } 
 <?php namespace LibraryDatabase; class PdoAdapter implements DatabaseAdapterInterface { protected $config = array(); protected $connection; 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."); } $this->config = compact("dsn", "username", "password", "options"); } public function connect() { if ($this->connection) { return; } try { $this->connection = new PDO( $this->config["dsn"], $this->config["username"], $this->config["password"], $this->config["options"] ); $this->connection->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $this->connection->setAttribute( PDO::ATTR_EMULATE_PREPARES, false); } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } public function disconnect() { $this->connection = null; } public function executeQuery($sql, array $parameters = array()) { $this->connect(); try { $this->statement = $this->connection->prepare($sql); $this->statement->execute($parameters); return $this->statement->fetchAll(PDO::FETCH_CLASS, "stdClass"); } catch (PDOException $e) { throw new RunTimeException($e->getMessage()); } } } 

Если у вас когда-либо была возможность создать свой собственный шедевр PDO с нуля, или если вы только что использовали одну из куч кучи, доступную в дикой природе, базовую логику вышеуказанного адаптера должно быть довольно легко понять. Несомненно, его наиболее привлекательным аспектом является реализация метода connect() , поскольку его обязанностью является создание объекта PDO по запросу, который затем используется, в свою очередь, для отправки запросов в базу данных.

Хотя по общему признанию это довольно скучно и скучно, полезно показать, как использовать своего рода «жестко запрограммированное» делегирование в реализации адаптеров, которые достаточно умны для запуска отложенных соединений с базой данных. Кроме того, пример ниже демонстрирует, как использовать адаптер:

 <?php $adapter = new PdoAdapter("mysql:dbname=mydatabase", "myfancyusername", "myhardtoguespassword"); $guests = $adapter->executeQuery("SELECT * FROM users WHERE role = :role", array(":role" => "Guest")); foreach($guests as $guest) { echo $guest->name . " " . $guest->email . "<br>"; } 

Если хотите, назовите меня невыносимым педантиком, но использование адаптера на самом деле является простым процессом, особенно если проанализировать его с точки зрения клиентского кода. Основная ошибка заключается в том, что адаптер скрывает зависимость. Это вводит неприятную связь, следовательно, делает всю реализацию немного грязной. С другой стороны, самое привлекательное достоинство адаптера — возможность вызывать базу данных только тогда, когда этого требует ситуация. Естественно, что этот вид компромисса может быть довольно легко решен путем введения фабрики в адаптер, который будет отвечать за создание объекта PDO при необходимости.

Несмотря на то, что этот подход является лучшим из обоих миров, поскольку он активно продвигает использование инъекций зависимостей, в то же время используя делегирование, на мой взгляд, это несколько излишне, по крайней мере, в данном случае, в частности.

Заключительные мысли

Я должен признать, что довольно сложно прийти к суждению о недостатках и тонкостях, выявленных наследованием, составом и делегированием, не вдаваясь в поверхностный, скромный анализ. Тем не менее, сделать решающий шаг, безусловно, стоит, учитывая, что каждый подход является центральной опорой парадигмы ООП.

Более того, хотя в этом случае я намеренно проводил изолированную оценку «по каждому подходу», что, как мы надеемся, в конечном итоге было поучительным, возможно объединить все подходы вместе для реализации более эффективных стратегий повторного использования кода. Здесь нет взаимного исключения.

Излишне говорить, что нет смысла накапливать их вслепую только потому, что вы можете. Во всех случаях будьте осознанными и убедитесь, что их использование ограничено контекстом, с которым вы имеете дело.

Изображение через Fotolia