Подтвердить что ты не робот

Что определяет действительное состояние объекта?

Я читаю статью о конструкторах, делающих слишком много работы. Один абзац гласит

В объектно-ориентированном стиле, где зависимости, как правило, инвертируются, конструктор имеет другую и более спартанскую роль. Его единственная задача - убедиться, что объект инициализируется в состояние, в котором он удовлетворяет своим основным инвариантам (другими словами, он гарантирует, что экземпляр объекта запускается в правильном состоянии и не более).

Вот базовый пример класса. При создании класса я передаю 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, потому что я не хочу, чтобы конструктор выполнял какую-либо логику как в предыдущей статье, так и в в этой статье. это ужасная практика.

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

Однако, если я не использую метод parse, тогда все мои свойства (в этом примере только один) вернут null.

Известно ли это как объект в "недопустимом состоянии"?

Кроме того, похоже, что мой метод parse является скрытой функцией initialise, которая также считается плохой другой статьей (хотя я не уверен, что это только тогда, когда конструктор вызывает этот метод, когда он вручную или оба). Несмотря на это, инициализирующий метод все еще выполняет сложную логику до установки свойства, которое должно произойти до того, как геттеры могут быть надежно вызваны.

Так что либо я неправильно понимаю эти статьи, либо эти статьи подталкивают меня к мысли, что, возможно, моя общая реализация этого простого класса неверна.

4b9b3361

Ответ 1

Как правило, это запах кода для выполнения работы в конструкторе, но причина практики больше связана с языком программирования, чем с мнением о лучших практиках. Существуют реальные случаи ребер, которые будут вводить ошибки.

В некоторых языках производные классы имеют свои конструкторы, выполненные снизу вверх, а на других языках сверху вниз. В PHP они вызываются сверху вниз, и вы даже можете остановить цепочку, не вызывая parent::__construct().

Это создает ожидания неизвестного состояния в базовых классах, и, что еще хуже, PHP позволяет либо вызывать родительский первый или последний в конструкторе.

Например:

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, если он не смог извлечь номер заказа или бросить пример. Но ни один из классов никогда не попадает в недопустимое состояние.

Существует имя для этого шаблона, где метод дает другой объект в качестве результата для инкапсуляции информации о состоянии, но я помню, что он назвал.

Ответ 2

Известно ли это как объект в "недопустимом состоянии"?

Да. Вы точно знаете, что метод parse является скрытой функцией initialise.

Чтобы избежать разбора инициализации, будьте ленивы. Самый ленивый подход - исключить поле $orderNumber и проанализировать его из $html внутри функции getOrderNumber(). Если вы ожидаете, что функция будет вызываться повторно и/или вы ожидаете, что синтаксический анализ будет дорогим, сохраните поле $orderNumber, но рассмотрите его как кеш. Проверьте его на null внутри getOrderNumber() и проанализируйте его только при первом вызове.


Что касается связанных статей, я в принципе согласен, что конструкторы должны быть ограничены инициализацией поля; однако, если эти поля анализируются из блока текста, и ожидается, что клиенты будут использовать большинство или все проанализированные значения, тогда ленивая инициализация имеет мало значения. Кроме того, когда синтаксический анализ текста не связан с IO или new подключением объектов домена, он не должен препятствовать тестированию черных ящиков, для которых невидимая нетерпительная и ленивая инициализация является невидимой.

Ответ 3

Одна из распространенных проблем, возникающих, когда конструктор "слишком много", состоит в том, что два объекта, которые несколько тесно связаны, должны ссылаться друг на друга (да, тесная связь - плохой запах, но это происходит).

Если Object A и Object B должны ссылаться друг на друга, чтобы быть "Valid", то как вы создаете?

Обычно ответ заключается в том, что ваш конструктор делает объекты не полностью "действительными", вы добавляете ссылку на другой недопустимый объект, а затем вы вызываете какой-то метод finalize/initialize/start для завершения и сделать ваш объект действительным.

Если вы все еще хотите быть "безопасным", вы можете защитить свои бизнес-методы, выбросив не инициализированное исключение, если оно вызывается до того, как объект "Действителен".

Injection Dependency имеет обобщенную версию этой проблемы, что, если у вас есть круговой цикл инъецируемых классов? После шаблона construct/initialize также решается общий случай, поэтому DI всегда использует этот шаблон.

Ответ 4

Обращаясь к вопросу в заголовке, я всегда рассматривал объект как находящийся в допустимом состоянии, когда он может выполнять свою работу без каких-либо проблем; то есть он работает так, как ожидалось.

