与 OOP 概念作斗争

Posted

技术标签:

【中文标题】与 OOP 概念作斗争【英文标题】:Struggling With OOP Concept 【发布时间】:2013-12-06 19:06:49 【问题描述】:

我真的为反复出现的 OOP/数据库概念而苦恼。

请允许我用伪 php 代码解释问题。

假设您有一个“用户”类,它在其构造函数中从users 表中加载其数据:

class User 
    public $name;
    public $height;

    public function __construct($user_id) 
        $result = Query the database where the `users` table has `user_id` of $user_id
        $this->name= $result['name'];
        $this->height = $result['height'];
    

简单,很棒。

现在,我们有一个“组”类,它从groups 表中加载其数据,并与groups_users 表连接,并根据返回的user_ids 创建user 对象:

class Group 
    public $type;
    public $schedule;
    public $users;

    public function __construct($group_id) 
        $result = Query the `groups` table, joining the `groups_users` table,
                    where `group_id` = $group_id
        $this->type = $result['type'];
        $this->schedule = $result['schedule'];

        foreach ($result['user_ids'] as $user_id) 
            // Make the user objects
            $users[] = new User($user_id);
        
    

一个组可以有任意数量的用户。

美丽、优雅、令人惊叹……在纸上。然而,实际上,创建一个新的组对象...

$group = new Group(21);  // Get the 21st group, which happens to have 4 users

...执行 5 个查询而不是 1 个。(组 1 个,每个用户 1 个。)更糟糕的是,如果我创建一个 community 类,其中有很多组,每个组中有很多用户,运行了非常多的查询!

不适合我的解决方案

多年来,我解决这个问题的方法是不以上述方式编码,而是在制作group 时,例如,我会将groups 表加入groups_users 表也到users 表,并在group 对象中创建一个类似用户对象的数组数组(从不使用/触摸user 类):

class Group 
    public $type;
    public $schedule;
    public $users;

    public function __construct($group_id) 
        $result = Query the `groups` table, joining the `groups_users` table,
                    **and also joining the `users` table,**
                    where `group_id` = $group_id
        $this->type = $result['type'];
        $this->schedule = $result['schedule'];

        foreach ($result['users'] as $user) 
            // Make user arrays
            $users[] = array_of_user_data_crafted_from_the_query_result;
        
    

...但是,当然,如果我创建一个“社区”类,在其构造函数中,我需要将communities 表与communities_groups 表与groups 表与@ 连接起来987654344@ 表与users 表。

...如果我创建一个“城市”类,在其构造函数中,我需要将cities 表与cities_communities 表与communities 表与communities_groups 表与groups 表与groups_users 表与users 表。

真是一场彻头彻尾的灾难!

我必须在具有一百万个查询的漂亮 OOP 代码 VS 之间做出选择吗? 1 查询并为每个超集手动编写这些连接?没有系统可以自动执行此操作吗?

我正在使用 CodeIgniter,并研究了无数其他 MVC,以及在其中构建的项目,如果不求助于我概述的两种有缺陷的方法之一,我找不到任何使用模型的好例子。

这似乎是以前从未做过的。

我的一位同事正在编写一个框架来实现这一点 - 您创建一个包含数据模型的类。其他更高模型可以包含该单个模型,它会制作和自动化表连接以创建包含对象实例化的更高模型较低的模型,全部在一个单个查询中。他声称他以前也从未见过这样做的框架或系统。

请注意: 我确实总是使用单独的类来实现逻辑和持久性。 (VO 和 DAO - 这是 MVC 的全部要点)。为了简单起见,我只是在这个思想实验中将两者结合起来,在类似 MVC 的架构之外。请放心,无论逻辑和持久性如何分离,此问题都会持续存在。我相信 this article,由 James 在这个问题下面的 cmets 中介绍给我,似乎表明我提出的解决方案(我多年来一直在关注)实际上是开发人员目前为解决这个问题所做的。然而,这个问题试图找到自动化该精确解决方案的方法,因此并不总是需要为每个超集手动编码。据我所知,这在 PHP 之前从未做过,我同事的框架将是第一个这样做的,除非有人能指出我这样做的。

而且,当然,我从不在构造函数中加载数据,我只在实际需要数据时调用我创建的 load() 方法。然而,这与这个问题无关,因为在这个思想实验中(以及在我需要自动化的现实情况中),我总是需要预先加载 的数据所有孩子的子集尽可能远,并且根据需要在未来某个时间点延迟加载它们。思想实验很简洁——它没有遵循最佳实践是一个有争议的问题,试图解决其布局的答案同样没有抓住重点。

编辑:为清楚起见,这是一个数据库架构。

CREATE TABLE `groups` (
  `group_id` int(11) NOT NULL,  <-- Auto increment
  `make` varchar(20) NOT NULL,
  `model` varchar(20) NOT NULL
)

CREATE TABLE `groups_users` ( <-- Relational table (many users to one group)
  `group_id` int(11) NOT NULL,
  `user_id` int(11) NOT NULL
)


