这是我的第三篇帖子PHP7系列中缺失前一个是关于命名参数.
Value对象没有标识,这意味着如果您有两个具有相同数据的对象,则它们被视为相等(以两个纬度、经度对为例)。一般来说,他们是不可变的除了简单的getter之外,没有其他方法。
这些对象是领域驱动设计以及一种常见的对象类型,即使是在设计良好的不遵循域驱动设计的代码库中也是如此。我当前的项目在德国维基媒体大体上遵循“干净的体系结构架构,这意味着每个“用例”或“交互器”都有两个值对象:请求模型和响应模型。这些当然不是唯一的Value对象,当这个相对较小的应用程序完成时,我们可能已经有50多个了。这使得PHP让创建Value Objects变得如此痛苦,这真的很不幸,尽管它当然不会阻止您这样做。
让我们看一个这样的值对象的示例:
1 2 三 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
|
班 联系人请求 { 私有的 $名字; 私有的 $姓氏; 私有的 $电子邮件地址; 私有的 $主题; 私有的 $message正文; 公众的 功能 __构造( 一串 $名字, 一串 $姓氏, 一串 $电子邮件地址, 一串 $主题, 一串 $message正文 ) { $这个->名字 = $名字; $这个->姓氏 = $姓氏; $这个->电子邮件地址 = $电子邮件地址; $这个->主题 = $主题; $这个->消息正文 = $message正文; } 公众的 功能 获取名字()以下为: 一串 { 返回 $这个->名字; } 公众的 功能 获取姓氏()以下为: 一串 { 返回 $这个->姓氏; } 公众的 功能 获取电子邮件地址()以下为: 一串 { 返回 $这个->电子邮件地址; } 公众的 功能 获取主题()以下为: 一串 { 返回 $这个->主题; } 公众的 功能 获取消息正文()以下为: 一串 { 返回 $这个->消息正文; } } |
如您所见,这是一个非常简单的类。那么我在抱怨什么?实际上有三种不同的情况。
1.初始化很糟糕
如果你读过我在这个系列中的前一篇文章,你可能会看到这篇文章。事实上,我在那篇文章的末尾提到了ValueObjects。为什么它很烂?
|
新的 联系人请求( “尼扬”, “猫”, 'maxwells-demon@entopywins.wtf', “小猫”, “小猫太棒了” ); |
这个缺少命名参数强制用户使用非命名参数的位置列表,这不仅不利于可读性,而且容易出错。当然,可以创建一个带有名字和姓氏字段的PersonName值对象,以及某种部分电子邮件消息值对象。不过,这只是部分缓解了问题。
有一些方法可以解决这个问题,尽管它们都不好。一个明显的解决方案,其缺点也同样明显,就是建设者使用流畅接口对于每个值对象。对我来说,增加的混乱足以使程序复杂化,从而取消了删除位置未命名参数列表所带来的好处。
避免位置列表的另一种方法是根本不使用构造函数,而是依赖setter。不幸的是,这确实带来了两个新问题。首先,Value对象在其整个生命周期内都是可变的。虽然有些人可能清楚地知道不应该使用这些setter,但它们的存在表明更改对象没有什么错。不得不依赖于这种特殊的理解或依赖于阅读文档的人肯定是不好的。其次,可以构造一个不完整的对象,即缺少必需字段的对象,并将其传递给系统的其余部分。如果没有进行自动检查,人们最终会错误地执行此操作,并且错误可能非常非本地,因此很难追踪其来源。
不久前,我尝试了一种方法来解决使用setter引入的这两个问题。我创建了一个名字很好的特性由使用Fluent接口格式的setter的Value Objects使用。
|
班 联系人请求 { 使用 值对象InPhp吸盘球; 私有的 $名字; // ... 公众的 功能 带有名字( 一串 $名字 )以下为: 自己 { $这个->名字 = $名字; 返回 $这个; } // ... } |
trait提供了一个静态newInstance方法,可以按如下方式构造using Value Object:
|
$contact请求 = 联系请求::新实例() ->带有名字( “尼扬” ) ->带有姓氏( “猫” ) // ... ->带消息正文( “粉红色毛茸茸的独角兽在彩虹上跳舞” ); |
trait还提供了一些实用函数来检查对象是否已完全初始化,默认情况下,这些函数将假定具有空值的字段未初始化。
最近我尝试了另一种方法,也使用了Value Objects使用的特性:FreezableValue对象。与前面的方法相比,我想在这里更改的一点是,初始化的Value Object的用户不必做任何与通过构造函数调用初始化的更标准的Value对象不同或附加的事情。冻结是一个非常简单的概念。对象一开始是可变的,然后当调用freeze时,修改就停止了。这是通过一个冻结
方法,该方法在调用时设置一个标志,该标志在每次调用setter时都会被检查。如果在设置标志时调用setter,则会引发异常。
|
$contact请求 = ( 新的 联系人请求() ) ->集合名字( “尼扬” ) ->设置姓氏( “猫” ) // ... ->带消息正文( “粉红色毛茸茸的独角兽在彩虹上跳舞” ) ->冻结(); $contact请求->集合名字( “尼扬” ); //吊杆 |
为了验证构造对象的代码中的初始化是否完成,trait提供了一个资产NoNullFields</span></span>
可以与一起调用的方法冻结
.(名称资产字段已初始化
实际上会更好,因为前者泄漏了实现细节,如果类重写它,结果会不正确。)
与第一种方法相比,第二种trait方法的缺点是每个Value对象都需要调用检查每个setter中冻结标志的方法。这是一件很容易忘记的事情,也是另一个潜在的错误来源。我还没有调查是否可以通过一些反射魔法来消除这种需求。
如果这些方法中的任何一种都能为自己买单,这是很有争议的,而且很明显,它们中没有一种接近于成为好人。
2.重复和混乱
对于值对象的每个部分,都需要一个构造函数参数(或setter)、一个字段和一个getter。这是大量的样板,而类语言构造提供的不需要的灵活性为不一致性创造了足够的空间。我在Value Objected中遇到过很多错误,这些错误是由构造函数中错误字段的赋值或getter中错误字段返回引起的。
“复制是设计良好的系统的主要敌人。”
―罗伯特·C·马丁
(我实际上不同意上述引用的(措辞),并将用“解释和修改的复杂性”取代“重复”。)
3.语言中缺少概念
用代码传达意图很重要。意图不明确会导致程序员在试图理解意图时浪费时间,而由于意图不被理解而导致的错误会浪费时间。当值对象是类,而许多其他事物是类时,可能不清楚给定的类是否打算成为值对象。当项目中有更多初级人员时,这尤其是一个问题。在语言本身中有一个专用的Value Object构造将使意图变得明确。它还迫使有意识和明确的操作将Value对象更改为其他对象,从而消除了代码腐烂的一种途径。
“干净的代码从来不会模糊设计者的意图,而是充满了清晰的抽象和简单的控制线。”
―Grady Booch,面向对象分析的作者
我可以喝
|
Value对象 联系人请求 { 一串 $名字; 一串 $姓氏; 一串 $电子邮件地址; } //构建新实例: $contact请求 = 新的 联系人请求( 名字=“尼扬”, 姓氏=“猫”, 电子邮件地址=“一些东西” ); //“字段”的访问: $名字 = $contact请求->名字; //语法错误: $contact请求->名字 = “哈克斯”; |
2022年更新
PHP 8.0(菲律宾比索)给我们带来了建造商物业推广和8.1比索带来只读属性。结合PHP 7.4中引入的更成熟的类型化属性,实现实体值对象前所未有地容易。没有专门的语法,但仍然非常好:
|
班 联系人请求 { 公众的 功能 __构造( 公众的 只读一串 $名字 = “nyan”, 公众的 只读一串 $姓氏 = “猫”, 公众的 只读一串 $电子邮件地址 = “一些东西”, ) { } } $请求 = 新的 联系人请求(名字以下为: “foo”, 姓氏以下为: “bar”, 电子邮件地址以下为: “呸”); |
另请参见
帖子视图: 32,649
相关的