4

背景

这是一个漫长而复杂的问题。我使用 php/MySQL 作为示例(因为它是一个实际示例),但这在理论上可以适用于其他语言。忍受我。

没有 ORM 的 MVC 框架

我正在创建一个使用自己的框架的应用程序。由于各种原因,不接受以下对此问题的回答:

  • 为什么不直接使用X框架?
  • 为什么不直接使用XORM?

该应用程序执行自己的查询(由我编写),这就是我现在希望它保留的方式。

商业逻辑?

“业务逻辑”似乎是一个毫无意义的流行词,但我认为它本质上是指

查询和基于这些查询构建结果集的逻辑

我还读到 MVC 中的模型应该执行所有业务逻辑。

User.php是 884 行

现在我已经让我的应用程序运行得很好,我想对其进行重构,以免出现这种可憎的情况。 User.php本质上是用户的模型(显然)。我在其中看到了一些我可以轻松摆脱的责任,但我遇到的一个主要障碍是:

如何协调 SOLID 与 MVC?

User.php增长如此之大的原因是因为我执行的任何查询都需要该文件中的用户成员。用户用于大量操作(它需要做的不仅仅是CRUD),因此任何需要userid,username等的查询都由该文件中的函数运行。显然查询应该在模型中(而不是控制器),但我觉得这绝对应该以某种方式分开。我需要完成以下工作:

  • 创建一个 API,以划分的方式涵盖所有这些必要的查询
  • 避免在不必要时授予对 DB 连接类的访问权限
  • User向视图添加数据(User.php现在正在这样做——视图对象是由 setter 注入的,我认为这也很糟糕)。

...所以我可以做的是创建其他对象,例如UserBranchManager, UserSiteManager,UserTagManager等,每个对象都可以有相关的查询和注入的 DB 对象来运行这些查询,但是他们如何获得User::$userid他们需要运行的令人垂涎的东西这些查询?不仅如此,我怎么能通过Branch::$branchid?这些成员不应该是私人的吗?为它们添加吸气剂也使这毫无意义。

我也不确定在哪里画出一个物体应该做多少的界限。许多操作相似但仍然不同。每个人的一个类将是巨大的矫枉过正。

可能的答案

如果我无法获得任何帮助,我会做(或至少尝试做)是为上面的对象(例如)构建某种依赖注入容器UserBranchManager并将它们注入相关控制器。这些将有一个DBQuery对象。该Query对象可以传递给低级模型(如User)以根据需要绑定参数,而更高级别的模型或它们所调用的任何东西都会将结果返回给控制器,控制器也会根据需要将数据添加到模板中。我看到的一些可能的障碍是创建适当的合约(例如,UserController最好依赖于用户模型的一些抽象),但不可避免地需要一些细节,尤其是在视图方面。

谁能提供一些智慧来回答我漫无边际的问题?

回复@tereško

他不仅在这里提供了一个很好的答案,而且还在How should a model be structure in MVC?

代码

根据要求,这里有一些极其精简的代码(基本上服务于一个请求)。一些重要的注意事项:

  • 现在,控制器不是类,只是文件
    • 控制器还处理很多路由
  • 没有“视图”对象,只有模板
  • 这可能看起来很糟糕

这些也是需要改进的地方,但我最担心的是模型(User特别是因为它失控了):

#usr.php -- controller
$route = route();
$user = '';
$branch = '<TRUNK>';
if (count($route) > 0) {
   if (count($route) > 1) {
      list($user, $branch) = $route;
   }
   else {
      list($user) = $route;
   }
}

$dec = new Decorator('user');
$dec->css('user');

if (isset($_SESSION['user']) && $_SESSION['user']->is($user)) {
   $usr = $_SESSION['user'];
}
else {
   $usr = new User(new DB, $user);
}
$usr->setUpTemplate($dec, $branch);
return $dec->execute();

# User.php -- model
class User {
   private $userid;
   private $username;
   private $db;

   public function __construct(DB $db, $username = null) {
      $this->username = $username;
      $this->db = $DB;
   }

   public function is($user) {
      return strtolower($this->username) === strtolower($user);
   }

   public function setUpTemplate(Decorator $dec, $branch) {
      $dec->_title = "$this->username $branch";
      // This function runs a moderately complicated query
      // joining the branch name and this user id/name
      $dec->branch = $this->getBranchDisplay($branch);
   }
}

关于答案的问题

在这里回答:

  • 您谈论离开缓存/身份验证/授权。这些重要吗?为什么他们不被覆盖?它们与模型/控制器/路由器有什么关系?
  • Data Mapper示例具有具有诸如..之Person类的方法的类getExemption,这些是什么?isFlaggedForAudit这些计算似乎需要数据库数据,那么它是如何获得它们的呢? Person Mapper离开select。这不重要吗?
  • 什么是“领域逻辑”?