CREATE TABLE `users` (
  `user_id` int(11) NOT NULL, <-- Auto increment
  `name` varchar(20) NOT NULL,
  `height` int(11) NOT NULL,
)

(还要注意,我最初使用了wheels 和cars 的概念,但那是愚蠢的,这个例子更清楚。)

解决方案:

我最终找到了一个可以做到这一点的 PHP ORM。它是Laravel's Eloquent。您可以指定模型之间的关系,它会使用如下语法智能地构建优化查询以进行预加载:

Group::with('users')->get();

这是一个绝对的救生员。我不必编写一个查询。它也不能使用连接,它可以智能地根据外键进行编译和选择。

【问题讨论】:

如果你仔细想想,你可能只有 3 个查询。 1 获得城市经销商,2 获得经销商汽车,1 获得车轮。如何检索数据取决于您,但您仍然可以使用优雅的 OOP 代码来反映对象。 @KennyThompson 我怀疑经销商实际上更接近于类似repository 的结构。仅仅因为您的数据库包含 10,000 辆汽车,并不意味着将它们全部转化为对象是可行的。 @Leng 这就是所谓的 N+1 问题,这是 ORM 的经典问题。您可以使用更有效的查询来拥有特定于组的 DAO。这是一篇关于该主题的文章,专门针对 PHP。您可以查找的其他概念是惰性加载和急切加载。 webadvent.org/2011/a-stitch-in-time-saves-nine-by-paul-jones @James Perfect,你已经回答了我的问题......查询和缝合正是我在上述问题的解决方案中提出的,这也是我多年来一直在做的事情。我想当我的同事发布他正在设计的这个框架时,他将是第一个创建真正的 OOP 渴望获取对象的模型(您不必自己构建连接和缝合 - 一旦您创建了子类,所有父类与之相关的类可以进行连接以急切地获取所有相关的孩子。) @Leng 如果想添加延迟加载,我听说 Java 中的 Hibernate 框架使用代理设计模式。 【参考方案1】:

假设您有一个“***”类,它从其构造函数中的***表中加载其数据

构造函数不应该做任何工作。相反,它们应该只包含分配。否则你很难测试实例的行为。

现在,我们有一个“汽车”类,它从与 cars_wheels 表连接的汽车表中加载其数据,并根据返回的 wheel_ids 创建车轮对象:

没有。这有两个问题。

您的Car 类不应同时包含用于实现“汽车逻辑”和“持久性逻辑”的代码。否则你会破坏SRP。***是类的依赖项,这意味着***应该作为构造函数的参数注入(很可能 - 作为***的集合,或者可能是一个数组)。

相反,您应该有一个映射器类,它可以从数据库中检索数据并将其存储在WheelCollection 实例中。还有一个汽车映射器,它将数据存储在Car 实例中。

$car = new Car;
$car->setId( 42 );
$mapper = new CarMapper( $pdo );
if ( $mapper->fetch($car) ) //if there was a car in DB

    $wheels = new WheelCollection;
    $otherMapper = new WheelMapper( $pdo );

    $car->addWheels( $wheels );
    
    $wheels->setType($car->getWheelType());
    // I am not a mechanic. There is probably some name for describing 
    // wheels that a car can use
    $otherMapper->fetch( $wheels );

类似的东西。在这种情况下,映射器负责执行查询。您可以为它们提供多个来源,例如:拥有一个检查缓存的映射器,如果失败,则仅从 SQL 中提取数据。

我真的必须在具有一百万个查询的漂亮 OOP 代码 VS 之间做出选择吗? 1 查询和恶心,un-OOP 代码?

不,丑陋来自active record 模式仅适用于最简单的用例(其中几乎没有关联的逻辑,具有持久性的美化值对象)。对于任何重要的情况,最好应用data mapper 模式。

..如果我创建一个“城市”类,在其构造函数中,我需要将城市表与 city_dealerships 表与经销商表与经销商_汽车表与汽车表与带有***的汽车轮表连接起来表。

仅仅因为您需要有关“莫斯科每个经销商的可用服务”的数据并不意味着您需要创建Car 实例,而且您肯定不会关心那里的车轮。网站的不同部分将具有不同的运营规模。

另一件事是您应该停止将类视为表抽象。 没有规定“类和表之间必须有 1:1 的关系”

再次以Car 为例。如果您看一下,拥有单独的Wheel(甚至WheelSet)课程只是愚蠢的。相反,您应该只拥有一个已包含所有部分的 Car 类。

$car = new Car;
$car->setId( 616 );

$mapper = new CarMapper( $cache );
$mapper->fetch( $car );

映射器不仅可以从“Cars”表中轻松获取数据,还可以从“Wheel”和“Engines”等表中轻松获取数据并填充$car 对象。

底线:停止使用活动记录。

P.S. 另外,如果你关心代码质量,你应该开始阅读PoEAA 的书。或者至少开始看lectures listed here

