PHP Ленивая загрузка объектов и внедрение зависимостей - PullRequest
18 голосов
/ 29 марта 2012

Я недавно начал читать о внедрении зависимостей, и это заставило меня переосмыслить некоторые из моих проектов.

У меня проблема примерно такая: допустим, у меня есть два класса: Car и Passenger;Для этих двух классов у меня есть несколько картографов данных для работы с базой данных: CarDataMapper и PassengerDataMapper

Я хочу иметь возможность сделать что-то вроде этого в коде:

$car = CarDataMapper->getCarById(23); // returns the car object
foreach($car->getPassengers() as $passenger){ // returns all passengers of that car
    $passenger->doSomething();
}

Прежде чем я что-то зналЧто касается DI, я бы построил свои классы так:

class Car {
    private $_id;
    private $_passengers = null;

    public function getPassengers(){
        if($this->_passengers === null){
            $passengerDataMapper = new PassengerDataMapper;
            $passengers = $passengerDataMapper->getPassengersByCarId($this->getId());
            $this->setPassengers($passengers);
        }
        return $this->_passengers;  
    }

}

У меня также был бы аналогичный код в методе Passenger-> getCar (), чтобы выбрать автомобиль, в котором находится пассажир.

Теперь я понимаю, что это создает зависимости (ну, я и раньше это понимал, но я не знал, что это «неправильно») между объектами Car и Passenger и объектами отображения данных.

При попыткеподумать над решением этих двух вариантов, но мне не очень нравится ни один из них:

1: делать что-то вроде этого:

$car = $carDataMapper->getCarById(23);
$passengers = $passengerDataMapper->getPassengersByCarId($car->getId());
$car->setPassengers($passengers);

foreach($car->getPassengers() as $passenger){
    $passenger->doSomething();
}

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

2: закачка картографических данных в машину ипассажирские объекты и имеющие что-то вроде этого:

class Car {
    private $_id;
    private $_passengers = null;
    private $_dataMapper = null;

    public function __construct($dataMapper){
        $this->setDataMapper($dataMapper);
    }

    public function getPassengers(){
        if($this->_passengers === null && $this->_dataMapper instanceof PassengerDataMapper){
            $passengers = $this->_dataMapper->getPassengersByCarId($this->getId());
            $this->setPassengers($passengers);
        }
        return $this->_passengers;  
    }
}

Мне это не нравится больше, потому что это не так, как автомобиль действительно не знает о картографе данных, и без картографа данных, автомобиль может вести себя непредсказуемо (не возвращать пассажиров, когда они действительно есть)

Итак, мой первый вопрос: я использую совершенно неправильный подход, потому что, чем больше я смотрю на него, тем больше он выглядит, как будто я строюORM вместо бизнес-уровня?

Второй вопрос: есть ли способ на самом деле отделить объекты и средства отображения данных таким образом, чтобы я мог использовать объекты, как описано в самом первом кодеблок?

Третий вопрос: я видел несколько ответов для других языков (я думаю, что для некоторой версии C), решающих эту проблему с помощью чего-то подобного описанному здесь: Как правильно внедрить доступ к даннымзависимость для отложенной загрузки? Поскольку у меня не было времени поиграть с другими языками, это не имеет смысла для меня, поэтому я был бы признателен, если бы кто-то объяснил примеры в ссылке в PHP-ish.

Я также посмотрел на некоторые структуры DI и прочитал о DI-контейнерах и инверсии управления, но из того, что я понял, они используются для определения и внедрения зависимостей для «не динамических» классов, где, например, Car будетзависит от двигателя, но для него не требуется, чтобы двигатель загружался динамически из БД, он просто был бы создан и внедрен в машину.

Извините за длинный пост и заранее спасибо.

Ответы [ 3 ]

10 голосов
/ 29 марта 2012

Может быть не по теме, но я думаю, что это вам немного поможет:

Я думаю, что вы пытаетесь найти идеальное решение.Но независимо от того, что вы придумали, через пару лет вы будете более опытными и определенно сможете улучшить свой дизайн.

За последние годы с моими коллегами мы разработали много ORM./ Бизнес-модели, но почти для каждого нового проекта мы начинали с нуля, поскольку все были более опытными, все извлекли уроки из предыдущих ошибок, и все столкнулись с новыми шаблонами и идеями.Все это добавляло к разработке дополнительный месяц или около того, что увеличивало стоимость конечного продукта.

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

