里氏代入原理及继承的正确使用方法类
Liskov substitution principle and proper way to use inherited classes
我有一些处理程序 ("controller") classes,他们可以以某种方式处理项目:
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:
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
class却没有getAuthor
方法。而且,如果我尝试更改 SecondHandler
class 中接受方法的签名,我将捕获有关声明兼容性的 E_STRICT
错误。当然,这有点违反 LSP。
我该如何解决这个问题?我是否需要两个接口,例如 INewHandler
和 IHandler
,具有不同的 execute
方法签名?但这是某种代码重复。
此外,我不能在处理程序中使用 __constructor(Item $item)
和 __construct(NewItem $item)
(以及不带参数的 execute
方法),这将被视为更好的解决方案:它们必须是不可变的并且只能应用程序生命周期中允许的每个策略的单个实例。
你确定要在这里使用策略模式吗?
看起来,此处策略的操作取决于它处理的元素的类型。在这种情况下,Visitor 模式也可能适用于此。
就目前而言,您似乎想要执行可扩展的数据记录(Item 和 NewItem)。考虑改为执行一些可插入的行为(通过接口实现)。
从您的文章中很难猜测该行为是什么,因为 (New)Item 在您提供的示例中只是一个美化的数据结构。
正如您自己发现的那样,PHP 的类型提示实现有很多限制,使场景(如您描述的场景)比应有的更难。在 Java 和 Swift 等其他类型化语言中,您的实现是绝对合法的。
在思考您的问题后,我找到了 Félix 提出的解决方案,但我认为它与问题相比设计过度。
我对你的问题的回答不是解决方案,而是我在 PHP 多年发展后给你的建议:
放弃 PHP 中的类型提示并按应有的方式开发...以动态方式.
PHP 比 Java/C++ 更类似于 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;
}
}
希望对您有所帮助。 ^_^
如果你想 work/manipulate 在另一个对象中使用对象,你 could/should 使用接口。
interface IStrategy
{
public function execute(ItemInterface $item);
}
interface ItemInterface
{
public function getTitle();
.....
}
如果您想扩展 public(新)项目的功能 class,您可以为新项目
创建新界面
interface NewItemInterface extends ItemInterface
{
...
}
class SecondStrategy implements IStrategy
{
public function execute(NewItemInterface $item)
{ .... }
}
或者您可以使用其他人提到的一些实例检查。
如果您的继承和建议 SecondHandler 应该同时处理 Item 和 NewItem 一开始是正确的,那么您应该能够将此功能隐藏在公共接口后面。根据您的示例,它可能被称为 toString() ,它可能是 Item 接口的一部分。
否则,您最初的设计可能有问题。而且你必须改变你的继承或你处理物品的方式。或者其他我们不知道的事情。
另外,我不知道你为什么需要DTO,但似乎对Doctrine 有一些误解。 Doctrine 是一个 ORM,它解决了你的持久性问题。它增加了对您如何与引入存储库的存储通信的限制,但它没有定义您的域逻辑。
@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()); }
}
请根据接口隔离找到一些解决方案。
```
# 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(); }
}
但我认为你不应该保留项目的依赖关系 class。你应该写一些重复的代码来保留 class pluggable/independent。所以Open/close原则应该也有。
我有一些处理程序 ("controller") classes,他们可以以某种方式处理项目:
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:
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
class却没有getAuthor
方法。而且,如果我尝试更改 SecondHandler
class 中接受方法的签名,我将捕获有关声明兼容性的 E_STRICT
错误。当然,这有点违反 LSP。
我该如何解决这个问题?我是否需要两个接口,例如 INewHandler
和 IHandler
,具有不同的 execute
方法签名?但这是某种代码重复。
此外,我不能在处理程序中使用 __constructor(Item $item)
和 __construct(NewItem $item)
(以及不带参数的 execute
方法),这将被视为更好的解决方案:它们必须是不可变的并且只能应用程序生命周期中允许的每个策略的单个实例。
你确定要在这里使用策略模式吗?
看起来,此处策略的操作取决于它处理的元素的类型。在这种情况下,Visitor 模式也可能适用于此。
就目前而言,您似乎想要执行可扩展的数据记录(Item 和 NewItem)。考虑改为执行一些可插入的行为(通过接口实现)。
从您的文章中很难猜测该行为是什么,因为 (New)Item 在您提供的示例中只是一个美化的数据结构。
正如您自己发现的那样,PHP 的类型提示实现有很多限制,使场景(如您描述的场景)比应有的更难。在 Java 和 Swift 等其他类型化语言中,您的实现是绝对合法的。
在思考您的问题后,我找到了 Félix 提出的解决方案,但我认为它与问题相比设计过度。
我对你的问题的回答不是解决方案,而是我在 PHP 多年发展后给你的建议:
放弃 PHP 中的类型提示并按应有的方式开发...以动态方式.
PHP 比 Java/C++ 更类似于 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;
}
}
希望对您有所帮助。 ^_^
如果你想 work/manipulate 在另一个对象中使用对象,你 could/should 使用接口。
interface IStrategy
{
public function execute(ItemInterface $item);
}
interface ItemInterface
{
public function getTitle();
.....
}
如果您想扩展 public(新)项目的功能 class,您可以为新项目
创建新界面interface NewItemInterface extends ItemInterface
{
...
}
class SecondStrategy implements IStrategy
{
public function execute(NewItemInterface $item)
{ .... }
}
或者您可以使用其他人提到的一些实例检查。
如果您的继承和建议 SecondHandler 应该同时处理 Item 和 NewItem 一开始是正确的,那么您应该能够将此功能隐藏在公共接口后面。根据您的示例,它可能被称为 toString() ,它可能是 Item 接口的一部分。
否则,您最初的设计可能有问题。而且你必须改变你的继承或你处理物品的方式。或者其他我们不知道的事情。
另外,我不知道你为什么需要DTO,但似乎对Doctrine 有一些误解。 Doctrine 是一个 ORM,它解决了你的持久性问题。它增加了对您如何与引入存储库的存储通信的限制,但它没有定义您的域逻辑。
@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()); }
}
请根据接口隔离找到一些解决方案。
```
# 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(); }
}
但我认为你不应该保留项目的依赖关系 class。你应该写一些重复的代码来保留 class pluggable/independent。所以Open/close原则应该也有。