什么定义了有效的对象状态?

时间:2017-04-05 16:29:11

标签: php oop design-patterns

我正在阅读an article关于施工人员做太多工作的问题。 一段写着

  

在面向对象的样式中,依赖关系倾向于被反转,构造函数具有不同的Spartan角色。它唯一的工作是确保对象初始化为满足其基本不变量的状态(换句话说,它确保对象实例以有效状态启动,而不是更多)。

这是一个类的基本示例。在创建类时,我传入需要解析的HTML,然后设置类属性。

OrderHtmlParser
{
    protected $html;

    protected $orderNumber;

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

    public function parse()
    {
        $complexLogicResult = $this->doComplexLogic($this->html);

        $this->orderNumber = $complexLogicResult;
    }

    public function getOrderNumber()
    {
        return $this->orderNumber;
    }

    protected function doComplexLogic($html)
    {
        // ...

        return $complexLogicResult;
    }
}

我正在使用

来调用它
$orderparser = new OrderHtmlParser($html);
$orderparser->parse()
$orderparser->getOrderNumber();

我使用parse函数,因为我不希望构造函数执行任何逻辑,因为上面的文章和this article都说这是一种可怕的做法。

public function __construct($html)
{
    $this->html = $html;
    $this->parse(); // bad
}

但是,如果我不使用parse方法,那么我的所有属性(在此示例中只是一个)将返回null

这是否称为“无效状态”的对象?

