Помогите создать гибкий базовый метод 'find' в классе обслуживания, используя принцип DRY - PullRequest
5 голосов
/ 08 июля 2011

В течение многих лет я переопределял один и тот же код снова и снова (с развитием), не находя какой-то метод чистого и эффективного его абстрагирования.

Шаблон - это базовый 'find [Тип] '' в моих сервисных слоях, который абстрагирует создание запроса на выборку от одной точки в сервисе, но поддерживает возможность быстрого создания более простых в использовании прокси-методов (см. пример метода PostServivce :: getPostById () ниже).

К сожалению, до сих пор мне не удавалось достичь этих целей:

  1. Уменьшить вероятность ошибок, вызванных отдельной повторной реализацией
  2. Предоставить допустимые / недействительные параметры параметровв IDE для автозаполнения
  3. Следуйте принципу DRY

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

class PostService
{
    /* ... */

    /**
     * Return a set of Posts
     *
     * @param Array $conditions Optional. An array of conditions in the format
     *                          array('condition1' => 'value', ...)
     * @param Array $options    Optional. An array of options 
     * @return Array An array of post objects or false if no matches for conditions
     */
    public function getPosts($conditions = array(), $options = array()) {
        $defaultOptions =  = array(
            'orderBy' => array('date_created' => 'DESC'),
            'paginate' => true,
            'hydrate' => 'array',
            'includeAuthor' => false,
            'includeCategories' => false,
        );

        $q = Doctrine_Query::create()
                        ->select('p.*')
                        ->from('Posts p');

        foreach($conditions as $condition => $value) {
            $not = false;
            $in = is_array($value);
            $null = is_null($value);                

            $operator = '=';
            // This part is particularly nasty :(
            // allow for conditions operator specification like
            //   'slug LIKE' => 'foo%',
            //   'comment_count >=' => 1,
            //   'approved NOT' => null,
            //   'id NOT IN' => array(...),
            if(false !== ($spacePos = strpos($conditions, ' '))) {
                $operator = substr($condition, $spacePost+1);
                $conditionStr = substr($condition, 0, $spacePos);

                /* ... snip validate matched condition, throw exception ... */
                if(substr($operatorStr, 0, 4) == 'NOT ') {
                  $not = true;
                  $operatorStr = substr($operatorStr, 4);
                }
                if($operatorStr == 'IN') {
                    $in = true;
                } elseif($operatorStr == 'NOT') {
                    $not = true;
                } else {
                    /* ... snip validate matched condition, throw exception ... */
                    $operator = $operatorStr;
                }

            }

            switch($condition) {
                // Joined table conditions
                case 'Author.role':
                case 'Author.id':
                    // hard set the inclusion of the author table
                    $options['includeAuthor'] = true;

                    // break; intentionally omitted
                /* ... snip other similar cases with omitted breaks ... */
                    // allow the condition to fall through to logic below

                // Model specific condition fields
                case 'id': 
                case 'title':
                case 'body':
                /* ... snip various valid conditions ... */
                    if($in) {
                        if($not) {
                            $q->andWhereNotIn("p.{$condition}", $value);
                        } else {
                            $q->andWhereIn("p.{$condition}", $value);
                        }
                    } elseif ($null) {
                        $q->andWhere("p.{$condition} IS " 
                                     . ($not ? 'NOT ' : '') 
                                     . " NULL");
                    } else {
                        $q->andWhere(
                            "p.{condition} {$operator} ?" 
                                . ($operator == 'BETWEEN' ? ' AND ?' : ''),
                            $value
                        );
                    }
                    break;
                default:
                    throw new Exception("Unknown condition '$condition'");
            }
        }

        // Process options

        // init some later processing flags
        $includeAuthor = $includeCategories = $paginate = false;
        foreach(array_merge_recursivce($detaultOptions, $options) as $option => $value) {
            switch($option) {
                case 'includeAuthor':
                case 'includeCategories':
                case 'paginate':
                /* ... snip ... */
                    $$option = (bool)$value;
                    break;
                case 'limit':
                case 'offset':
                case 'orderBy':
                    $q->$option($value);
                    break;
                case 'hydrate':
                    /* ... set a doctrine hydration mode into $hydration */ 
                    break;
                default:
                    throw new Exception("Invalid option '$option'");
            }
        }

        // Manage some flags...
        if($includeAuthor) {
            $q->leftJoin('p.Authors a')
              ->addSelect('a.*');
        } 

        if($paginate) {
            /* ... wrap query in some custom Doctrine Zend_Paginator class ... */
            return $paginator;
        }

        return $q->execute(array(), $hydration);
    }

