我正在学习OOP,特别是接口。我也在尝试学习SOLID,在这种情况下是D。
从this站点开始,初始程序实现了'concretion' - 在这种情况下,PDFBook
被打字以传递给构造函数。后来,
此类型提示更改为常规EBook
接口。接受任何实现此接口的内容。在这种情况下有道理。
然而,即使编码到接口,我发现通常还没有在接口中定义额外的方法,但这些方法对于该结构是唯一的。
在这种情况下,PDFBook可能在实现doDPFOnlyThing
接口的任何其他类中未定义方法EBook
。
如果我将PDFBook
对象传递给myFunc()
哪个类型提示EBook
接口,根据我的理解,如果我只使用 定义的方法在界面 - read()
- 然后这会坚持DIP是吗?同样,传入myFunc()
实现接口的任何内容都可以调用其read()
方法,因为它遵守接口协定。
myFunc(Ebook $book) {
$book->read();
}
如果myFunc()
必须使用doDPFOnlyThing()
仅在PDFBook类中可用,该怎么办?我假设这会添加依赖项,因为此方法仅存在于PDFBook
具体化?
myFunc(Ebook $book) {
$book->doDPFOnlyThing();
}
在这种情况下,做什么更好?
答案 0 :(得分:6)
虽然通过实现对接口进行类型提示有助于减少耦合,但在尝试编写通用接口时也会很痛苦。如你所说,使用知道的方法会很好。
那就是说,你实际上有两种不同的方法。在调用myFunc
并传递EBook
时,您绝对应该依赖接口中的方法。如果某个方法需要调用doPDFOnlyThing
并且它依赖于EBook
而不是PDFBook
,那么这将违反该原则。
你可以做的一件事是:
public myFunc(EBook $book)
{
$book->read();
}
public myPDFFunc(PDFBook $book)
{
$book->read(); //Still valid from EBook contract
$book->doPDFOnlyThing();
}
虽然这可能会起作用,但这是一个肮脏的修复,你可能最终会违反开放/封闭原则,因为你将回来编辑课程。 (最终,客户需要KindleBook
,其中包含doKindleOnlyThing
方法。)
那么如何解决这个问题?
你想要对一个界面进行类型提示但是使用实现中的方法的问题就像吃蛋糕一样吃它......
要解决此问题,您需要更多地抽象设计。让我们使用一个示例,您将创建一个客户端,该客户端将读取各种格式的书籍,这些书籍都来自作为基类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()
{
//you only need to override this method
//if needed, otherwise you can leave it
//out and default to the parent class
//implementation.
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()
{
//You are safe to use PDFBook methods here
//because you have a guarantee they are available
$this->book->doPDFOnlyThing();
$this->book->read();
}
}
这两个具体实现只是将$book
中的给定对象发送到父构造函数,然后父构造函数将其缓存在$this->book
属性中。初始化时需要对任何书籍执行的任何操作都可以在GenericBookReader
中完成,所有类都将使用新方法,而不必单独更新。当然,如果一个特定的类需要一些特殊的初始化,可以在它们自己的构造函数而不是父构造函数中完成。
此时,您已在自己的处理程序中而不是在单个类中将EBook
和PDFBook
相互抽象。这是向前迈出的一步,因为现在在readBook()
类的MyPDFBookReader
方法中,您可以保证doPDFOnlyThing()
可供使用。
现在要将所有这些粘合在一起,你需要一个阅读书籍的客户。客户端应该能够接受任何EBook
,确定它的类型,创建相应的Reader
类,然后调用readBook()
方法。命名约定在这里工作得很好,因为我们可以动态地构建类名。
class BookClient
{
public function readBook(EBook $book)
{
//Get the class name of $book
$name = get_class($book);
//Make the 'reader' class name and see if it exists
$readerClass = $name . 'Reader';
if (class_exists($readerClass))
{
//Class exists - yay! Read the book...
$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()
{
//You are safe to use KindleBook methods here
//because you have a guarantee they are available
$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()
{
//you only need to override this method
//if needed, otherwise you can leave it
//out and default to the parent class
//implementation.
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()
{
//You are safe to use PDFBook methods here
//because you have a guarantee they are available
$this->book->doPDFOnlyThing();
$this->book->read();
}
}
class MyKindleBookReader extends GenericBookReader
{
public function __construct(KindleBook $book)
{
parent::__construct($book);
}
public function readBook()
{
//You are safe to use KindleBook methods here
//because you have a guarantee they are available
$this->book->doKindleOnlyThing();
$this->book->read();
}
}
class BookClient
{
public function readBook(EBook $book)
{
//Get the class name of $book
$name = get_class($book);
//Make the 'reader' class name and see if it exists
$readerClass = $name . 'Reader';
if (class_exists($readerClass))
{
//Class exists - yay! Read the book...
$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
$client->readBook(new MyKindleBook()); //prints: waiting FOREVER for the stupid menu to start...reading from a MyKindleBook
答案 1 :(得分:4)
这是一个经过深思熟虑的问题,但有一个选项是创建一个符合您实现的对象的整个定义的接口。
但是,这违反了Open/Closed原则,因为您实现中的所有方法实际上并不需要作为您需要的依赖项。请阅读this SO post了解详情。
另一个选项是为类中所需的精确依赖项创建一个接口,然后选择仅实现这些依赖项的实现。有时您可能需要创建一个新实现,refactor现有实现,或create a wrapper现有实现
答案 2 :(得分:2)
在这种情况下,它也违反了&#34; L&#34; SOLID,作为引用有可能像Java一样抛出NOSuchMethodFoundException
。
在你的情况下,你需要两个接口,一个只有read()
功能而另一个只有doPDFOnlyThing()
所以现在你已经创建了一个适配器,用它可以调用低级别的具体化,稍后你也可以使用包含doPDFOnlyThing()
的Interace用于其他结构,如Image Pdf,安全Pdf等。因此,您需要实现两个接口。