此外,它有些感觉就像我的解析方法是一个伪装的initialise函数,另一篇文章也认为它很糟糕(尽管我不确定这是否只是在构造函数调用该方法,在手动调用时或两者都调用。无论如何,初始化方法在设置属性之前仍然会做一些复杂的逻辑 - 这需要在可靠地调用getter之前发生。

所以要么我误解了这些文章,要么这些文章都在促使我认为这个简单类的整体实现可能不正确。

6 个答案:

答案 0 :(得分:6)

通常,在构造函数中执行工作是一种代码气味,但实践背后的原因更多地与编程语言有关,而不是对最佳实践的看法。有真正的边缘情况会引入错误。

在某些语言中,派生类的构造函数从下往上执行,而其他语言则从上到下执行。在PHP中,它们从上到下调用​​,您甚至可以通过不调用parent::__construct()来停止链。

这会在基类中创建未知的状态期望,并且更糟糕的是PHP允许您在构造函数中首先调用parent或者在构造函数中调用last。

例如;

class A extends B {
     public __construct() {
           $this->foo = "I am changing the state here";
           parent::__construct(); // call parent last
     }
}

class A extends B {
     public __construct() {
           parent::__construct(); // call parent first
           $this->foo = "I am changing the state here";
     }
}

在上面的示例中,类B的构造函数以不同的顺序调用,如果B在构造函数中执行了大量工作,那么它可能不会处于程序员期望的状态。

那么你如何解决问题?

这里需要两个类。一个将包含解析器逻辑,另一个将包含解析器结果。

class OrderHtmlResult {
      private $number;
      public __construct($number) {
            $this->number = $number;
      }
      public getOrderNumber() {
            return $this->number;
      }
}

class OrderHtmlParser {
      public parse($html) {
          $complexLogicResult = $this->doComplexLogic($this->html);
          return new OrderHtmlResult($complexLogicResult);
      }
}

$orderparser = new OrderHtmlParser($html);
$order = $orderparser->parse($html)
echo $order->getOrderNumber();

在上面的示例中,如果parse()方法无法提取订单号,则可以让null方法返回{{1}},或者举例。但是这两个阶级都没有进入无效状态。

这个模式有一个名称,其中一个方法产生另一个对象作为结果以封装状态信息,但我记得它被称为。

答案 1 :(得分:4)

  

这是一个处于无效状态的对象'?

是。您完全正确地认为parse方法是伪装的initialise函数。

为了避免初始化解析,是懒惰的。最懒的方法是消除$orderNumber字段并从$html函数内部getOrderNumber()解析它。如果您希望重复调用该函数和/或您希望解析成本很高,那么请保留$orderNumber字段,但将其视为缓存。在null内检查getOrderNumber()并仅在第一次调用时解析它。

关于链接的文章,我原则上同意构造函数应限于字段初始化;但是,如果这些字段是从一个文本块中解析出来的,并且期望客户端将使用大部分或全部解析的值,那么延迟初始化几乎没有价值。 此外,当文本解析不涉及IO或new域对象时,它不应该阻碍黑盒测试,因为急切初始化是不可见的。

答案 2 :(得分:3)

当构造函数“过多”时发生的一个常见问题是两个有点紧密相关的对象需要相互引用(是的,紧密链接是一种难闻的气味,但它会发生)。

如果对象A和对象B必须互相引用才能“有效”,那么如何创建?

答案通常是你的构造函数使对象不完全“有效”,你将引用添加到另一个无效对象,然后调用某种finalize / initialize / start方法来完成并使你的对象有效。

如果您仍希望成为“安全”,则可以通过在对象为“有效”之前调用未初始化的异常来保护您的业务方法。

依赖注入有一个这个问题的通用版本,如果你有一个循环的注入类怎么办?构造/初始化模式之后也解决了一般情况,因此DI总是使用该模式。

答案 3 :(得分:3)

解决标题中的问题我一直认为对象在没有任何问题的情况下可以执行其工作时处于有效状态;也就是说,它按预期工作。

在查看链接的文章时,我跳出的是构造函数逻辑创建了很多对象:我计算了7.所有这些对象都与所讨论的类(ActiveProduct)紧密耦合,因为它们是直接引用的并且构造函数将this指针传递给其他对象构造函数:

    VirtualCalculator = new ProgramCalculator(this, true);
    DFS = new DFSCalibration(this);

在这种情况下,ActiveProduct还没有完成初始化,但ProgramCalculator和DFSCalibration可以通过方法和属性回调到ActiveProduct,并导致各种恶作剧,因此代码非常可疑。 通常在OOP中,您希望将对象传递给构造函数,而不是在构造函数中实例化它们。您还希望使用Dependency Inversion Principle并在将对象传递给允许dependency injection的构造函数时使用接口或抽象/纯虚拟类。

对于您的类OrderHtmlParser,这似乎不是一个问题,因为有问题的复杂逻辑似乎不在OrderHtmlParser类之外。我很好奇为什么doComplexLogic函数被定义为受保护,这意味着继承类可以调用它。

那说如何处理初始化可能就像使Parse方法静态并使用它构造OrderHtmlParser类的实例并使构造函数成为私有一样简单,以便调用者必须调用Parse方法来获取实例:

OrderHtmlParser
{
    protected $html;

    protected $orderNumber;

    private function __construct()
    {

    }

    public static function parse($html)
    {
        $instance = new OrderHtmlParser();

        $instance->html = $html;

        $complexLogicResult = $instance->doComplexLogic($this->html);

        $instance->orderNumber = $complexLogicResult;

        return $instance;
    }

    public function getOrderNumber()
    {
        return $this->orderNumber;
    }

    protected function doComplexLogic($html)
    {
        // ...

        return $complexLogicResult;
    }
}

答案 4 :(得分:1)

谢谢你提出的好问题!

这是一个容易出错的设计,可以将大数据传递给构造函数,构造函数只是将它存储在对象中,以便以后处理这些大数据。

让我再次引用你的美丽报价(大胆的标记是我的):

  

在面向对象的样式中,依赖关系倾向于被反转,构造函数具有不同的Spartan角色。 唯一的工作就是确保对象初始化为满足其基本不变量的状态(换句话说,它确保对象实例在有效状态下启动,而不是更多)。 / p>

你的例子中解析器类的设计很麻烦,因为构造函数接受输入数据是一个真正的数据来处理,而不仅仅是如下面引用中提到的“初始化数据”,但实际上并不处理数据。

在非常古老的编程课程中,在1980年代,我们被告知程序有输入和输出。

将$ html视为程序输入。

构造函数不应该接受程序输入。它们只应接受配置,初始化数据,如字符集名称或其他可能不会在以后提供的配置参数。如果他们接受大数据,他们可能需要有时抛出异常,构造函数中的异常是一种非常糟糕的风格。应不惜一切代价避免施工人员的例外情况。例如,您可以将文件名传递给构造函数,但不应该在构造函数中打开文件,依此类推。

让我稍微修改你的课程。

enum ParserState (undefined, ready, result_available, error);


OrderHtmlParser
{

    protected $orderNumber;
    protected $defaultEncoding;
    protected ParserState $state;

    public function __construct($orderNumber, $defaultEncoding default "utf-8")
    {
        $this->orderNumber = $orderNumber;
        $this->defaultEncoding = $defaultEncoding;
        $this->state = ParserState::ready;
    }

    public function feed_data($data)
    {
        if ($this->state != ParserState::ready) raise Exception("You can only feed the data to the parser when it is ready");

        // accumulate the data and parse it until we get enough information to make the result available

        if we have enough result, let $state = ParserState::resultAvailable;
    }

    public function ParserState getState()
    {
        return $this->state
    }

    public function getOrderNumber()
    {
        return $this->orderNumber;
    }

    protected function getResult($html)
    {
        if ($this->state != ParserState::resultAvailable) raise Exception("You should wait until the result is available");

        // accumulate the data and parse it until we get enough information to make the result available
    }
}

如果您将设计课程以使其具有明显的设计,人们将不会忘记调用任何方法。原始问题中的设计存在缺陷,因为与逻辑相反,构造函数确实采用了数据但没有对其进行任何操作,并且需要一个不明显的特殊函数。如果您将设计简单明了,您甚至不需要状态。只有在结果准备好的情况下长时间累积数据的类才需要状态,例如,从TCP / IP套接字异步读取HTML以将此数据提供给解析器。

$orderparser = new OrderHtmlParser($orderNumber, "Windows-1251");
repeat
  $data = getMoreDataFromSocket();
  $orderparser->feed_data($data);
until $orderparser->getState()==ParserState::resultAvailable;
$orderparser->getResult();

关于对象状态的初始问题。如果你设计一个类,使得构造函数只有在接收和处理数据的方法时才获取初始化数据,所以没有单独的函数来存储数据并解析可能忘记调用的数据,没有状态是必要的。如果仍然需要按顺序收集或提供数据的长生命对象的状态,则可以使用上面示例中的枚举类型。我的例子是抽象语言,而不是特定的编程语言。

答案 5 :(得分:0)

检查他下面可能会有所帮助

在状态模式中,当环境发生变化时,课程将改变它的行为。

在此示例中,BookContext类包含BookTitleStateInterface的实现,从BookTitleStateStars开始。 BookTitleStateStars和BookTitleStateExclaim将在BookContext中相互替换,具体取决于它们被调用的次数。

<?php

class BookContext {
    private $book = NULL;
    private $bookTitleState = NULL; 
    //bookList is not instantiated at construct time
    public function __construct($book_in) {
      $this->book = $book_in;
      $this->setTitleState(new BookTitleStateStars());
    }
    public function getBookTitle() {
      return $this->bookTitleState->showTitle($this);
    }  
    public function getBook() {
      return $this->book;
    }
    public function setTitleState($titleState_in) {
      $this->bookTitleState = $titleState_in;
    }
}

interface BookTitleStateInterface {
    public function showTitle($context_in);
}

class BookTitleStateExclaim implements BookTitleStateInterface {
    private $titleCount = 0; 
    public function showTitle($context_in) {
      $title = $context_in->getBook()->getTitle();
      $this->titleCount++;
      $context_in->setTitleState(new BookTitleStateStars());
      return Str_replace(' ','!',$title);
    }
}

class BookTitleStateStars implements BookTitleStateInterface {
    private $titleCount = 0; 
    public function showTitle($context_in) {
      $title = $context_in->getBook()->getTitle();
      $this->titleCount++;
      if (1 < $this->titleCount) {
        $context_in->setTitleState(new BookTitleStateExclaim); 
      }
      return Str_replace(' ','*',$title);
    }
}

class Book {
    private $author;
    private $title;
    function __construct($title_in, $author_in) {
      $this->author = $author_in;
      $this->title  = $title_in;
    }
    function getAuthor() {return $this->author;}
    function getTitle() {return $this->title;}
    function getAuthorAndTitle() {
      return $this->getTitle() . ' by ' . $this->getAuthor();
    }
}

  writeln('BEGIN TESTING STATE PATTERN');
  writeln('');

  $book = new Book('PHP for Cats','Larry Truett');;
  $context = new bookContext($book);

  writeln('test 1 - show name');
  writeln($context->getBookTitle());
  writeln('');

  writeln('test 2 - show name');
  writeln($context->getBookTitle());
  writeln('');

  writeln('test 3 - show name');
  writeln($context->getBookTitle());
  writeln('');

  writeln('test 4 - show name');
  writeln($context->getBookTitle());
  writeln('');

  writeln('END TESTING STATE PATTERN');

  function writeln($line_in) {
    echo $line_in."<br/>";
  }

?>

<强>输出

BEGIN TESTING STATE PATTERN

test 1 - show name
PHP*for*Cats

test 2 - show name
PHP*for*Cats

test 3 - show name
PHP!for!Cats

test 4 - show name
PHP*for*Cats

END TESTING STATE PATTERN

来自reference

的参考资料