    /* ... snip ... */
}

Phewf

Преимущества этой базовой функции:

  1. , что позволяет мне быстро поддерживать новые условия и параметры, каксхема развивается
  2. , что позволяет мне быстро реализовывать глобальные условия в запросе (например, добавив опцию 'excludeDisabled' со значением по умолчанию true и отфильтровав все модели disabled = 0, если только вызывающая сторона явно не указана)говорит по-другому).
  3. это позволяет мне быстро создавать новые, более простые в использовании методы, которые прокси вызывает обратно к методу findPosts.Например:
class PostService
{
    /* ... snip ... */

    // A proxy to getPosts that limits results to 1 and returns just that element
    public function getPost($conditions = array(), $options()) {
        $conditions['id'] = $id;
        $options['limit'] = 1;
        $options['paginate'] = false;
        $results = $this->getPosts($conditions, $options);
        if(!empty($results) AND is_array($results)) {
            return array_shift($results);
        }
        return false;
    }

    /* ... docblock ...*/       
    public function getPostById(int $id, $conditions = array(), $options()) {
        $conditions['id'] = $id;
        return $this->getPost($conditions, $options);
    }

    /* ... docblock ...*/
    public function getPostsByAuthorId(int $id, $conditions = array(), $options()) {
        $conditions['Author.id'] = $id;
        return $this->getPosts($conditions, $options);
    }

    /* ... snip ... */
}

Недостатки MAJOR при таком подходе:

  • Та же самая монолитная 'находка [Метод Model] s создается в каждой службе, обращающейся к модели, при этом в основном меняются только конструкция переключателя условия и имена базовых таблиц.
  • Нет простого способа выполнения операций И ​​/ ИЛИ.Все условия в явном виде ANDed.
  • Вводит множество возможностей для ошибок опечаток
  • Вводит множество возможностей для разрывов в API, основанном на соглашениях (например, для более поздней службы может потребоваться реализация другого соглашения о синтаксисе для указанияопция orderBy, которая становится утомительной для обратного переноса на все предыдущие службы).
  • Нарушение принципов DRY.
  • Допустимые условия и параметры скрыты для анализаторов автозаполнения IDE и параметров параметров и условийтребуется длинное объяснение блока документации для отслеживания разрешенных опций.

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

Идея, над которой я работал, была чем-то вроде следующего (текущий проект будет Doctrine2 fyi, так что небольшие изменения там) ...

namespace Foo\Service;

use Foo\Service\PostService\FindConditions; // extends a common \Foo\FindConditions abstract
use Foo\FindConditions\Mapper\Dql as DqlConditionsMapper;

use Foo\Service\PostService\FindOptions; // extends a common \Foo\FindOptions abstract
use Foo\FindOptions\Mapper\Dql as DqlOptionsMapper;

use \Doctrine\ORM\QueryBuilder;

class PostService
{
    /* ... snip ... */
    public function findUsers(FindConditions $conditions = null, FindOptions $options = null) {

        /* ... snip instantiate $q as a Doctrine\ORM\QueryBuilder ... */

        // Verbose
        $mapper = new DqlConditionsMapper();
        $q = $mapper
                ->setQuery($q)
                ->setConditions($conditions)
                ->map();

        // Concise
        $optionsMapper = new DqlOptionsMapper($q);        
        $q = $optionsMapper->map($options);


        if($conditionsMapper->hasUnmappedConditions()) {
            /* .. very specific condition handling ... */
        }
        if($optionsMapper->hasUnmappedConditions()) {
            /* .. very specific condition handling ... */
        }

        if($conditions->paginate) {
            return new Some_Doctrine2_Zend_Paginator_Adapter($q);
        } else {
            return $q->execute();
        }
    }

    /* ... snip ... */
}