回答 5863870(特别是代码示例):

  • 这些工厂对象不应该是抽象(而不是依赖于通过创建new)还是这些是特殊的?
  • 您的 API 如何包含必要的定义文件?
  • 我已经阅读了很多关于在构造函数中注入依赖项的最佳方式(如果它们是强制性的)。我假设您以这种方式设置工厂,但为什么不是对象/映射器/服务本身呢?抽象呢?
  • 您是否担心代码重复(例如,大多数模型_object_factory在其类定义中需要一个成员)?如果是这样,你怎么能避免这种情况?
  • 你正在使用protected. 为什么?

如果您可以提供任何特定的代码示例,那将是最好的,因为这样我更容易拿起东西。

我理解你的回答所说的理论,这很有帮助。我仍然感兴趣(但不完全确定)的是确保以最佳方式处理此 API 中对象的依赖关系(最糟糕的方式new无处不在)。

4

2 回答 2

2

不要混淆 SOLID(你可以在我的博客上得到一个很好的解释:http: //crazycoders.net/2012/03/confoo-2012-make-your-project-solid/

在考虑围绕您尝试构建的应用程序的框架时,SOLID 非常棒。数据本身的管理是另一回事。您不能真正将 SOLID 的 S 的单一职责应用于依赖于其他业务模型(例如用户、组和权限)的业务模型。

因此,当您构建应用程序时,您必须释放一些 SOLID。SOLID 的好处是确保您的应用程序背后的框架是强大的。

例如,如果您构建自己的框架和业务模型,您可能会有一个基类 MODEL 另一个用于 DATABASEACCESS,只要记住您的 MODEL 不应该知道如何获取数据,只需知道它必须获取数据。

例如:

Good: 
    - MyApp_Models_User extends MyApp_Framework_Model
    - MyApp_Models_Group extends MyApp_Framework_Model
    - MyApp_Models_Permission extends MyApp_Framework_Model
    - MyApp_Framework_Model
    - MyApp_Framework_Provider
    - MyApp_Framework_MysqliProvider extends MyApp_Framework_Provider

      In this good part, you create a model like this:
      $user = new MyApp_Models_User(new MyApp_Framework_MysqliProvider(...));
      $user->load(1234);

这样,您将防止单一责任的失败,您的提供者用于从存在的众多提供者之一加载数据,并且您的模型代表您提取的数据,它不知道如何读取或写入数据,这就是提供者的工作......

Bad way:
    - MyApp_Model_User
    - MyApp_Model_Group
    - MyApp_Model_Permission

      define('MyDB',   'localhost');
      define('MyUser', 'user');
      define('MyPass', 'pass');
      $user = new MyApp_Models_User(1234);

使用这种糟糕的方法,你首先打破了单一的责任,你的模型代表了一些东西,还管理了数据的输入/输出。此外,您通过声明您需要为模型定义常量以连接到数据库并完全抽象数据库方法来创建依赖项,如果您需要更改它们并拥有 37 个模型,您将有大量工作要做...

现在,如果你想以糟糕的方式工作,你可以……我仍然这样做,我知道,但有时,当你有蹩脚的结构并想要重构时,你可以而且应该违背一个原则,只是为了正确而缓慢地重构,然后,再重构一点,最终得到一些 SOLID 和 MVC 外观。

请记住,SOLID 并不适用于所有事情,它只是一个指导方针,但它是非常非常好的指导方针。

于 2012-07-19T13:39:47.387 回答
1

好吧..这取决于./user.php文件中的实际内容。如果我不得不猜测,你会遇到这样的情况,你的用户“模型”有太多的责任。基本上,您违反了单一责任原则,并且不确定如何解决该问题。

你没有提供任何代码..所以,让我们继续猜测......

您的用户“模型”可能正在实施活动记录模式。这将是 SRP 问题的主要来源。您可以观看这部分的讲座 幻灯片。它将解释其中的一部分。重点将是使用类似于数据映射器模式的东西,而不是使用活动记录。

此外,您可能会注意到一些适用于User类实例的域逻辑似乎发生在您的“模型”之外。以不同的结构分离该部分可能是有益的。否则,您将强制控制器内部的域逻辑。也许这个评论可以对整个主题有所启发。

您可能在用户“模型”中塞满的另一件事可能是授权(不要与身份验证混淆)机制的一部分。将这一责任分开可能是务实的。

更新

  • 您谈论离开缓存/身份验证/授权。这些重要吗?为什么他们不被覆盖?它们与模型/控制器/路由器有什么关系?

    缓存是您稍后将在应用程序中添加的东西。域对象不关心数据来自哪里。因此,您可以在服务级别对象或现有数据映射器中添加缓存。我建议选择前一个选项,因为更改现有映射器可能会产生无法预料的副作用。而且因为它只会使现有的映射器过于复杂。

    namespace Application\Service;
    
    class Community{
    
        public function getUserDetails( $uid )
        {
            $user = $this->domainObjectFactory->build('User');
            $cache = $this->cacheFactory->build('User');
    
            $user->setId( $uid );
    
            try
            {
                $cache->pull( $user );
            }
            cache( NotFoundException $e)
            { 
                $mapper = $this->mapperFactory->build('User');
                $mapper->pull( $user );
                $cache->push( $user );
            }
    
            return $user->getDetails();
    
        }
    }
    

    这将说明基于用户 ID 的非常简化的用户信息获取。该代码创建域对象并为其提供 ID,然后将此$user对象用作搜索缓存详细信息的条件,或者,如果失败,则通过数据映射器从数据库中提取该信息。此外,如果成功,则将详细信息推送到缓存中,以备下次使用。

    您可能会注意到,当映射器在存储(通常 - SQL 数据库)中找不到具有此类 ID 的用户时,此示例未处理这种情况。正如我所说,这是一个简化的示例。

    此外,您可能会注意到,可以根据具体情况轻松添加这种缓存,并且不会彻底改变您的逻辑行为方式。

    授权是另一部分,它不应该直接影响您的业务逻辑。我已经链接了我提供身份验证的首选方式。这个想法是,不是检查控制器内部的凭据(如这里这里这里这里),而是在控制器上执行方法之前检查访问权限。这样,您就可以使用其他选项来处理拒绝访问,而不会被困在特定的控制器中。

  • Data Mapper 示例的类具有诸如..之Person类的方法getExemption(),它们是什么?isFlaggedForAudit()这些计算似乎需要数据库数据,那么它是如何获得它们的呢?Person Mapper离开选择。这不重要吗?

    该类Person是一个域对象。它将包含与该实体直接关联的域逻辑部分。

    对于要执行的那些方法,映射器应该首先加载数据。在 PHP 中,它看起来像这样:

    $person = new Person;
    $mapper = new PersonMapper( $databaseConnection );
    
    $person->setId( $id );
    $mapper->fetch( $person );
    
    if ( $person->isFlaggedForAudit() )
    {
        return $person->getTaxableEearnings();
    }
    

    中的方法名称PersonMapper作为示例,以便您了解该类应该如何使用。我通常命名方法fetch()store()并且remove()(或 push/pull/remove ...取决于我使用了多少 GIT)。恕我直言,没有必要单独使用update()insert()方法。如果对象的数据最初是由 mapper 检索的,那么它是一个UPDATE. 如果不是 - 它是一个INSERT. 您还可以确定它对应的值是否PRIMARY KEY已设置(在某些情况下,至少)。

  • 什么是“领域逻辑”?

    它是代码的一部分,它知道如何创建发票并为特定产品应用折扣价。这也是确保您不提交注册表单的代码,您不声明您出生于 1592 年。

    MVC 由两层组成:表示层(可以包含:视图、模板、控制器)和模型层(可以包含:服务、域对象、映射器)。表示层处理用户交互和响应。模型层处理业务和验证规则。你可以说领域业务逻辑是模型中的一切,不涉及存储。

    我想没有简单的方法可以解释。

  • 这些工厂对象不应该是抽象(而不是依赖于通过 new 创建)还是这些是特殊的?

    哪些“工厂对象”,在哪里?你说的是这个片段吗?

    $serviceFactory = new ServiceFactory(
        new DataMapperFactory( $dbhProvider ),
        new DomainObjectFactory
        );
    $serviceFactory->setDefaultNamespace('Application\\Service');
    

    整个代码片段应该在bootstrap.php文件中(或者您可能正在使用index.phpinit.php为此)。它是应用程序的入口点。它不是任何类的一部分,因此你不能在那里“紧密耦合”。没有什么可以“耦合”的。

  • 您的 API 如何包含必要的定义文件?

    该片段不是整个bootstrap.php文件。上面的代码是自动加载器的包含和初始化。我目前正在使用动态加载器,它允许来自同一命名空间的类位于不同的文件夹中。

    此外,这种自动加载器是开发阶段的工件。在生产代码中,您必须使用加载器,它与预定义的哈希表一起使用,并且不需要实际遍历命名空间和位置树。

  • 我已经阅读了很多关于在构造函数中注入依赖项的最佳方式(如果它们是强制性的)。我假设您以这种方式设置工厂,但为什么不是对象/映射器/服务本身呢?抽象呢?

    你在说什么 ?!?

  • 您是否担心代码重复(例如,大多数模型在其类定义中需要 _object_factory 成员)?如果是这样,你怎么能避免这种情况?

    你真的看过评论中的代码片段有多久了?!?!?

  • 您正在使用受保护的。为什么?

    因为,如果值/方法是用protected可见性定义的,您可以在扩展原始类时访问它们。此外,该代码示例是为旧版本的 answer 制作的。检查日期。

和不。我不会提供任何具体的代码示例。因为每个情况都不一样。如果您想进行复制粘贴开发,请使用 CakePHP 或 CodeIgniter。

于 2012-07-19T14:41:19.830 回答