我有一些处理程序("控制器")类,它们可以以某种方式处理项目:
interface IHandler
{
public function execute(Item $item);
}
class FirstHandler implements IHandler
{
public function execute(Item $item) { echo $item->getTitle(); }
}
class SecondHandler implements IHandler
{
public function execute(Item $item) { echo $item->getId() . $item->getTitle(); }
}
class Item
{
public function getId() { return rand(); }
public function getTitle() { return 'title at ' . time(); }
}
但我需要在子项类中添加一些新功能:
class NewItem extends Item
{
public function getAuthor() { return 'author ' . rand(); }
}
并在SecondHandler 中使用
class SecondHandler implements IHandler
{
public function execute(Item $item) { printf('%d %s, author %s', $item->getId(), $item->getTitle(), $item->getAuthor()); }
}
但Item
类实际上并没有getAuthor
方法。而且,如果我试图更改SecondHandler
类中accept方法的签名,我会发现E_STRICT
关于声明兼容性的错误。当然,这有点违反LSP。
如何解决此问题?我是否需要两个接口,例如INewHandler
和IHandler
,它们具有不同的execute
方法签名?但这是某种代码重复。
此外,我不能在处理程序中使用__constructor(Item $item)
和__construct(NewItem $item)
(以及没有参数的execute
方法),这将被视为更好的解决方案:它们必须是不可变的,并且在应用程序生命周期中只允许每个策略的单个实例。
正如您自己所发现的,PHP的类型提示实现有很多局限性,这使得场景(如您所描述的场景)变得更加困难。在Java和Swift等其他类型语言中,您的实现是绝对合法的。
在思考了你的问题后,我找到了Félix提出的解决方案,但我认为与这个问题相比,它的设计太过了。
我对你的问题的回答不是一个解决方案,而是我在多年的PHP开发后给你的一个建议:
放弃PHP中的类型提示,以动态方式进行开发。。
与Java/C++相比,PHP更类似于Ruby/Python/JavaScript,尝试从静态类型语言复制1到1会导致强制和复杂的实现。
实现问题的解决方案很简单,所以不要把它过于复杂,保持它的简单性(KISS原则)。
声明方法的参数而不使用类型,并在真正需要的地方实现检查(例如抛出异常)。
interface IStrategy
{
public function execute($item);
}
class FirstStrategy implements IStrategy
{
public function execute($item) {
echo $item->getTitle();
}
}
class SecondStrategy implements IStrategy
{
public function execute($item) {
// execute(NewItem $item) is identical to this check.
if (! $item instanceof NewItem) {
throw new Exception('$item must be an instance of NewItem');
}
echo $item->getAuthor();
}
}
class Item
{
public function getId() { return rand(); }
public function getTitle() { return 'title at ' . time(); }
}
class NewItem extends Item
{
public function getAuthor() { return 'author ' . rand(); }
}
同样,不要在Java中思考,而是尽可能遵循鸭子打字的方式
在可能的情况下,尽量不要严格强制参数的类型,而是根据可用的接口调整代码的行为(Duck Typing)。
class SecondStrategy implements IStrategy
{
public function execute($item) {
$message = $item->getTitle();
// PHP 5 interface availability check.
if (is_callable([$item, 'getAuthor'])) {
$message .= ' ' . $item->getAuthor();
}
// With PHP 7 is even better.
// try {
// $message .= ' ' . $item->getAuthor();
// } catch (Error $e) {}
echo $message;
}
}
我希望能帮助你。^_^
@daniele orlando和@ihor burlachenko都提出了有效的观点。考虑以下方法重载,这是一种折衷,应该可以很好地扩展:
interface IHandler
{
/**
* @param $item Item|NewItem
*/
public function execute($item);
// protected function executeItem(Item $item);
// protected function executeNewItem(NewItem $item);
}
trait IHandlerTrait
{
public function execute($item)
{
switch(true) {
case $item instanceof Item:
return $this->executeItem($item);
case $item instanceof NewItem:
return $this->executeNewItem($item);
default:
throw new 'InvalidArgumentException("Unsupported parameter type " . get_class($item));
}
}
protected function executeItem(Item $item)
{
throw new 'LogicException(__CLASS__ . " cannot handle execute() for type Item");
}
protected function executeNewItem(NewItem $item)
{
throw new 'LogicException(__CLASS__ . " cannot handle execute() for type NewItem");
}
}
class FirstHandler implements IHandler
{
use IIHandlerTrait;
protected function executeItem(Item $item) { echo $item->getTitle(); }
}
class SecondHandler implements IHandler
{
use IIHandlerTrait;
// only if SecondHandler still need to support `Item` for backward compatibility
protected function executeItem(Item $item) { echo $item->getId() . $item-> getTitle(); }
protected function executeNewItem(NewItem $item) { printf('%d %s, author %s', $item->getId(), $item->getTitle(), $item->getAuthor()); }
}
您确定要在此处使用策略模式吗?
看起来,战略的行动取决于它处理的元素的类型。在这种情况下,访问者模式可能也适用于此。
目前,您似乎想要执行一个可扩展的数据记录(Item和NewItem)。考虑执行一些可插入的行为(通过接口实现)。
从你的写作中很难猜测这种行为会是什么,因为(New)Item在你提供的例子中只是一个美化的数据结构。
如果您想在另一个对象中处理/操作对象,您可以/应该使用接口。
interface IStrategy
{
public function execute(ItemInterface $item);
}
interface ItemInterface
{
public function getTitle();
.....
}
如果您想扩展(New)Item类的公共功能,您可以为newItem 创建新的接口
interface NewItemInterface extends ItemInterface
{
...
}
class SecondStrategy implements IStrategy
{
public function execute(NewItemInterface $item)
{ .... }
}
或者,您可以像其他人提到的那样使用一些实例检查。
如果您的继承和建议SecondHandler应该同时处理Item和NewItem,那么您应该能够将此功能隐藏在公共接口后面。从您的示例中,它可能被调用为toString(),它可能是Item接口的一部分。
否则,您最初的设计可能有问题。您必须更改您的继承或处理项目的方式。或者其他我们不知道的事情。
此外,我不知道你为什么需要DTO,但似乎对条令有一些误解。条令是一个ORM,它解决了你的持久性问题。它通过引入存储库增加了与存储通信方式的限制,但并没有定义域逻辑。
根据接口分离,请找到一些解决方案。
```
# based on interface segrigation.
interface BasicInfo
{
public function getId();
public function getTitle();
}
interface AuthorInfo
{
public function getAuthor();
}
interface IHandler
{
public function execute(Item $item);
}
class FirstHandler implements IHandler
{
public function execute(Item $item) { echo $item->getTitle(); }
}
class SecondHandler implements IHandler
{
public function execute(Item $item) { echo $item->getId() . $item->getTitle(); }
}
class Item implements BasicInfo
{
public function getId() { return rand(); }
public function getTitle() { return 'title at ' . time(); }
}
class Item2 extends Item implements AuthorInfo
{
public function getAuthor() { return 'author ' . rand(); }
}
但是我认为您不应该保留Item类的依赖关系。您应该编写一些重复的代码来保持类的可插入性/独立性。因此,开放/关闭原则也应该存在。