И, наконец, образец Foo \ SerВайс \ PostService \ FindConditions класс:

namespace Foo\Service\PostService;

use Foo\Options\FindConditions as FindConditionsAbstract;

class FindConditions extends FindConditionsAbstract {

    protected $_allowedOptions = array(
        'user_id',
        'status',
        'Credentials.credential',
    );

    /* ... snip explicit get/sets for allowed options to provide ide autocompletion help */
}

Foo \ Options \ FindConditions и Foo \ Options \ FindOptions действительно очень похожи, поэтому, по крайней мере, на данный момент они оба расширяют общий родительский класс Foo \ Options.Этот родительский класс обрабатывает инициализацию разрешенных переменных и значений по умолчанию, получает доступ к заданным параметрам, ограничивает доступ только к определенным параметрам и предоставляет интерфейс итератора для DqlOptionsMapper для циклического перебора параметров.

К сожалению, после взлома этого дляЧерез несколько дней я чувствую разочарование по поводу сложности этой системы.Как и раньше, в этой группе до сих пор нет поддержки групп условий и условий ИЛИ, и возможность указать альтернативные операторы сравнения условий была полным затруднением при создании класса Foo \ Options \ FindConditions \ Comparison, обернутого вокруг значения при указании FindConditions.значение ($conditions->setCondition('Foo', new Comparison('NOT LIKE', 'bar'));).

Я бы предпочел использовать чье-то решение, если бы оно существовало, но мне еще не приходилось сталкиваться с тем, что делает то, что я ищу.

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

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

1 Ответ

4 голосов
/ 09 июля 2011

Я думаю, вы слишком усложняете вещи.

Я работал над проектом, использующим Doctrine 2, который имеет довольно много сущностей, различное использование для них, различные сервисы, пользовательские репозитории и т. Д., И я 'мы нашли что-то вроде этого, работает довольно хорошо (для меня в любом случае) ..

1.Репозитории для запросов

Во-первых, я обычно не делаю запросы в сервисах.Doctrine 2 предоставляет EntityRepository и возможность подкласса его для каждой сущности для этой конкретной цели.

  • По мере возможности я использую стандартные магические методы в стиле findOneBy ... и findBy ....Это избавляет меня от необходимости писать DQL самостоятельно и довольно неплохо работает из коробки.
  • Если мне нужна более сложная логика запросов, я обычно создаю специфичные для прецедента средства поиска в репозиториях.Это такие вещи, как UserRepository.findByNameStartsWith и тому подобное.
  • Я обычно не создаю супер-фантазий "Я могу взять любые аргументы, которые вы мне дадите!"тип магических искателей.Если мне нужен конкретный запрос, я добавляю определенный метод.Хотя может показаться, что вам нужно написать больше кода, я думаю, что это гораздо проще и понятнее для понимания.(Я пытался просмотреть ваш код поиска, и он был довольно сложным, глядя местами)

Другими словами ...

  • Попробуйтеиспользуйте то, что доктрина уже дает вам (методы магического поиска)
  • Используйте пользовательские классы репозитория, если вам нужна пользовательская логика запросов
  • Создайте метод для каждого типа запроса

2.Сервисы для объединения логики, не связанной с сущностью

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

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

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

Вы можете использовать entitymanager в своих службах для получения классов хранилища илипостоянные сущности.

3.Сущности для логики, специфичной для сущности

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

Простым примером для этого случая может быть то, что, возможно,При отправке электронной почты в приведенном выше сценарии используется какое-то приветствие. «Здравствуйте, мистер Андерсон» или «Здравствуйте, мистер Андерсон».

Так, например, вам понадобится логика для определения соответствующего приветствия.Это то, что вы могли бы иметь в классе сущностей - например, getGreeting или что-то еще, что могло бы тогда учитывать пол и национальность пользователя и возвращать что-то на основании этого.(при условии, что пол и национальность будут храниться в базе данных, но не само приветствие - приветствие будет рассчитываться по логике функции)

Вероятно, я также должен указать, что сущности обычно не знать либо о менеджере сущностей, либо о репозиториях.Если логика требует какой-либо из них, она, вероятно, не относится к самому классу сущностей.

Преимущества этого подхода

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

...