依赖性反转和接口

myo*_*yol 7 php oop interface

我正在学习OOP,特别是接口.我也在尝试学习SOLID,在这种情况下是D.

这个站点,初始程序实现了'concretion' - 在这种情况下PDFBook是typehinted传递给构造函数.稍后,此类型提示将更改为常规EBook界面.接受任何实现此接口的内容.在这种情况下有道理.

但是,即使在对接口进行编码时,我发现通常还没有在接口中定义额外的方法,但这些方法对于该结构是唯一的.在这种情况下,PDFBook可能有一个doDPFOnlyThing未在实现该EBook接口的任何其他类中定义的方法.

如果我传递一个类型提示接口的PDFBook对象,根据我的理解,如果我使用接口中定义的方法- 那么这会坚持DIP是吗?因此,任何传递给实现接口的东西都可以调用它们的方法,因为它遵守接口契约.myFunc()EBookread()myFunc()read()

myFunc(Ebook $book) {

    $book->read();
}
Run Code Online (Sandbox Code Playgroud)

如果只能在PDFBook类中myFunc()使用doDPFOnlyThing(),该怎么办?我假设这会添加依赖关系,因为这种方法只存在于PDFBook具体结果中?

myFunc(Ebook $book) {

    $book->doDPFOnlyThing();
}
Run Code Online (Sandbox Code Playgroud)

在这种情况下,做什么更好?

Cra*_*tic 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();
}
Run Code Online (Sandbox Code Playgroud)

虽然这可能会起作用,但这是一个肮脏的修复,你可能最终会违反开放/封闭原则,因为你将回来编辑课程.(最终客户会想要一个KindleBookdoKindleOnlyThing方法的客户.)

那么如何解决这个问题呢?

你想要对一个接口进行类型提示但是使用来自实现的方法的问题就像拥有你的蛋糕并且吃它一样......

要解决这个问题,您需要更多地抽象设计.让我们使用一个示例,您将创建一个客户端,该客户端将读取各种格式的书籍,这些书籍都是从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...';
    }
}
Run Code Online (Sandbox Code Playgroud)

EBook接口患read()方法和PDFBook接口扩展EBook,并添加doPDFOnlyThing()方法合同.具体实现MyEBookMyPDFBook将分别利用各自的接口.

接下来,我们需要构建一些处理程序类,它们可以接受任何书籍并对它们执行某种操作.我们将在这里使用一个命名约定,其中所有处理程序类都具有Reader后面的后缀.所以处理程序MyPDFBook将是MyPDFBookReader.这个惯例稍后会很方便.

我们将从一个抽象类开始,该类可以接受任何实现EBook并将其存储在类属性中.该类还希望所有子类都实现一个名为的方法readBook().

abstract class GenericBookReader
{
    protected $book;

    public function __construct(EBook $book)
    {
        $this->book = $book;
    }

    abstract public function readBook();
}
Run Code Online (Sandbox Code Playgroud)

现在我们有了可以接受的抽象类,EBook我们可以构建特定的实现,这些实现将类型提示到特定的接口类 - 例如PDFBookEBook.

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

这两个具体实现都只是将给定对象发送$book到父构造函数,然后父构造函数将其缓存在$this->book属性中.初始化时需要对任何书籍执行的任何操作都可以完成,GenericBookReader并且所有类都将使用新方法,而不必单独更新.当然,如果一个特定的类需要一些特殊的初始化,可以在它们自己的构造函数而不是父构造函数中完成.

此时,您已经在自己的处理程序中而不是在单个类中抽象EBookPDFBook远离彼此.这是向前迈出的一步,因为现在在类的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();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

以下是这些类的用法:

$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
Run Code Online (Sandbox Code Playgroud)

所有这些看起来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();
    }
}
Run Code Online (Sandbox Code Playgroud)

示例用法扩展:

$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
Run Code Online (Sandbox Code Playgroud)

这种使用抽象的特殊设置很好地支持开放/封闭原则.你必须添加一些代码,但你没有改变任何现有的实现 - 甚至不是客户端!

希望这提供了一个额外的角度来查看您的问题.查看您希望设置实现的方式,并开始查看可以抽象出来的内容.有时最好将物体放在彼此的黑暗中,并使用与它们配合使用的特殊处理程序.在这个例子中,没有一本书需要关心另一本书是如何工作的.因此,一个接受任何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
Run Code Online (Sandbox Code Playgroud)