帮助使用DRY原则在服务类中创建一个灵活的“find”方法 [英] Help creating a flexible base 'find' method in a service class using the DRY principle

查看:161
本文介绍了帮助使用DRY原则在服务类中创建一个灵活的“find”方法的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

多年以来,我一直在重复执行相同的代码(与进化),而没有找到一些干净,有效地抽象出来的方法。



该模式是我的服务层中的基础find [Type] s方法,它将选择查询创建抽象到服务中的一个点,但支持快速创建更容易使用的代理方法的能力(请参见示例PostServivce :: getPostById ()方法下面)。



不幸的是,迄今为止,我一直无法满足这些目标:



< $>
  • 减少由不同的重新实现引入的错误的可能性

  • 将有效/无效的参数选项公开为IDE进行自动完成

  • 按照DRY原则

  • 我最近的实现通常看起来像下面的例子。该方法采用一系列条件和一系列选项,并从这些数组中创建并执行Doctrine_Query(我今天在这里重写了一遍,所以可能会有一些打字错误/语法错误,它不是一个直接的剪切和粘贴)。

      class PostService 
    {
    / * ... * /

    / **
    *返回一组帖子
    *
    * @param Array $ conditions可选。格式为
    * array('condition1'=>'value',...)的条件数组
    * @param Array $ options可选。一个选项数组
    * @return Array一个post对象的数组,如果条件没有匹配,则为false
    * /
    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 ='=';
    //这部分特别讨厌:(
    //允许条件操作符规范像
    //'slug LIKE'=>'foo%',
    //' comment_count> ='=> 1,
    //'approved NOT'=> null,
    //'id NOT IN'=>数组(...),
    if(false!==($ spacePos = strpos($ conditions,''))){
    $ operator = substr($ condition,$ spacePost + 1);
    $ conditionStr = substr ,0,$ spacePos);

    / * ... snip验证匹配条件,抛出异常... * /
    if(substr($ operatorStr,0,4)==' NOT $){
    $ not = true;
    $ operatorStr = substr($ operatorStr,4);
    }
    if($ operatorStr =='IN'){
    $ in = true;
    } elseif($ operatorStr =='NOT'){
    $ not = true;
    } else {
    / * ... snip验证匹配条件,抛出异常... * /
    $ operator = $ operatorStr;
    }

    }

    开关($ condition){
    //加入表条件
    case'Author.role':
    case'Author.id':
    //硬设置包含作者表
    $ options ['includeAuthor'] = true;

    // break;有意省略
    / * ...删除其他类似的情况,省略中断... * /
    //允许条件下降到逻辑低于

    //模型具体条件字段
    case'id':
    case'title':
    case'body':
    / * ...剪切各种有效条件... * /
    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异常(未知条件$条件);
    }
    }

    //进程选项

    // init一些后来的处理标记
    $ includeAuthor = $ includeCategories = $ paginate =假;
    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':
    / * ...设置一个学说水合模式为$ hydration * /
    break;
    default:
    throw new Exception(Invalid option'$ option');
    }
    }

    //管理一些标志...
    如果($ includeAuthor){
    $ q-> leftJoin('p。作者a')
    - > addSelect('a。*');
    }

    如果($ paginate){
    / * ...在一些自定义的Doctrine Zend_Paginator类中包装查询... * /
    return $ paginator;
    }

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

    / * ...剪切... * /
    }

    Phewf



    此基本功能的好处是:


    1. 它允许我快速支持新的条件和选项,因为模式演变

    2. 它允许我快速实现查询的全局条件例如,添加一个默认为true的excludeDisabled选项,并且过滤所有disabled = 0模型,除非调用者明确地说出不同的方式)。

    3. 它允许我快速创建新的,更简单的使用方法,代理调用回到findPosts方法。例如:



      class PostService 
    {
    / * ... snip ... * /

    //一个代理getPosts将结果限制为1并返回该元素
    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);
    }
    返回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()){
    $条件['Author.id'] = $ id;
    return $ this-> getPosts($ conditions,$ options);
    }

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

    这种方法的缺点是:




      <在每个模型访问服务中都创建了同样的单一find [Model] s方法,其中大部分只是条件切换结构和基表名称的更改。
    • 没有简单的方法执行AND / OR条件操作。所有条件都明确地进行了ANDed。

    • 引入了许多打字错误的机会。

    • 在基于约定的API中介绍许多关于休息的机会(例如,后来的服务可能需要执行不同的语法约定来指定orderBy选项,这对所有以前的服务的后端端口变得乏味)。

    • 违反DRY原则。

    • IDE自动完成解析器隐藏有效条件和选项,选项和条件参数需要冗长的文档块解释才能跟踪允许的选项。



    在过去的几天里,我试图开发一个更多的OO解决方案来解决这个问题,但是觉得我正在开发TOO复杂的解决方案,这个解决方案太僵硬,限制性很强。



    我正在努力的想法是符合以下条件的(目前的项目将是Doctrine2 fyi,所以稍微改变)...

     命名空间Foo\Service; 

    使用Foo\Service\PostService\FindConditions; //扩展一个常见的\Foo\FindConditions抽象
    使用Foo\FindConditions\Mapper\Dql作为DqlConditionsMapper;

    使用Foo\Service\PostService\FindOptions; //扩展一个通用的\Foo\FindOptions抽象
    使用Foo\FindOptions\Mapper\Dql作为DqlOptionsMapper;

    使用\Doctrine\ORM\QueryBuilder;

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

    / * ... snip实例化$ q作为Doctrine\ORM\QueryBuilder ... * /

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

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


    if($ conditionsMapper-> hasUnmappedConditions()){
    / * ..非常具体的条件处理... * /
    }
    if($ optionsMapper-> hasUnmappedConditions()){
    / * ..非常具体的条件处理... * /
    }

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

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

    最后,Foo\Service\PostService\FindConditions类的一个示例:

     命名空间Foo\Service\PostService; 

    使用Foo\Options\FindConditions作为FindConditionsAbstract;

    class FindConditions extends FindConditionsAbstract {

    protected $ _allowedOptions = array(
    'user_id',
    'status',
    'Credentials。凭证',
    );

    / * ... snip显式允许选项的get / set提供ide自动填充帮助* /
    }

    Foo\Options\FindConditions和Foo\Options\FindOptions非常相似,所以至少现在至少他们都扩展了一个常见的Foo\Options父类。这个父类处理允许的变量和默认值的初始化,访问集合选项,限制只访问定义的选项,并为DqlOptionsMapper提供一个循环访问选项的迭代器接口。



    不幸的是,在这几天黑客攻击之后,我对这个系统的复杂性感到沮丧。同样地,条件组和OR条件仍然不能得到支持,并且指定替代条件比较运算符的能力一直是创建一个Foo\Options\FindConditions\Comparison类围绕值的一个完整的错误,指定一个FindConditions值( $ conditions-> setCondition('Foo',new Comparison('NOT LIKE','bar')); )。



    我宁愿使用别人的解决方案,如果它存在,但我还没有遇到任何可以做我正在寻找的东西。



    我想超越这个过程,回到实际构建我正在开展的项目,但我甚至没有看到结束。



    所以,Stack Overflowers:
    - 有没有什么更好的方法可以提供我没有包含缺点的好处?

    解决方案

    我认为你是过度复杂的事情。



    我已经使用了一个使用Doctrine 2的项目,许多实体,不同的用途,各种服务,定制仓库等,我发现这样的东西比较好(对我来说)..



    1 。查询资料库



    首先,我通常不会在服务中进行查询。 Doctrine 2提供了EntityRepository和为每个实体进行子类化的选项。




    • 只要有可能,我使用标准的findOneBy。 ..和findBy ...风格的魔法方法。这样我就不必自己编写DQL,而且很好地开箱即用。

    • 如果我需要更复杂的查询逻辑,我通常会创建用例特定的存储库中的查找器。这些东西就像 UserRepository.findByNameStartsWith 和类似的东西。

    • 我通常不会创造超级花哨的我可以采取任何认为你给我!魔术师的类型。如果我需要一个特定的查询,我添加一个特定的方法。虽然这可能需要您编写更多的代码,但我认为这是一个更简单和更容易理解的方式。 (我试图通过你的寻找者代码,这是相当复杂的地方)



    所以换句话说...




    • 尝试使用什么教义已经给你(魔法查找器方法)

    • 如果您需要自定义查询逻辑,请使用自定义存储库类

    • 为每个查询类型创建一个方法



    2。用于组合非实体逻辑的服务



    使用服务将事务组合在您可以从控制器使用的简单界面之后,轻松测试单元测试。



    例如,假设您的用户可以添加好友。每当用户与其他人交朋友时,都会发送一封电子邮件给对方通知。这是您在服务中所需要的。



    您的服务将(例如)包含一个方法 addNewFriend which需要两个用户。然后,它可以使用存储库查询一些数据,更新用户的朋友数组,并调用一些其他类,然后发送电子邮件。



    您可以使用您的服务中的entitymanager用于获取存储库类或持久实体。



    3。实体特定逻辑的实体



    最后,您应该尝试将特定于实体的业务逻辑直接放入实体类。



    这种情况的一个简单的例子可能是,在上述情况下发送的电子邮件可能会使用某种问候方式。安德森先生或安德森女士。



    所以例如你需要一些逻辑来确定适当的问候语。这是你可以在实体类中使用的东西 - 例如, getGreeting 或某些东西,然后可以考虑用户的性别和国籍,并返回一些基于此的内容。 (假设性别和国籍将被存储在数据库中,而不是问候本身 - 问候语将由函数的逻辑计算)



    我也应该指出实体应该通常不知道实体管理者或存储库。如果逻辑需要其中之一,它可能不属于实体类本身。



    此方法的优点


    $ b $我发现我在这里详细介绍的方法工作得很好。它是可维护的,因为它通常对于什么事情是非常明显的,它不依赖于复杂的查询行为,并且因为事物被清楚地分解成不同的区域(回馈,服务,实体),所以单位测试是非常简单的好吧。


    For years now I've been reimplementing the same code over and over (with evolution) without finding some method of cleanly, and efficiently, abstracting it out.

    The pattern is a base 'find[Type]s' method in my service layers which abstracts select query creation to a single point in the service, but supports the ability to quickly create easier to use proxy methods (see the example PostServivce::getPostById() method way below).

    Unfortunately, so far, I've been unable to satisfy these goals:

    1. Reduce possibility for errors introduced by distinct re-implementation
    2. Expose valid/invalid parameter options to IDEs for autocompletion
    3. Follow the DRY principle

    My most recent implementation usually looks something like the following example. The method takes an array of conditions, and an array of options, and from these creates and executes a Doctrine_Query (I mostly rewrote this out here today, so there may be some typos/syntax errors, it's not a direct cut and paste).

    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

    The benefits of this base function are:

    1. it allows me to quickly support new conditions and options as the schema evolves
    2. it allows me a means to quickly implement global conditions on the query (for example, adding an 'excludeDisabled' option with a default of true, and filtering all disabled = 0 models, unless a caller explictly says differently).
    3. it allows me to quickly create new, simpler to use, methods which proxy calls back to the findPosts method. For example:

    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 ... */
    }
    

    The MAJOR drawbacks with this approach are:

    • The same monolithic 'find[Model]s' method gets created in every model-accessing service, with mostly only the condition switch construct and base table names changing.
    • No simple way to perform AND/OR conditon operations. All conditions explicitly ANDed.
    • Introduces many opportunities for typo errors
    • Introduces many opportinuties for breaks in the convention-based API (for example a later service may require implementing a different syntax convention for specifying the orderBy option, which becomes tedious to back-port to all previous services).
    • Violates DRY principles.
    • Valid conditions and options are hidden to IDE auto-completion parsers and the options and conditions parameters require lengthy doc block explanation to track allowed options.

    Over the last few days I've attempted to develop a more OO solution to this problem, but have felt like I'm developing TOO complex a solution which will be too rigid and restrictive to use.

    The idea I was working towards was something along the lines of the following (current project will be Doctrine2 fyi, so slight change there)...

    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 ... */
    }
    

    And lastly, a sample of the Foo\Service\PostService\FindConditions class:

    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 and Foo\Options\FindOptions are really quite similar, so, for now at least, they both extend a common Foo\Options parent class. This parent class handles intializing allowed variables and default values, accessing the set options, restricting access to only defined options, and providing an iterator interface for the DqlOptionsMapper to loop through options.

    Unfortunately, after hacking at this for a few days now, I'm feeling frustrated with the complexity of this system. As is, there is still no support in this for condition groups and OR conditions, and the ability to specify alternate condition comparison operators has been a complete quagmire of creating a Foo\Options\FindConditions\Comparison class wrap around a value when specifying an FindConditions value ($conditions->setCondition('Foo', new Comparison('NOT LIKE', 'bar'));).

    I'd much rather use someone else's solution if it existed, but I've yet to come across anything that does what I'm looking for.

    I'd like to get beyond this process and back to actually building the project I'm working on, but I don't even see an end in sight.

    So, Stack Overflowers: - Is there any better way that provides the benefits I've identified without including the drawbacks?

    解决方案

    I think you're overcomplicating things.

    I've worked on a project using Doctrine 2 which has quite a lot of entities, different uses for them, various services, custom repositories etc. and I've found something like this works rather well (for me anyway)..

    1. Repositories for queries

    Firstly, I don't generally do queries in services. Doctrine 2 provides the EntityRepository and the option of subclassing it for each entity for this exact purpose.

    • Whenever possible, I use the standard findOneBy... and findBy... style magic methods. This saves me from having to write DQL myself and works rather nicely out of the box.
    • If I need more complicated querying logic, I usually create use-case specific finders in the repositories. These are things like UserRepository.findByNameStartsWith and things like that.
    • I generally don't create a super fancy "I can take any args you give me!" type of magic finders. If I need a specific query, I add a specific method. While this may seem like it requires you to write more code, I think it's a much simpler and easier to understand way of doing things. (I tried going through your finder code and it was rather complicated looking in places)

    So in other words...

    • Try to use what doctrine already gives you (magic finder methods)
    • Use custom repository classes if you need custom querying logic
    • Create a method per query type

    2. Services for combining non-entity logic

    Use services to combine "transactions" behind a simple interface you can use from your controllers or test easily with unit tests.

    For example, let's say your users can add friends. Whenever a user friends someone else, an email is dispatched to the other person to notify. This is something you would have in your service.

    Your service would (for example) include a method addNewFriend which takes two users. Then, it could use a repository to query for some data, update the users' friend arrays, and call some other class which then sends the email.

    You can use the entitymanager in your services for getting repository classes or persisting entities.

    3. Entities for entity-specific logic

    Lastly you should try to put your business logic that is specific to an entity directly into the entity class.

    A simple example for this case could be that maybe the email sending in the above scenario uses some sort of a greeting.. "Hello Mr. Anderson", or "Hello Ms. Anderson".

    So for example you would need some logic to determine the appropriate greeting. This is something you could have in the entity class - For example, getGreeting or something, which could then take into account the user's gender and nationality and return something based on that. (assuming gender and nationality would be stored in the database, but not the greeting itself - the greeting would be calculated by the function's logic)

    I should probably also point out that the entities should generally not know of either the entitymanager or the repositories. If the logic requires either of these, it probably doesn't belong into the entity class itself.

    Benefits of this approach

    I have found the approach I've detailed here works rather well. It's maintainable because it generally is quite "obvious" to what things do, it doesn't depend on complicated querying behavior, and because things are split clearly into different "areas" (repos, services, entities) it's quite straightforward to unit test as well.

    这篇关于帮助使用DRY原则在服务类中创建一个灵活的“find”方法的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

    查看全文
    登录 关闭
    扫码关注1秒登录
    发送“验证码”获取 | 15天全站免登陆