我有一个关于"单元测试"和对象继承的问题。例如:
我有一个类a,它继承了类b。让我们假设这两个类的唯一区别是add方法。在类B中,这个add方法稍微扩展了一下。现在我想为类B的add函数写一个单元测试,但由于parent::add调用,我依赖于父类a。在这种情况下,我不能模拟父类的add方法,所以结果测试将是一个集成测试,但如果我想它是一个单元测试?我不希望类B中添加方法的测试因为类a中的父方法而失败。在这种情况下,只有父方法的单元测试应该失败。
class B exends A
{
public function add($item)
{
parent::add($item);
//do some additional stuff
}
....
}
class A
{
protected $items = [];
public function add($item)
{
$this->items[] = $item;
}
....
}
当然,我可以使用对象聚合并将父对象传递给子构造函数,因此我可以模拟父方法add,但这是最好的方法吗?我将很少再使用对象继承了。
class B
{
protected $a;
public function __contruct(A $a)
{
$this->a = $a;
}
public function add($item)
{
$this->a->add($item);
//do some additional stuff
}
....
}
class A
{
protected $items = [];
public function add($item)
{
$this->items[] = $item;
}
....
}
我将非常感谢你的意见。谢谢! 问问自己,你想要实现什么样的继承?如果B 是一种 a,那么你想要接口继承。如果B与a共享大量代码,那么您需要实现继承。有时两者都需要。
接口继承将语义划分为严格的层次结构,从泛化到专门化组织语义。认为分类。接口(方法签名)表示行为:类响应的消息集,以及类发送的消息集。当从一个类继承时,您隐式地接受父类代表您发送的所有消息的责任,而不仅仅是它可以接收的消息。由于这个原因,超类和子类之间的耦合是紧密的,每个类都必须严格替代另一个类(参见Liskov替代原则)。实现继承将数据表示和行为(属性和方法)的机制封装到一个方便的包中,以便子类重用和增强。根据定义,子类继承父类的接口,即使它只需要实现。
最后一部分很关键。再读一遍:子类继承接口,即使他们只想要实现。
B是否严格要求A的接口?B能代替A吗,在所有情况下都匹配协方差和反方差?
如果答案是是,那么你有真正的子类型。祝贺你。现在你必须测试相同的行为两次,因为在B中你负责维护A的行为:对于A能做的每件事,B必须能够做到。
如果答案是否,那么您只需要共享实现,测试实现是否工作,然后测试B和A分别分派到实现中。
在实际中,我避免使用extends
。当我想要实现继承时,我使用trait
在一个地方定义static
行为†,然后使用use
在需要的地方合并它。当我想要接口继承时,我定义了许多狭窄的interface
,然后在所有具体类型中与implements
结合,可能使用trait
来利用行为。
对于你的例子,我会这样做:
trait Container {
public function add($item) { $this->items[] = $item; }
public function getItems() { return $this->items; }
private $items = [];
}
interface Containable { public function add($item); }
class A implements Containable { use Container; }
class B implements Containable {
use Container { Container::add as c_add; }
public function add($item) {
$this->c_add($item);
$this->mutate($item);
}
public function mutate($item) { /* custom stuff */ }
}
Container::add
和B::mutate
将进行单元测试,而B::add
将进行集成测试。
extends
是邪恶的。阅读ThoughtWorks入门文章《组合与继承:如何选择》,以获得对设计权衡的更深入理解。
†"静态行为",你问?是的。低耦合是一个目标,这也适用于特性。尽可能地,trait应该只引用它定义的变量。最安全的方法是使用静态方法,将它们的所有输入作为形式参数。最简单的方法是在trait中定义成员变量。(但是,请避免让trait使用在trait中没有明确定义的成员变量——否则,这就是盲目耦合!)我发现,性状成员变量的问题是,当混合多个性状时,会增加碰撞的机会。不可否认,这很小,但对于库作者来说,这是一个实用的考虑。