Если, конечно, вы не пишете код для исследования или для удовольствия.

TL; DR: Ваше будущее я всегда перехитрит ваше нынешнее, поэтому не думайте об этом.Просто тщательно выберите рабочее решение, освоите его и придерживайтесь его, пока оно не решит ваши проблемы: D

Чтобы ответить на ваши вопросы:

  1. Ваш код отличнохорошо, но чем больше вы будете пытаться сделать его «умным», «абстрактным» или «свободным от зависимостей», тем больше вы будете склоняться к ORM.

  2. То, что вы хотите впервый блок кода вполне выполним.Посмотрите, как работает Doctrine ORM, или этот очень простой подход ORM, который я использовал несколько месяцев назад для проекта выходного дня:

https://github.com/aletzo/dweet/blob/master/app/models

5 голосов
/ 30 марта 2012

Я собирался сказать: «Я знаю, что это старый вопрос, но ...», затем я понял, что вы опубликовали его 9 часов назад, и это круто, потому что я просто пришел к удовлетворительному «решению» для себя.Я подумал о реализации, а затем понял, что это то, что люди называют «внедрение зависимостей».

Вот пример:

class Ticket {

    private $__replies;
    private $__replyFetcher;
    private $__replyCallback;
    private $__replyArgs;

    public function setReplyFetcher(&$instance, $callback, array $args) {
        if (!is_object($instance))
            throw new Exception ('blah');
        if (!is_string($callback))
            throw new Exception ('blah');
        if (!is_array($args) || empty($args))
            throw new Exception ('blah');
        $this->__replyFetcher = $instance;
        $this->__replyCallback = $callback;
        $this->__replyArgs = $args;
        return $this;
    }

    public function getReplies () {
        if (!is_object($this->__replyFetcher)) throw new Exception ('Fetcher not set');
        return call_user_func_array(array($this->__replyFetcher,$this->__replyCallback),$this->__replyArgs);
    }

}

