这更像是一个长篇评论,因为它解释了你的困境的根源,但没有提供任何解决方案。
OOP 第 1 课:仅仅因为您正在使用类并不意味着您正在编写面向对象的代码。
对象中的公共属性很少有很好的用例。让我们看一下OP的示例:
class Gallery {
public $galleryID;
public $galleryName;
// ...
}
将我们的属性定义为public
之后,以下两段代码有何不同?
$gallery = new Gallery;
$gallery->galleryId = 42;
$gallery->galleryName = 'some name';
// vs:
$gallery = array(
'galleryId' => 42,
'galleryName' => 'some name'
);
如果您说“它们根本没有什么不同”,那么您是对的。事实上,基于对象的代码会因为new
. 还有一些其他因素,例如传递对对象的引用而不是复制新数组的能力,但这些不会影响这种特殊情况。
OOP 第 2 课:对象是一个黑盒子
创建一个只是可变属性集合的对象的问题在于,您的其余代码完全了解该对象内部发生的情况。让我们谈谈为什么这很糟糕......
在复杂性方面,人类并不是很好。好的软件旨在通过将功能封装到离散单元中来部分降低复杂性。在这种情况下,我们希望将“图库”实体的所有逻辑封装到Gallery
类中。作为领域驱动设计 (DDD) 方法的一部分,这很有意义。我们要做的是Gallery
与外界隔绝;我们希望它的内部实现对我们的其余代码是不透明的。我们应用程序的其余部分不应该知道或关心这些Gallery
功能是如何工作的,只要它按预期工作即可。这里的额外好处是我们可以专注于让画廊按照它应该的方式工作,然后忘记它。我们不必被迫记住如何Gallery
与一个Image
或一个一起工作Revision
. 这种松耦合是面向对象设计中最强大的工具之一。
虽然它可能适用于非常小的规模,但不可能同时将整个应用程序的逻辑保持在您的脑海中。不管你有多聪明,我们的大脑只是没有足够的内存。
回到代码,如果我们的应用程序代码知道如何Gallery
为自己分配一个名称,那么我们已经允许“画廊”的逻辑泄漏到程序的其余部分。当我们决定要在分配新画廊名称时对其进行验证时会发生什么?我们现在必须将验证逻辑放在我们指定画廊名称的代码中的任何地方,因为我们还没有隔离关于“画廊性”这个抽象概念的所有内容。更好的设计是将Gallery
属性分配封装在对象本身中:
class Gallery {
private $galleryId;
private $name;
public function setName($name) {
$this->name = $name;
}
public function getName($name) {
return $this->name;
}
}
如果我们以这种方式构建我们的类,当我们需要为画廊指定名称时,我们总是有一个单一的入口点。现在,当我们对画廊的需求在未来发生变化时(他们将会),我们所有的应用程序代码——对画廊名称分配背后的逻辑视而不见——与破损隔离开来。我们只需向我们的名称设置器添加一个新方法,并在我们的程序中创建最小的动荡:
class Gallery {
private $galleryId;
private $name;
public function setName($name) {
$this->validateName($name);
$this->name = $name;
}
private function validateName($name) {
if (!preg_match('/^[a-z]+$/', $name)) {
throw new Exception;
}
}
public function getName($name) {
return $this->name;
}
}
解决 OP
要回答如何将封装Revision
对象表示为更高级别Gallery
实例的属性的问题,我们需要一些上下文。看来 OP 试图做的是模型域实体,这些实体将被写入后端持久层(例如数据库、平面文本文件等)并从中检索。
贫血域模型是处理这个问题的一种方法,但是它通常被认为是一种反模式。马丁福勒写道:
贫血域模型的基本症状是乍一看它看起来像真实的东西。有对象,许多以领域空间中的名词命名,这些对象与真正的领域模型所具有的丰富关系和结构相关联。当您查看行为时,问题就来了,您会意识到这些对象几乎没有任何行为,这使得它们只不过是成袋的 getter 和 setter。实际上,这些模型通常带有设计规则,即不要将任何域逻辑放入域对象中。相反,有一组服务对象捕获所有域逻辑。这些服务位于域模型之上,并使用域模型来存储数据。
考虑到这些论点,您应该考虑使用诸如DataMapper或Gateway模式之类的东西来处理需要持久化到某种形式的后端存储的域对象。
备择方案
让我们暂时忘掉Revision
对象,想象一下我们想使用一个Slideshow
对象来输出画廊中的图像。这个类可能看起来像:
class Slideshow {
private $gallery;
public function __construct(Gallery $gallery) {
$this->gallery = $gallery;
}
public function play() {
// do something with the gallery here
}
}
忽略 php 代码实际上不会用于“播放”幻灯片的事实,因为这会在客户端代码中发生。这里重要的Slideshow
是使用Composition来访问Gallery
. 这种结构大大优于直接在内部使用new
a ,因为:Gallery
Slideshow
现在Slideshow
是可插入的——我们可以插入任何遵循“画廊”概念的对象(通常Gallery
会被声明为符合指定的接口契约)。
现在Slideshow
即将进行测试。我们如何处理Gallery
提供的图像类型不合适的情况?如果我们直接在Gallery
内部实例化 a,Slideshow
我们将无法模拟这种情况。通过注入依赖项,Slideshow
我们有机会测试我们的代码处理不同操作条件的能力。
当然,有时在另一个类中直接实例化一个对象是合适的。有关此主题的更多指导,我建议 Miško Hevery 在他的文章To "new" or not to "new"中提出建议。