虽然在接口上使用类型提示而不是实现有助于减少耦合,但在尝试编写通用接口时可能会变得很麻烦。就像你说的那样,使用已知方法会更好。
话虽如此,事实上你有两种不同的方法。当调用myFunc
并传递一个EBook
时,你应该只依赖于接口中的方法。如果一个方法需要调用doPDFOnlyThing
并且它依赖于一个EBook
而不是PDFBook
,那么这将违反这个原则。
你可以做的一件事是:
public myFunc(EBook $book)
{
$book->read();
}
public myPDFFunc(PDFBook $book)
{
$book->read();
$book->doPDFOnlyThing();
}
尽管这可能有效,但它是一种肮脏的解决方法,您很可能会在此过程中违反开放/封闭原则,因为您将回来并编辑该类。(最终客户将需要一个具有
doKindleOnlyThing
方法的
KindleBook
。)
那么如何解决这个问题呢?
你想要对一个接口进行类型提示,但又想使用实现中的方法,就像你想要拥有蛋糕却又想吃掉它一样...
为了解决这个问题,您需要更抽象地设计您的代码。让我们以以下代码为例,您正在制作一个客户端,用于阅读各种格式的书籍,这些书籍都派生自
EBook
接口,并实现为基类
MyEBook
。
interface EBook
{
public function read();
}
interface PDFBook extends EBook
{
public function doPDFOnlyThing();
}
class MyEBook implements EBook
{
public function read()
{
echo 'reading from a ' . get_class($this);
}
}
class MyPDFBook extends MyEBook implements PDFBook
{
public function read()
{
parent::read();
}
public function doPDFOnlyThing()
{
echo 'doing what a PDF does while...';
}
}
EBook
接口约定了read()
方法,PDFBook
接口扩展了EBook
并添加了doPDFOnlyThing()
方法到接口中。具体实现MyEBook
和MyPDFBook
将各自使用它们的接口。
接下来,我们需要构建一些处理器类,可以对任何书籍执行某种操作。我们将在这里使用一种命名规则,即所有处理器类的后缀都为Reader
。因此,MyPDFBook
的处理器将是MyPDFBookReader
。稍后这个约定将会很方便。
我们将从一个抽象类开始,该类可以接受任何EBook
的实现,并将其存储在类属性中。该类还期望所有子类实现一个名为readBook()
的方法。
abstract class GenericBookReader
{
protected $book;
public function __construct(EBook $book)
{
$this->book = $book;
}
abstract public function readBook();
}
现在我们有了可以接受任何
EBook
的抽象类,我们可以构建特定的实现,这些实现将对特定的接口类进行类型提示 - 例如
PDFBook
或
EBook
。
class MyBookReader extends GenericBookReader
{
public function __construct(EBook $book)
{
parent::__construct($book);
}
public function readBook()
{
$this->book->read();
}
}
class MyPDFBookReader extends GenericBookReader
{
public function __construct(PDFBook $book)
{
parent::__construct($book);
}
public function readBook()
{
$this->book->doPDFOnlyThing();
$this->book->read();
}
}
这两个具体实现只是将给定的对象
$book
发送到父构造函数中,然后在
$this->book
属性中缓存它。任何需要在初始化时对书籍进行的操作都可以在
GenericBookReader
中完成,所有类都将使用新方法,而不必逐个进行更新。当然,如果特定的类需要一些特殊的初始化,则可以在它们自己的构造函数中完成,而不是在父构造函数中。
目前为止,您已经将
EBook
和
PDFBook
分别放置在自己的处理程序中,而不是在单个类中。这是一步向前的进展,因为现在在
MyPDFBookReader
类的
readBook()
方法中,您有一个保证可以使用
doPDFOnlyThing()
。
现在,为了将所有这些组合在一起,您需要一个读书客户端。客户端应该能够接受任何
EBook
,确定它是什么类型的书,创建适当的
Reader
类,然后调用
readBook()
方法。这里的命名约定很好用,因为我们可以动态地构建类名称。
class BookClient
{
public function readBook(EBook $book)
{
$name = get_class($book);
$readerClass = $name . 'Reader';
if (class_exists($readerClass))
{
$reader = new $readerClass($book);
$reader->readBook();
}
}
}
这些类的用法如下:
$client = new BookClient();
$client->readBook(new MyEBook()); //prints: reading from a MyBook
$client->readBook(new MyPDFBook()); //prints: doing what a PDF does while...reading from a MyPDFBook
所有这些看起来可能很复杂,只是为了进行一个简单的
readBook()
调用,但所获得的灵活性是值得的。例如,稍后客户会说
"哪里支持Kindle图书?",而你会说
"马上就来!"
interface KindleBook extends EBook
{
public function doKindleOnlyThing();
}
class MyKindleBook extends MyEBook implements KindleBook
{
public function doKindleOnlyThing()
{
echo 'waiting FOREVER for the stupid menu to start...';
}
}
class MyKindleBookReader extends GenericBookReader
{
public function __construct(KindleBook $book)
{
parent::__construct($book);
}
public function readBook()
{
$this->book->doKindleOnlyThing();
$this->book->read();
}
}
扩展示例用法:
$client = new BookClient();
$client->readBook(new MyEBook()); //prints: reading from a MyBook
$client->readBook(new MyPDFBook()); //prints: doing what a PDF does while...reading from a MyPDFBook
$client->readBook(new MyKindleBook()); //prints: waiting FOREVER for the stupid menu to start...reading from a MyKindleBook
这种使用抽象化的设置很好地支持了开闭原则。你需要添加一些代码,但是你没有更改任何现有的实现-甚至不包括客户端!
希望这提供了一个额外的角度来看待您的问题。看看您想要设置实现的方式,并开始查看可以抽象出来的内容。有时最好让对象彼此独立,并具有与它们一起工作的特殊处理程序。在这个例子中,没有任何一本书需要关心其他书是如何工作的。因此,一个类接受任何EBook,但具有与该接口的特定子实现一起工作的方法最终成为代码异味。
希望这可以帮助您。以下是完整的示例代码,可复制并粘贴以自行尝试。
<?php
interface EBook
{
public function read();
}
interface PDFBook extends EBook
{
public function doPDFOnlyThing();
}
interface KindleBook extends EBook
{
public function doKindleOnlyThing();
}
class MyEBook implements EBook
{
public function read()
{
echo 'reading from a ' . get_class($this);
}
}
class MyPDFBook extends MyEBook implements PDFBook
{
public function read()
{
parent::read();
}
public function doPDFOnlyThing()
{
echo 'doing what a PDF does while...';
}
}
class MyKindleBook extends MyEBook implements KindleBook
{
public function doKindleOnlyThing()
{
echo 'waiting FOREVER for the stupid menu to start...';
}
}
abstract class GenericBookReader
{
protected $book;
public function __construct(EBook $book)
{
$this->book = $book;
}
abstract public function readBook();
}
class MyBookReader extends GenericBookReader
{
public function __construct(EBook $book)
{
parent::__construct($book);
}
public function readBook()
{
$this->book->read();
}
}
class MyPDFBookReader extends GenericBookReader
{
public function __construct(PDFBook $book)
{
parent::__construct($book);
}
public function readBook()
{
$this->book->doPDFOnlyThing();
$this->book->read();
}
}
class MyKindleBookReader extends GenericBookReader
{
public function __construct(KindleBook $book)
{
parent::__construct($book);
}
public function readBook()
{
$this->book->doKindleOnlyThing();
$this->book->read();
}
}
class BookClient
{
public function readBook(EBook $book)
{
$name = get_class($book);
$readerClass = $name . 'Reader';
if (class_exists($readerClass))
{
$reader = new $readerClass($book);
$reader->readBook();
}
}
}
$client = new BookClient();
$client->readBook(new MyEBook());
$client->readBook(new MyPDFBook());
$client->readBook(new MyKindleBook());
GenericBookReader
?这样做有什么好处呢? - myolReader
类中创建一个$book
属性,并在每个类的构造函数中进行赋值。此外,也不可能有默认行为。如果您需要设置书籍作者的名称,则setAuthor($author)
方法适合放在父类中,而不是在所有类中重复自己。虽然在示例中只有3个类,但如果有20个呢? - Crackertastic