Затем, на вашем уровне обслуживания (где вы 'координировать действия между несколькими сопоставителями и моделями) вы можете вызвать метод 'setReplyFetcher' для всех объектов заявки, прежде чем возвращать их к тому, что вызывает сервисный слой - или - вы можете сделать что-то очень похожее с каждым сопоставителем,давая мапперу приватное свойство fetcherInstance и callback для каждого маппера, в котором будет нуждаться объект, а затем установите THAT на уровне обслуживания, тогда маппер позаботится о подготовке объектов.Я все еще оцениваю различия между двумя подходами.

Пример координации на уровне обслуживания:

class Some_Service_Class {
    private $__mapper;
    private $__otherMapper;
    public function __construct() {
        $this->__mapper = new Some_Mapper();
        $this->__otherMapper = new Some_Other_Mapper();
    }
    public function getObjects() {
        $objects = $this->__mapper->fetchObjects();
        foreach ($objects as &$object) {
            $object->setDependentObjectFetcher($this->__otherMapper,'fetchDependents',array($object->getId()));
        }
        return $objects;
    }
}

В любом случае, классы объектов не зависят от классов mapper и mapperклассы не зависят друг от друга.

РЕДАКТИРОВАТЬ: Вот пример другого способа сделать это:

class Some_Service {
    private $__mapper;
    private $__otherMapper;
    public function __construct(){
        $this->__mapper = new Some_Mapper();
        $this->__otherMapper = new Some_Other_Mapper();
        $this->__mapper->setDependentFetcher($this->__otherMapper,'someCallback');
    }
    public function fetchObjects () {
        return $this->__mapper->fetchObjects();
    }        
}

class Some_Mapper {
    private $__dependentMapper;
    private $__dependentCallback;
    public function __construct ( $mapper, $callback ) {
        if (!is_object($mapper) || !is_string($callback)) throw new Exception ('message');
        $this->__dependentMapper = $mapper;
        $this->__dependentCallback = $callback;
        return $this;
    }
    public function fetchObjects() {
        //Some database logic here, returns $results
        $args[0] = &$this->__dependentMapper;
        $args[1] = &$this->__dependentCallback;
        foreach ($results as $result) {
            // Do your mapping logic here, assigning values to properties of $object
            $args[2] = $object->getId();
            $objects[] = call_user_func_array(array($object,'setDependentFetcher'),$args)
        }
    }
}

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

1 голос
/ 31 марта 2012

Вот еще один подход, о котором я подумал.Вы можете создать объект «DAOInjection», который действует как контейнер для конкретного DAO, обратного вызова и аргументов, необходимых для возврата нужных объектов.Классы должны знать только об этом классе DAOInjection, поэтому они по-прежнему отделены от всех ваших DAO / mappers / services / etc.

class DAOInjection {
    private $_DAO;
    private $_callback;
    private $_args;
    public function __construct($DAO, $callback, array $args){
        if (!is_object($DAO)) throw new Exception;
        if (!is_string($callback)) throw new Exception;
        $this->_DAO = $DAO;
        $this->_callback = $callback;
        $this->_args = $args;
    }
    public function execute( $objectInstance ) {
        if (!is_object($objectInstance)) throw new Exception;
        $args = $this->_prepareArgs($objectInstance);
        return call_user_func_array(array($this->_DAO,$this->_callback),$args);
    }
    private function _prepareArgs($instance) {
        $args = $this->_args;
        for($i=0; $i < count($args); $i++){
            if ($args[$i] instanceof InjectionHelper) {
                $helper = $args[$i];
                $args[$i] = $helper->prepareArg($instance);
            }
        }
        return $args;
    }
}

Вы также можете передать InjectionHelper в качестве аргумента.InjectionHelper действует как еще один контейнер обратного вызова - таким образом, если вам нужно передать какую-либо информацию о лениво загружаемом объекте в его внедренный DAO, вам не придется жестко кодировать ее в объекте.Кроме того, если вам нужно объединить методы в один канал - скажем, вам нужно передать $this->getDepartment()->getManager()->getId() введенному DAO по любой причине - вы можете.Просто передайте его как getDepartment|getManager|getId конструктору InjectionHelper.

class InjectionHelper {
    private $_callback;
    public function __construct( $callback ) {
        if (!is_string($callback)) throw new Exception;
        $this->_callback = $callback;
    }
    public function prepareArg( $instance ) {
        if (!is_object($instance)) throw new Exception;
        $callback = explode("|",$this->_callback);
        $firstCallback = $callback[0];
        $result = $instance->$firstCallback();
        array_shift($callback);
        if (!empty($callback) && is_object($result)) {
            for ($i=0; $i<count($callback); $i++) {
                $result = $result->$callback[$i];
                if (!is_object($result)) break;
            }
        }
        return $result;
    }
}

Чтобы реализовать эту функцию в объекте, вам понадобятся инъекции при создании, чтобы гарантировать, что объект имеет или может получить всю необходимую ему информацию.Каждый метод, использующий инъекцию, просто вызывает метод execute () соответствующего DAOInjection.

class Some_Object {
    private $_childInjection;
    private $_parentInjection;
    public function __construct(DAOInjection $childInj, DAOInjection $parInj) {
        $this->_childInjection = $childInj;
        $this->_parentInjection = $parInj;
    }
    public function getChildObjects() {
        if ($this->_children == null)
            $this->_children = $this->_childInjection->execute($this);
        return $this->_children;
    }
    public function getParentObjects() {
        if ($this->_parent == null)
            $this->_parent = $this->_parentInjection->execute($this);
        return $this->_parent;
    }
}

Затем я бы в конструкторе моего класса обслуживания создал бы сопоставления, относящиеся к этой службе, используя соответствующий DAOInjection.классы как аргументы для конструкторов картографов.Затем мапперы позаботятся о том, чтобы каждый объект имел свои инъекции, потому что работа маппера состоит в том, чтобы возвращать завершенные объекты и обрабатывать сохранение / удаление объектов, в то время как работа сервиса заключается в координации отношений между различными мапперами, объектами и т. Д.on.

В конечном итоге вы можете использовать его для внедрения обратных вызовов в службы ИЛИ мапперам, например, вы хотите, чтобы ваш объект Ticket извлекал родительского пользователя, который находится за пределами сферы действия Ticket Service.- служба тикетов может просто ввести обратный вызов в «Сервис пользователя», и ей не нужно будет ничего знать о том, как DAL работает с другими объектами.

Надеюсь, это поможет!

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...