Управление зависимостями в программном коде Сергей Юдин (syfisher)

Презентация:



Advertisements
Похожие презентации
TDD в Magento. С чего всё начиналось.. Agenda Введение в TDD Важные принципы при разработке ПО.
Advertisements

Перегрузка операторов x = a + b результат 1-й операнд2-й операнд оператор По количеству операндов операторы делятся на: унарные (один операнд) бинарные.
1 © Luxoft Training 2012 Java: расширенные вопросы Модуль #8.
Что нового в PHP 5.3Что нового в PHP 5.3Почему PHP 5.3? PHP 5.2 существует уже 1.5 года. В нем найдено несколько серьезных ошибок, которые не могут быть.
1.Введение 2.Немного теории a.Концептуальная диаграмма b.Суть фреймворка c.Как это работает 3.Пример: IT Developers v1 4.Actionscript 3 [Multicore version]
Основы ООП и C# Работа с объектами и классами. Классы Класс специальный тип данных для описания объектов. Он определяет данные и поведение типа. Определение.
©Павловская Т.А. (СПбГУ ИТМО) Курс «С#. Программирование на языке высокого уровня» Павловская Т.А.
Архитектура «D7»: модули, классы, жизненный цикл Кирсанов Алексей Ведущий разработчик 1C-Битрикс.
Методики «Inversion of Control» и «Dependency Injection». Применение в Spring. Малышкин Фёдор
Внедрение зависимостей. IoC-контейнеры Лекция 03.
Ресурсы WPF Два типа ресурсов WPF: объектные ресурсы (object resource) – определенный.NET-объект, который можно использовать многократно; ресурсы сборки.
Что нового в PHP 5.3 Дмитрий Стогов.
1 Современные системы программирования. Часть 2. Системное и прикладное программное обеспечение Малышенко Владислав Викторович.
Чистый код паттерны проектирования Олег Антонов Senior Web Developer MobiDev Corporation.
Программная иженерия Андрей Дмитриев ©
Объектно-ориентированный подход в языке C#. Класс в языке C# - ссылочный тип, определенный пользователем. Для классов ЯП C# допустимо только единичное.
Обзор возможностей Инверсия управления Аспектно-ориентированное программирование.
Особенности C# Индексаторы, события, частичные методы, расширяющие методы, сборщик мусора DraggonZ.
Ассоциативные списки Поиск данных происходит не по индексу или положению объекта, а по его ассоциативной связи: public interface Map { // Доступ к объектам.
Преобразования типов В языке C/C++ имеется несколько операций преобразования типов. Они используются в случае, если переменная одного типа должна рассматриваться.
Транксрипт:

Управление зависимостями в программном коде Сергей Юдин (syfisher)

Пару слов о себе Сергей Юдин (aka syfisher) 25 лет ООО «БИТ» г. Пенза Один из разработчиков фрейворка Limb3 (limb-project.com) Ведущий мастер-класса по TDD (опыт TDD около 4-х лет) Один из основателей agiledev.ru

ООП – это управление зависимостями Требования постоянно меняются Кода становится больше Зависимости между различными участками кода растут Если не управлять зависимостями – проект неизбежно «загнивает» С гниющей кодовой базой никто не хочет работать: «Давайте все переделаем!»

Признаки «гниющего» проекта Хрупкость – поломки в разных местах, где мы их не ожидаем Монолитность – повторное использование невозможно Вязкость – «грязные» приемы работают лучше, чем «правильные» Закрепощенность – изменения идут «со крипом», очень медленно и непредсказуемо Чрезмерная сложность – «без пол-литра не разобраться» или «позовите мне этого умника, который это написал»

Пример - WebSpider class WebSpider{ function __construct($db_params) { $this->db_params = $db_params; } function crowl($url) { $this->_crawlRecursive($uri, $uri); } function _crawlRecursive($uri, $context_uri) { if($this->_alreadyVisited($url)) return; if(!$content = file_get_contents($uri)) return; $this->_index($content, $url); $this->_markVisited($url); foreach($this->_extractUrls($content) as $link) $this->_crawlRecursive($link, $uri); } […] }

Пример - WebSpider Решает задачу обхода страниц и индексации Заполняет MySQL-индекс, название таблицы с индексом в базе данных фиксировано Делает все сам, и пока нам это нравится

Новые требования! Должен индексировать страницы только с определенных доменов Имя таблицы с индексом нужно иметь возможность задавать произвольно

Новые требования! class WebSpider{ […] function __construct($db_params, $index_table_name, $allowed_domains = array()) { $this->db_params = $db_params; $this-> index_table_name = $ index_table_name; $this->allowed_domains = $allowed_domains; } […] function _crawlRecursive($uri, $context_uri) { if($this->_alreadyVisited($url)) return; if(!$content = file_get_contents($uri)) return; $this->_index($content, $url); $this->_markVisited($url); foreach($this->_extractUrls($content) as $link) { if($this->_allowedDomainToVisit($uri))) $this->_crawlRecursive($link, $uri); } […] }

И еще раз! Ссылки определенного рода должны пропускаться Должен поддерживаться не только MySQL индекс

Наша реакция: первый вариант Вариант –динамическое подключение файла с индексером –и пара параметров в конструктор

Наша реакция: первый вариант class WebSpider{ […] function __construct($indexer_params, $allowed_domains = array(), $allows_links_regexs = array()) { require_once($indexer_params[indexer_path]); $indexer_class = $indexer_params[class_name]; $this->indexer = new $indexer_class($indexer_params); $this->allowed_domains = $allowed_domains; $this->allowed_links_regexs = $allows_links_regexs; } […] }

Недостатки первого варианта Неочевидные параметры для индексера Мы не застрахованы от изменений в логике по выбору нужных url-ов Сложности в тестировании (как внедрить мок-объект индексера в WebSpider?)

Вариант 2 – ООП подход Избавим WebSpider от знаний, как создается и как конфигурируется индексер Выделим класс UriExtractor, который будет знать все, что касается выделения url-ов из контента и их фильтрации

Вариант 2 – ООП подход class WebSpider { protected $indexer; protected $uri_extractor ; function __construct($indexer, $uri_extractor) { $this->indexer = $indexer; $this->uri_extractor = $uri_extractor; } function crowl($url) { $this->_crawlRecursive($uri, $uri); } function _crawlRecursive($uri, $context_uri) { if($this->_alreadyVisited($url)) return; if(!$content = file_get_contents($uri)) return; $this->_markVisited($url); $this->indexer->index($content, $url); foreach($this->uri_extractor->extractUrls($content) as $link) $this->_crawlRecursive($link, $uri); } […] }

И еще чуть-чуть Выделим интерфейсы WebSpiderIndexer и WebSpiderUriExtractor Заставим MySQLIndexer и UriExtractor реализовывать эти интерфейсы

И еще чуть-чуть class WebSpider { […] function __construct(WebSpiderIndexer $indexer, WebSpiderUriExtractor $uri_extractor) { $this->indexer = $indexer; $this->uri_extractor = $uri_extractor; }[…] } Interface WebSpiderIndexer{ function doIndex(); } Interface WebSpiderUriExtractor { function extractUrls($content); } class MySQLIndexer implements WebSpiderIndexer{…} class ProjectUriExtractor implements WebSpiderUriExtractor{…} $indexer = new MySQLIndexer($connection, $index_table_name); $extractor = new ProjectUriExtractor($allowed_domains, $exclude_url_regexps); $spider = new WebSpider($indexer, $extractor); $spider->сrawl($starting_url);

Результат рефакторинга Мы застраховали себя от подобных изменений (новые индексеры и правила выбора url-ов) Мы действовали исходя из реальных требований WebSpider теперь не имеет зависимостей от индексера и экстрактора Такой код намного проще тестировать Теперь в этой библиотеке соблюдены базовые принципы ООП –Принцип инверсии зависимостей –Принцип открытия-закрытия –Принцип отделения интерфейса

Базовые принципы ООП Принцип персональной ответственности (Single Responsibility Principle) – класс обладает только 1 ответственностью, поэтому существует только 1 причина, приводящая к его изменению Принцип открытия-закрытия (Open-Closed Principle) – классы должны быть открыты для расширений, но закрыты для модификаций. Расширение поведения производится за счет делегирования Принцип подстановки Лискоу (Liskov Substitution Principle) – дочерние классы можно использовать через интерфейсы базовых классов без знания о том, что это дочерний класс. Другими словами дочерний класс не должен отрицать поведение родительского класса. Принцип инверсии зависимостей (Dependency Inversion Principle) – зависимости внутри системы стоятся на основе абстракций (интерфейсы или абстрактный классы). Модули верхнего уровня не зависят от модулей нижнего уровня. Абстракции не зависят от подробностей. Принцип отделения интерфейса (Interface Segregation Principle) – клиенты не должны попадать в зависимость от методов, которыми они не пользуются. Клиенты сами определяют, какие интерфейсы им нужны. (подробнее в книге Р.Мартина «Быстрая разработка программ»)

Наш пример с WebSpider Принцип инверсии-зависимостей –WebSpider зависит от интерфейсов WebSpiderIndexerMySQLIndexer зависит от WebSpiderIndexer –В итоге зависимости строятся на основе абстракций (в виде интерфейсов) –Можно применить адаптер (Adapter) и использовать иные индексеры вместе с WebSpider-ом class OtherIndexerAdapter extends OtherIndexer implements WebSpiderIndexer { function index($content, $uri) { parent :: addToIndex($url, $content, time()); } $spider = new WebSpider(new OtherIndexerAdapter(), $extractor);

Что это нам дает? Менее связанный код (low coupling) Небольшие, сильно зацепленные классы (high cohesion) Возможность повторного использования (reuse) Расширяемость

Однако… Применение принципов ООП не гарантирует создание хорошей архитектуры Можно применять шаблоны проектирования, создавать кучу классов – но иметь систему, которой неудобно и непонятно пользоваться Чувство баланса приходит только с опытом и зависит от вашего проекта

Хорошая архитектура Низкая стоимость создания и поддержки – иногда самое хорошоее решение – первое, если оно работает. YAGNI (You Arent Gonna Need It) – вам это не потребуется. Не стоит проектировать на будущее Простота – чем меньше архитектурных решений, тем проще понимать систему. KISS – Keep It Simple Stupid. 1 класс вместо 3-х декораторов и фабрики. Очевидность использования – минимум телодвижений, чтобы получить результат Расширяемость – когда есть требования, система должна легко вбирать функционал Устойчивость – грамотное разделение ролей между компонентами позволяет быстро локализовывать ошибки. Тесты ломаются только в некоторых местах. Возможность повторного использования - низкое количество зависимостей от других компонентов определяет возможности по повторному использованию

Как избежать загнивания проекта? Рефакторинг не позволяет коду «загнивать» Тесты дают уверенность в безопасности рефакторинга

Самые важные зависимости Любая система содержит несколько часто используемых объектов («звездные» объекты) –Соединение с базой данных (connection) –Кеш (cache) –Запрос (Request) и ответ (Response) –Пользователь –и т.д. Нужно обеспечить легкий доступ к ним из любой точки приложения

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

Характер зависимостей Статический Динамический (инверсия зависимостей) class Server { protected $logger; function __construct(Logger $logger) { $this->logger = $logger; } function perform() { […] $this->logger->logOk(Served Ok); } class Server { function perform() { […] Log::logOk(Served Ok); }

Инверсия зависимостей Для соблюдения принципа инверсии зависимостей: –Избегайте инициализации объектов конкретных классов, объекты должны создаваться фабриками –Избегайте ассоциаций с конкретными классами –Объекты не должны порождаться статическими классами Зависимости от «звездных» объектов лучше строить на основе интерфейсов Это касается тех зависимостей, которые могут иметь тенденцию к изменениям

Что нам нужно? Динамический характер зависимостей –возможность смены «звездных» объектов без изменения клиентского кода Доступность в любой точке приложения –Заранее предсказать, где потребуется «звездный» объект достаточно сложно Поздняя инициализация –Связь со внешними ресурсами, когда это действительно нужно

Варианты связывания Пассивный подход (push) –Передача через конструктор (constructor injection) –Передача через set-метод (setter injection) –Передача в качестве параметра метода (parameter injection) –Установка атрибута класса (field injection) Активный подход (pull) –Локатор или реестр(service locator, registry) –Одиночка (singleton) Комбинированный –Контекст (context)

Передача через конструктор Плюсы: –Наглядно, все зависимости налицо –Облегчает тестирование Минусы: –Рост числа параметров конструктора –Лишние знания для клиентов class Client{ protected $server; public function __construct(Server $server){ $this->server = $server; } public function action(){ [...] $this->server->serve(); [..] } }

Использование set-методов Плюсы : –Позволяет заменять объекты при необходимости (обычно есть, объект который используется по-умолчанию, если setter не был вызван) Минусы : –Можно забыть вызвать тот или иной setter class Client{ protected $server; public function setServer(Server $server){ $this->server = $server; } function getServer(){ if(!is_object($this->server)) $this->server = new DefaultServer(); return $this->server; } public function action(){ $this->getServer()->serve(); } }

Одиночки (Singleton) Плюсы: –Легко реализовать –Обеспечивает глобальный доступ –Обеспечивает lazy initialization Минусы: –Одиночки – вариант статической зависимости –Сложность проведения изоляции в тестах –Затруднено наследование и расширение поведения class Log{ static function instance(){ static $log_instance = null; if(!$log_instance) $log_instance = new Log(); return $log_instance; }

Одиночки (Singleton) Таким приемом можно привести нашу одиночку в соответствие с принципом инверсии зависимостей Такие одиночки легко поддаются расширению и не препятствуют тестированию interface Logger { function logOk($message); } class Log implements Logger { protected $driver; function instance(){...} function logOk($message){ $this->driver->logOk($message); } function setDriver($driver){$this->driver = $driver;} }

Реестр (Registry) Используется для хранения именно объектов, а не каких-то параметров конфигурации class Registry { protected $_cache_stack; function __construct() { $this->_cache_stack = array(array()); } function setEntry($key, $item) { $this->_cache_stack[0][$key] = $item; } function getEntry($key) { return $this->_cache_stack[0][$key]; } function isEntry($key) { return ($this->getEntry($key) !== null); } … function instance() { static $registry = false; if (!$registry) { $registry = new Registry(); } return $registry; } function save() { array_unshift($this->_cache_stack, array()); if (!count($this->_cache_stack)) { trigger_error('Registry lost'); } function restore() { array_shift($this->_cache_stack); }

Реестр (Registry) Плюсы: –Обеспечивают глобальный доступ –Удобны при тестировании –Легко реализовать и использовать Минусы: –Сложно реализовать lazy initialization (нужны proxy-объекты) –Настройка реестра может быть сложной процедурой –Отсутствие четкого API, неочевидное содержимое реестра

Контекст (Context) Registry + четкий интерфейс = контекст Используется в некоторых фреймворках, например, Symphony Передается по цепочке всем объектам системы

Контекст (Context) class sfContext{ protected $controller = null, $databaseManager = null, $request = null, $response = null, $storage = null, $logger = null, $user = null; protected static $instance = null; public static function getInstance() { if (!isset(self::$instance)){ $class = __CLASS__; self::$instance = new $class(); self::$instance->initialize(); } return self::$instance; } public static function hasInstance() { return isset(self::$instance); } … protected function initialize() { $this->logger = sfLogger::getInstance(); if (sfConfig::get('sf_logging_enabled')) { $this->logger->info('{sfContext} initialization'); } if (sfConfig::get('sf_use_database')) { // setup our database connections $this->databaseManager = new sfDatabaseManager(); $this->databaseManager->initialize(); } public function getResponse() { return $this->response; } public function setResponse($response) { $this->response = $response; } public function getLogger(){ return $this->logger; } […] }

Контекст (Context) Плюсы: –Четкое API, понятно, что из контекста можно получить –Легко организовать lazy initialization –Можно обеспечить изоляцию в тестах Минусы: –Если реализован и используется где-то как одиночка, тогда могут возникнуть сложности с преодоление статических зависимостей в клиентском коде –Передавать контекст с уровня на уровень по цепочке может быть неудобно –Относительно сложно расширять, так как приходится создавать новые классы на вариации объектов, который можно получить из контекста (если при этом реализовывать соответствующий lazy initialization)

Limb3 Toolkit - локатор «по-нашему» Какие цели преследовались: –Глобальный доступ из любого места –Легкость расширения –Удобное использование для изоляции при тестировании –Отсутствие критичных статических зависимостей в клиентском коде

Limb3 Toolkit – архитектура

Limb3 Toolkit – схема работы Tools регистрируются в Toolkit Toolkit – одиночка Клиенты зависят только от Toolkit-а Toolkit делегирует методы tools-ам class SpeakingTools extends lmbAbstractTools { function sayHello($name) { echo Hello,. $name; } lmbToolkit :: merge(new SpeakingTools ()); […] lmbToolkit :: instance()->sayHello(Ivan); // echos Hello, Ivan.

Limb3 Toolkit – идеи реестра для тестирования class OtherSpeakingTools extends lmbAbstractTools{ function sayHello($name) { echo I dont know you,. $name; } class SomeClassTest extends UnitTestCase{ function setUp() { lmbToolkit :: save(); lmbToolkit :: merge(new OtherSpeakingTools()); } function tearDown() { lmbToolkit :: restore(); } [...] }

Limb3 Toolkit - самооценка Плюсы –Легко расширяется –Позволяет иметь в tools любые методы, включая фабричные или вспомогательные –Отлично обеспечивает изоляцию в тестах –Позволяет подменять tools частично (например, только 1 метод из 10) Минусы: –Неочевидно, в каком именно tools находится вызываемый метод, и нужно просматривать все merge() вызовы

Оценка всех pull-вариантов Pull-варианты ( Одиночка, Реестр, Контекст, Локатор) Плюсы : –Проще использовать, когда вы используете только 1 объект какого- то типа –Проще добраться до нужных объектов где-то грубоко в клиентском коде –Не имеют влияния на интерфейс клиентских классов Минусы: –Зависимости неочевидны, приходится просматривать код –Сложнее интегрировать библиотеки, если они используют каждая свой вариант –Клиентские классы имеют зависимости от подобных решений

Оценка всех push-вариантов Pull-варианты ( передача через конструктор, использование set-методов, передача в качестве параметров в методы, установка полей напрямую) Плюсы: –Очевидность зависимостей –Легкость изоляции при тестировании Минусы: –Все зависимости должны быть инициализированы при передаче иначе придется использовать proxy- объекты –Увеличивает количество параметров методов или конструкторов, если зависимостей много –Увеличивает знания, которыми обладают клиенты. Возможно, что эти знания эти им не нужны

IoC контейнеры IoC (Inversion of Control) контейнер – это специальный объект-сборщик, который на основании схемы зависимостей между классами и абстракциями может создать граф объектов. Любой IoC контейнер реализует принцип инверсии зависимостей IoC контейнеры появились в Java –Spring –Pico container IoC контейнеры используются на самых верхних уровнях приложений для инициализации объектов с учетом всех зависимостей Явных преимуществ от использования IoC контейнеров не видно

Java Pico Container Pico Container – позиционируется как минималистический dependency injection интструмент для Java Поддерживает Constructor Injection и Setter Injection

Пример для Pico Container public interface Server { void serve(Object client); } public class ConcreteServer implements Server { public void serve(Object client) { System.out.println("Im serving a client " + client); } public class Client { Server server; public Client(Server server) { this.server = server; } public void action() { server.serve(this); }

Пример для Pico Container Pico самостоятельно определит, что в качестве параметра для конструктора класса Client подходит класс ConcreteServer, так как он реализует интерфейс Server MutablePicoContainer pico = new DefaultPicoContainer(); pico.registerComponentImplementation(Server.class, ConcreteServer.class); pico.registerComponentImplementation(Client.class); Client client = (Client) pico.getComponentInstance(Client.class); client.action();

Java Spring Позиционируется как complete dependency injection tool Позволяет описывать зависимости Java-кодом или через XML-файл

Пример для Java Spring XML файл описания Получение объектов с учетом зависимостей BeanFactory factory = new XmlBeanFactory(new FileInputStream("dependency.xml")); Client client = (Client)factory.getBean("client"); client.action();

IoC контейнеры для PHP Pico Container for PHP (автор Павел Козловский) Phemto (автор Маркус Бейкер) Оба контейнера имеют статус alpha! Разработка обоих контейнеров остановлена в 2006 году

Pico Container for PHP Pico Container for PHP – порт Java Pico Container Автор: Павел Козловский Поддерживает Constructor Injection и Setter Injection Поддерживает Lazy Including Нет документации – только модульные тесты Разработка остановлена в начале 2006 года

Пример для PHP Pico Container interface Number { function getValue(); } class One implements Number { function getValue() { return 1; } } class Two implements Number { function getValue() { return 2; } } class Adder implements Number { public $result; function __construct(Number $a_one, Number $a_two) { $this->result = $a_one->getValue() + $a_two->getValue(); } function getValue() { return $this->result; }

Пример для PHP Pico Container $pico = new DefaultPicoContainer(); $pico->regComponentImpl('One'); $pico->regComponentImpl('Two'); $pico->regComponentImpl( 'Adder', //key 'Adder', //class name array('one' => new BasicComponentParameter('One'), 'two' => new BasicComponentParameter('Two'))); $ci = $pico->getComponentInstance('Adder'); echo $ci->getValue(); // 3

Пример Lazy Including для Pico $pico = new DefaultPicoContainer(); $pico->regComponent(new LazyIncludingComponentAdapter( new ConstructorInjectionComponentAdapter('LazyIncludeModelWithDpendencies'), path/to/first_class.php)); $pico->regComponent(new LazyIncludingComponentAdapter( new ConstructorInjectionComponentAdapter('LazyIncludeModelDependend'), path/to/second_class.php));

Phemto Автор Маркус Бейкер (создатель SimpleTest) Архитектура создана in PHPway и не копирует Java 250 строк рабочего кода + тесты Поддерживает Constructor Injection Поддерживает Lazy Including Отсутствие документации – только модульные тесты Разработка остановлена в середине 2006 года

Пример для Phemto $injector = new Phemto(); $injector->register('One'); $injector->register('Two'); $injector->register('Adder'); $result = $injector->instantiate('Adder'); $this->assertEqual($result->result, 3);

Наш опыт по зависимостям На уровне библиотек и подсистем – мы предпочитаем push-методы, такие как Constructor Injection и Parameter Injection На уровне приложений мы используем pull- подход (Service Locator) в виде Toolkit-а Мы не встречались с приложениями, где бы потребовались IoC контейнеры Главное – это иметь облегчить тестирование и обеспечить возможность расширения Хорошая архитектура – та, что подходить для вашего приложения