Посмотрев на связанную статью, я выскочил из-за того, что логика конструктора создавала много объектов: я подсчитал 7. Все эти объекты были тесно связаны с рассматриваемым классом (ActiveProduct), поскольку они были указаны напрямую и конструктор передавал этот указатель другим конструкторам объектов:

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

В этом случае ActiveProduct еще не завершил свою инициализацию, но ProgramCalculator и DFSCalibration могут вернуться в ActiveProduct с помощью методов и свойств и вызвать всевозможные озорства, поэтому по этой причине код очень подозрительный. В общем случае в ООП вы хотите передать объекты конструктору, не создавая их в конструкторе. Также вы хотите использовать Принцип инверсии зависимостей и использовать интерфейсы или абстрактные/чистые виртуальные классы при передаче объектов конструкторам, которые позволят инъекция зависимостей.

В случае вашего класса 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;
    }
}

Ответ 5

Я полностью согласен с комментарием от @trincot:

Когда вы создаете Parser с конструктором, нет необходимости передавать html.

Возможно, вы хотите использовать объект Parser второй раз с другим входом.

Итак, чтобы иметь чистый конструктор, я использую функцию reset(), которая также вызывается в начале и которая сбрасывает начальное состояние объекта.

Пример:

class OrderHtmlParser
{
  protected $html;
  protected $orderNumber;

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

  public function reset()
  {
    $this->html = null;
    $this->orderNumber = null;
  }
  /**
   * Parse the given Context and return the result
   */ 
  public function parse($html)
  {
    // Store the Input for whatever
    $this->html = $html;
    // Parse
    $complexLogicResult = $this->doComplexLogic($this->html);
    // Store the Result for whatever
    $this->orderNumber = $complexLogicResult;
    // return the Result
    return $this->orderNumber;
  }

  public function getOrderNumber(){}
  protected function doComplexLogic($html){}
}

Подобно этому, Parsing Object может делать то, что он должен делать:

Разбирайте так часто, как вы хотите:

$parser = new OrderHtmlParser();
$result1 = $parser->parse($html1);
$parser->reset();
$result2 = $parser->parse($html2);

Ответ 6

Спасибо за хороший вопрос!

Это склонный к ошибкам дизайн для передачи больших данных конструктору, который просто сохранит его внутри объекта, чтобы обработать эти большие данные позже.

Позвольте мне еще раз процитировать вашу красивую цитату (смелый знак мой):

В объектно-ориентированном стиле, где зависимости, как правило, инвертируются, конструктор имеет другую и более спартанскую роль. Его единственная задача - убедиться, что объект инициализируется в состояние, в котором он удовлетворяет своим основным инвариантам (другими словами, он гарантирует, что экземпляр объекта запускается в правильном состоянии и не более).

Конструкция класса парсера в вашем примере затруднительна, потому что конструктор принимает входные данные, которые являются реальными данными для обработки, а не только "данные инициализации", как указано в приведенной ниже цитате, но фактически не обрабатывает данные.

В очень старых курсах программирования, в 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
    }
}

Если вы создадите класс таким образом, чтобы он имел очевидный дизайн, люди не забудут вызывать любой метод. Дизайн в вашем исходном вопросе был ошибочным, потому что, вопреки логике, конструктор действительно принимал данные, но ничего не делал с ним, и требовалась специальная функция, которая не была очевидна. Если вы сделаете дизайн простым и очевидным, вам не нужны даже состояния. Состояние требуется только для классов, которые накапливают данные в течение длительного времени, пока результат не будет готов, например, для асинхронного чтения HTML из сокета TCP/IP для передачи этих данных в синтаксический анализатор.

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

Как о ваших начальных вопросах о состояниях объектов. Если вы создаете класс таким образом, чтобы конструктор получал только данные инициализации, тогда как есть методы, которые получают и обрабатывают данные, поэтому нет никаких отдельных функций для хранения данных и анализа данных, которые могут быть забыты для вызова, не существует состояний необходимы. Если вам все еще нужны состояния для долгоживущих объектов, которые собирают или предоставляют данные последовательно, вы можете использовать тип перечисления, как в приведенном выше примере. Мой пример - в абстрактном языке, а не в конкретном языке программирования.

Ответ 7

проверить, что он ниже может быть полезен полный

В шаблоне состояния класс изменит его поведение при изменении условий.

В этом примере класс 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

ссылка из ссылка