单功能访问器是一个坏习惯吗?
他们不是一个好主意。原因很简单:
他们有多重职责(设置和获取数据)。好的功能有单一的职责并且做得很好。
-
他们掩盖了意图。您无法查看方法调用并了解将要发生的情况。
你觉得怎么样body()
方法呢?出色地,body
是一个名词。并且将方法(应该是动词)作为名词是令人困惑的。
但如果该方法是age()
。年龄既是动词又是名词。所以当你看到$obj->age()
,你是在告诉物体给你它的年龄吗?或者你是在告诉物体自己老化?
Whereas $obj->getAge()
非常清楚你想要做什么。和$obj->getBody()
同样清楚。
-
它增加了方法的复杂性。
您的整体复杂性将保持不变(因为存在相同的逻辑),但它会转向更少的方法。这意味着您将拥有一种更复杂的方法,而不是两种极其简单的方法。
所以是的,我会避免它。
但让我们退后一步。与其问“单函数访问器”是否是一个坏习惯,不如让我们问关于一般访问器的同样问题......
属性访问器是一个坏习惯吗?
我的回答是:是的。
取决于对象的角色:
它取决于的是对象的作用,以及具体属性的作用。这是根据具体情况而定的。
有大量不同类型的对象(域对象、服务、代理、集合等)。有些是有状态的,有些则不是。
如果对象没有状态,则它没有属性,因此我们可以忽略它。
对于那些有状态的对象来说,它们为什么有那个状态呢?
如果是因为他们代表国家,那么国家就应该是公开的(不是说财产应该公开,而是国家应该向外界公开)。因此,代表业务实体的领域模型应该具有公共状态。
class User {
public $name;
}
但如果对象的作用不是代表状态,而是用它做某事,那么它就不应该被暴露。
class UserMapper {
protected $pdo;
public function __construct(Pdo $connection) {
$this->pdo = $connection;
}
public function findUserById($id) {
...
}
在映射器的情况下,$pdo
是偶然状态。映射器的工作不是表示数据库连接的数据,但它需要它才能工作。
底线
底线是never暴露对象不直接表示的状态。
仅仅因为您拥有财产,并不意味着您应该公开它。事实上,很多时候你不应该暴露它。这被称为信息隐藏 http://en.wikipedia.org/wiki/Information_hiding.
取决于状态类型:
但是有状态的对象类型又如何呢?事实证明,状态有两种基本类型:
-
应用状态
想想配置之类的事情。基本上状态在构建时未定义,但在运行时已知。
请务必注意,此状态不应在请求过程中发生更改。而且它也应该是相当相同的请求到请求(除了部署等)。
-
用户状态
考虑派生或依赖于用户输入的状态和数据。一个明显的例子是从表单提交的数据。
但一个不太明显的例子是,如果您对不同类型的表示使用不同的渲染器。因此,如果用户请求 JSON 响应,则在属性中设置的 JSON 渲染代码将被视为用户状态。
我的主张:
属性访问器应用状态很糟糕。应用程序状态没有理由在运行中更改,因此您没有理由必须传播状态。
属性访问器用户状态 may没事。但还有更多的事情要做。
取决于访问器的作用
在你的例子中:
public function setBody($body)
{
$this->body=$body;
}
你本质上是在做$body
公共财产。
还有nothing错了。但为什么需要方法呢?有什么问题:public $body;
首先?
有人会说“财产是邪,千万不能公!”。这完全是废话,因为这正是您对该访问器所做的事情。
现在,如果访问器执行了一些输入信息(通过类型提示)或其他验证逻辑(最小长度等),那么这是一个不同的故事......
或者是吗?
让我举个例子。
class Person {
public $name;
public $age;
}
vs
class StrictPerson {
protected $name;
protected $age;
public function setName($name) {
if (!is_string($name)) throw new BlahException();
if (strlen($name) < 10) throw new InvalidException();
$this->name = $name;
}
public function getName() {
return $this->name;
}
public function setAge($age) {
if (!is_int($age)) throw new ....
if ($age < 0 || $age > 150) throw new ...
$this->age = $age;
}
public function getAge() {
return $this->age;
}
}
现在,很明显这些属性总是有效的。正确的?正确的?正确的?
嗯,不。如果我创建一个孩子会发生什么:
class LoosePerson extends StrictPerson {
public function setName($name) {
$this->name = $name;
}
public function setAge($age) {
$this->age = $age;
}
}
突然之间,我们所有的验证都消失了。现在你可以说这是有意为之的,这是程序员的问题。或者您可以简单地将属性更改为private
相反,要保持它们始终有效。正确的?正确的?正确的?
嗯,不。如果我这样做会发生什么:
$a = new StrictPerson;
$r = new ReflectionProperty($a, 'name');
$r->setAccessible(true);
$r->setValue($a, 'Bob');
我刚刚在一个应该始终验证的对象上设置了一个无效值。
底线
仅当您满足以下条件时,才可以使用访问器作为验证器:always使用它们。你使用的每一个工具总是会用到它们。像 mysqli 和 PDO 以及 Doctrine 和 PHPUnit 这样直接设置属性而不是调用 setter 的东西可能会导致大量问题。
相反,您可以使用外部验证器:
class PersonValidator {
public function validate(Person $person) {
if (!is_string($person->name)) {
throw new Blah...
}
if (strlen($person->name) < 10) {
throw new Blah...
}
if (!is_int($age)) throw new ....
if ($age < 0 || $age > 150) throw new ...
return true;
}
}
那么,属性访问器是一个坏习惯吗?
我认为,是的,很多时候它们是一个坏习惯。
现在,在某些情况下您应该使用它们:
-
当你must在界面中表示该状态
属性在接口中是不可指定的。所以如果你must表示对象在接口中公开状态,那么您应该使用 getter 和 setter。
I'd strongly敦促您考虑为什么要创建该界面。对于简单的数据对象,通常最好依赖核心对象类型(因为由于数据对象上没有逻辑,多态性无法受益)。
-
当您出于某种原因需要隐藏内部表示时。
一个示例是表示 unicode 字符串的类。您可能有一个访问器方法来获取原始值。但您希望这是一种方法,以便您可以将内部表示形式转换为正确字符集的字符串。
这就提出了一个有趣的观点。
当您创建访问器时,绝对不应该创建属性访问器。您应该创建一个状态访问器。不同之处在于属性访问器必须始终返回属性或设置它)。
另一方面,如果需要,状态访问器可以“伪造它”。因此,我们上面关于 unicode 字符串类的示例可以在内部将字符串表示为代码点数组。然后,当您访问“字符串”状态时,它会将数组转换为实际的字符串。
对象应该抽象
如果您要使用访问器,它应该是抽象状态。不代表它。
底线 / TLDR
Property访问器是一个坏习惯。如果您打算这样做,请将属性公开并使用验证器对象。
State访问器不是一个坏习惯。它们对于维护有用的抽象非常有用。