我的 2 美分

【讨论】:

是的,这就是他的原始代码的制作方式,但我似乎在逻辑上失败了 =] 你可以随时用$wheels-&gt;setCarId( $car-&gt;getId() ); 替换该行,或者如果你想为不止一辆汽车获得***:@ 987654341@。 API 可能会改变,但基本思想是:将 SQL 与 domain logic 分开。 已更新...第二次查看的整个$wheel = new WheelCollection 位实际上似乎有点.. emm ...真是愚蠢的事情。 非常感谢。我正在努力为您的系统创建伪代码please see this PasteBin。如您所见,我被困在WheelCollection 类以及WheelMapperfetch() 方法中,该方法接受WheelCollection 类对象,而CarMapper' s fetch() 方法接受 Car 类对象。 我看到您已添加到您的答案中。不幸的是,我确实需要一个Wheel 课程。我需要抓住单个***而不需要抓住汽车。请记住,这只是一个思想实验,任何地方都没有汽车或车轮。也可能是人和群体、砖墙、字母和单词等。组成一个整体的部分。 一般来说,您可以将“集合类”视为美化的数组(或者可能是注册表)。它需要设置一些条件、向其添加新“事物”以及从中检索单个实例的方法。当 mapper 从存储中获取数据时,或者一些外部代码在 mapper 存储它们之前添加新的“事物”时,mapper 将使用“add stuff”方法。也许this 有点帮助。【参考方案2】:

Rails 中的 ActiveRecord 实现了延迟加载的概念,即推迟数据库查询,直到您真正需要数据。因此,如果您实例化一个my_car = Car.find(12) 对象,它只会查询该行的汽车表。如果稍后你想要my_car.wheels,那么它会查询wheels 表。

我对上面的伪代码的建议是不要在构造函数中加载每个关联的对象。汽车构造函数应该只查询汽车,并且应该有一个方法来查询它的所有车轮,还有一个方法来查询它的经销商,它只查询经销商并推迟收集所有其他经销商的汽车,直到你特别说了什么喜欢my_car.dealership.cars

后记

ORM 是数据库抽象层,因此必须调整它们以便于查询而不是微调。它们允许您快速构建查询。如果稍后您决定需要微调查询,那么您可以切换到发出原始 sql 命令或尝试以其他方式优化您要获取的对象数量。当您开始进行性能调整时,这是 Rails 中的标准做法 - 寻找使用原始 sql 发出时效率更高的查询,并寻找在您需要对象之前避免急切加载(与延迟加载相反)的方法。

【讨论】:

非常感谢您的回复。当然,如果我有一个class User,其中包含一个由其他用户对象组成的public $friends 数组,我只会在我真正需要该数据时使用user-&gt;getFriends() 填充它。我熟悉这种做法,通过这种方式,我实际上可以为数组中的每个朋友使用user 对象。然而,在汽车/车轮的情况下,我总是需要 -every-car 拥有 -every-wheel,并且 -every-dealer 拥有 -every-car,等等。因此,上述设计模型将不起作用。跨度> “汽车构造函数应该只查询汽车” - 这不是构造函数应该做的。 另外,如果实例化user 对象会通过查询加载数据,那么调用user-&gt;getFriends() 将对用户拥有的每个朋友执行查询,因为它会实例化user 对象对于每个朋友,这让我们回到同一个问题。 Leng,那么您不会优化user-&gt;getFriends() 以便它在一个查询中一次获取所有朋友吗?我很难想到一个合理的场景,在这种场景中,人们总是需要始终拥有所有数据。这似乎是一种反模式。 @Leng - 这取决于您如何编写user 课程。也许您编写它以便可以使用已预取的数据对其进行实例化,以便您对所有朋友进行一次查找,然后将每一行的数据传递给 User 构造函数。有很多方法可以做到这一点,它只是需要更多的代码,你想要的效率就越高。【参考方案3】:

一般来说,我建议使用一个构造函数来有效地获取查询行或较大查询的一部分。如何做到这一点取决于您的 ORM。这样,您可以获得高效的查询,但您可以在事后构造其他模型对象。

一些 ORM(django 的模型,我相信一些 ruby​​ ORM)试图巧妙地构造查询,并且可能能够为您自动执行此操作。诀窍是弄清楚何时需要自动化。我个人对 PHP ORM 不熟悉。

【讨论】:

抱歉,但是当您说我应该有一个“有效地获取查询行的构造函数”时,我不明白您的意思。我假设您的意思是:public function __construct($query_row) 。您能否使用您提出的设计从我的问题的思想实验中给出一个 wheelcar 类的伪代码示例? 这听起来像一个映射器。

以上是关于与 OOP 概念作斗争的主要内容,如果未能解决你的问题,请参考以下文章

在 React 中与 JS Promises 作斗争

与内部可变性作斗争

在可可中与货币作斗争

与意外的交叉线程和停止作斗争

与 OkHttp 拦截器作斗争

JS 8-1 OOP概念与继承