想象一下你有这个类
class Ai1ec_Less_Parser_Controller {
/**
* @var Ai1ec_Read_Variables_Startegy
*/
private $read_variable_strategy;
/**
* @var Ai1ec_Save_Variables_Strategy
*/
private $write_variable_strategy;
/**
* @var Ai1ec_Less_Variables_Collection
*/
private $less_variables_collection;
/**
* @var Ai1ec_Less_Parser
*/
private $ai1ec_less_parser;
/**
* We set the private variables in the constructor. I feel that there are too many parameters.
* Should i use setter instead and throw an exception if something is not set?
*
* @param Ai1ec_Read_Variables_Startegy $read_variable_strategy
* @param Ai1ec_Save_Variables_Strategy $write_variable_strategy
* @param Ai1ec_Less_Variables_Collection $less_variables_collection
* @param Ai1ec_Less_Parser $ai1ec_less_parser
*/
public function __construct( Ai1ec_Read_Variables_Startegy $read_variable_strategy,
Ai1ec_Save_Variables_Strategy $write_variable_strategy,
Ai1ec_Less_Variables_Collection $less_variables_collection,
Ai1ec_Less_Parser $ai1ec_less_parser ) {
}
}
我需要设置这些变量,所以我在构造函数中设置了它们(但看起来参数太多了)。另一种选择是使用setter来设置它们,然后在一个方法中,如果所需的变量之一没有像那样设置,则抛出异常
public function do_something_with_parser_and_read_strategy() {
if( $this->are_paser_and_read_strategy_set === false ) {
throw new Exception( "You must set them!" );
}
}
private function are_paser_and_read_strategy_set () {
return isset( $this->read_variable_strategy ) && isset( $this->ai1ec_less_parser );
}
你认为这两种方法中的一种更好吗?为什么?
您的类是不可变的吗?如果是这样的话,那么通过构造函数进行100%的成员填充通常是最好的方法,但我同意,如果你有超过5或6个参数,它可能会开始看起来很难看。
如果你的类是可变的,那么拥有一个具有所需参数的构造函数就没有任何好处。通过访问器/赋值器方法(也称为属性)公开成员。
工厂模式(如@Ray所建议的)可能会有所帮助,但前提是你有各种类似的类——对于一次性的,你可以简单地使用静态方法来实例化对象,但你仍然会遇到"参数太多"的问题。
最后一种选择是接受一个带有字段的对象(每个参数一个字段),但要小心使用这种技术——如果某些值是可选的,那么只需使用方法重载(不幸的是,PHP不支持)。
我会坚持你正在做的事情,只有当它出现问题时,我才会改变它。
类命名控制器在某种程度上反映了MVC,或者一般来说,任何负责处理序列的机制。
在任何情况下,数据对象类往往有许多字段——这是它们的责任。依赖于许多其他对象的规则对象可能会丢失一点。
正如我所看到的,有四个对象:读取、保存、解析和为某个对象提供集合接口。为什么一个人的阅读和写作界面不同?这不能合为一体吗?Parser本身应该是一个库,因此可能没有理由在任何地方将其组合,尽管它可以为自己使用读取器/写入器,并作为回报提供集合。因此,解析器是否可能接受reader的参数并返回一个集合对象?
这更多的是关于具体案例。一般来说,方法有很多参数(或者由不同域的其他对象初始化对象中的许多字段)表明存在某种设计缺陷。
这篇关于构造函数初始化的文章可能是一种主题——它建议在构造函数初始化中使用。只需确保跟进要点:
如果构造函数中有很多合作者要提供呢一个庞大的结构参数列表,就像任何大型参数列表是CodeSmell。
正如Ray所写的那样,有可能使用setter进行初始化,也有关于这方面的文章。就我的观点而言,我认为马丁·福勒确实很好地总结了这些案例。
没有"更好"的方法。但这里有几件事你必须考虑:
- 构造函数不是继承的
- 如果类需要太多的对象,它就要承担太多的责任
这可能会影响您选择类实现的接口类型。
一般的经验法则是:
如果参数是类函数所必需的,则应通过构造函数注入这些参数
如果使用工厂初始化实例,则会出现异常。工厂构建实例形式多样的类是很常见的,其中一些实现了相同的接口和/或扩展了相同的父类。然后,通过setter注入共享对象就更容易了。
使用调用setter的工厂而不是使用一组参数的构造函数来创建对象要灵活得多。查看生成器和工厂模式。
为访问未完全构建的对象抛出异常是很好的!
任何有2个以上(有时是3个)参数的函数,我总是传递一个数组,所以它看起来像:
public function __construct(array $options = array()) {
// Figure out which ones you truly need
if ((!isset($options['arg1'])) || (mb_strlen($options['arg1']) < 1)) {
throw new Exception(sprintf('Invalid $options[arg1]: %s', serialize($options)));
}
// Optional would look like
$this->member2 = (isset($options['arg1'])) && ((int) $options['arg2'] > 0)) ? $options['arg2'] : null;
// Localize required params (already validated above)
$this->member1 = $options['arg1'];
}
传递一系列选项可以实现未来的增长,而无需更改函数签名。然而,它的缺点是,函数必须本地化数组的所有元素,以确保访问不会引发警告/错误(如果数组中缺少元素)。
在这种情况下,工厂解决方案不是一个好的选择,因为您仍然需要将值传递给工厂,以便它可以用正确的值初始化对象。
"构造函数参数太多"的标准解决方案是生成器模式。控制器类本身仍然有一个长构造函数,但客户端可以在构造函数上使用setter,后者稍后将调用长构造函数。
如果你只在一个或两个地方构造你的控制器对象,那么创建一个生成器就不值得那么麻烦了;在这种情况下,只需使用当前代码即可。