patterns - php the right way
¿Qué define un estado de objeto válido? (7)
Estoy leyendo un artículo sobre constructores que hacen demasiado trabajo. Un párrafo lee
En el estilo orientado a objetos, donde las dependencias tienden a invertirse, el constructor tiene un rol diferente y más espartano. Su único trabajo es asegurarse de que el objeto se inicialice en un estado donde satisfaga sus invariantes básicos (en otras palabras, se asegura de que la instancia del objeto comience en un estado válido y nada más).
Aquí hay un ejemplo básico de una clase. En la creación de la clase, paso el HTML que debe analizarse para luego establecer las propiedades de la clase.
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;
}
}
Lo estoy llamando usando
$orderparser = new OrderHtmlParser($html);
$orderparser->parse()
$orderparser->getOrderNumber();
Utilizo una función de parse
porque no quiero que el constructor esté haciendo ninguna lógica, ya que tanto el artículo anterior como este artículo indican que esta es una práctica terrible.
public function __construct($html)
{
$this->html = $html;
$this->parse(); // bad
}
Sin embargo, si no uso el método de parse
, entonces todas mis propiedades (en este ejemplo, solo una) devolverían el null
.
¿Se conoce esto como un objeto en un ''estado inválido''?
Además, en cierto modo parece que mi método de análisis es una función de initialise
disfrazada, lo que también se considera incorrecto en el otro artículo (aunque no estoy seguro si eso es solo cuando un constructor llama a ese método, cuando se llama manualmente o ambos). En cualquier caso, el método de inicialización sigue haciendo una lógica compleja antes de establecer una propiedad, lo que debe suceder antes de que se pueda llamar a los captadores de manera confiable.
Entonces, o estoy malinterpretando estos artículos o estos artículos me están empujando a pensar que tal vez mi implementación general de esta clase simple sea incorrecta.
¿Se conoce esto como un objeto en un ''estado inválido''?
Sí. Estás exactamente en lo cierto de que el método de parse
es una función de initialise
disfrazada.
Para evitar el análisis de inicialización, sea perezoso . El enfoque más perezoso es eliminar el campo $orderNumber
y analizarlo desde $html
dentro de la función getOrderNumber()
. Si espera que esa función se llame repetidamente y / o espera que el análisis sea costoso, entonces mantenga el campo $orderNumber
pero trate como un caché. Compruébelo en busca de un null
dentro de getOrderNumber()
y getOrderNumber()
solo en la primera invocación.
Con respecto a los artículos vinculados, estoy de acuerdo en principio en que los constructores deben limitarse a la inicialización de campo; sin embargo, si esos campos se analizan a partir de un bloque de texto y se espera que los clientes utilicen la mayoría o todos los valores analizados, entonces la inicialización perezosa tiene poco valor. Además, cuando el análisis de texto no implica E / S o la creación de new
objetos de dominio, no debe impedir las pruebas de caja negra, para las cuales la inicialización ávida y perezosa es invisible.
¡Gracias por la buena pregunta!
Este es un diseño propenso a errores para pasar datos grandes al constructor que simplemente los almacenará dentro del objeto para procesar estos datos grandes más adelante.
Permítanme citar nuevamente su hermosa cita (la marca en negrita es mía):
En el estilo orientado a objetos, donde las dependencias tienden a invertirse, el constructor tiene un rol diferente y más espartano. Su único trabajo es asegurarse de que el objeto se inicializa en un estado en el que satisface sus invariantes básicos (en otras palabras, se asegura de que la instancia del objeto comience en un estado válido y nada más).
El diseño de la clase de analizador en su ejemplo es molesto porque el constructor toma los datos de entrada que son datos reales para procesar, no solo los "datos de inicialización" como se menciona en la cita a continuación, pero en realidad no procesa los datos.
En los muy antiguos cursos de programación, en la década de 1980, nos dijeron que un programa tiene entradas y salidas.
Piense en el $ html a partir de la entrada del programa.
Los constructores no deben aceptar la entrada del programa. Se supone que solo aceptan la configuración, los datos de inicialización como el nombre del conjunto de caracteres u otros parámetros de configuración que pueden no proporcionarse más adelante. Si aceptan datos grandes, es probable que se necesiten para lanzar excepciones a veces, y las excepciones en el constructor son un estilo muy malo. Las excepciones en los constructores deben evitarse a toda costa. Por ejemplo, puede pasar un nombre de archivo al constructor, pero no debe abrir archivos en el constructor, y así sucesivamente.
Déjame modificar un poco tu clase.
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
}
}
Si diseña la clase de tal manera que tenga un diseño evidente, la gente no olvidará llamar a ningún método. El diseño en su pregunta original fue defectuoso porque, contrariamente a la lógica, el constructor tomó los datos pero no hizo nada con ellos, y se necesitaba una función especial que no fuera obvia. Si va a hacer que el diseño sea simple y obvio, no necesitará siquiera estados. Los estados solo son necesarios para las clases que acumulan datos durante mucho tiempo hasta que el resultado esté listo, por ejemplo, para la lectura asíncrona del HTML desde el socket TCP / IP para enviar estos datos a un analizador.
$orderparser = new OrderHtmlParser($orderNumber, "Windows-1251");
repeat
$data = getMoreDataFromSocket();
$orderparser->feed_data($data);
until $orderparser->getState()==ParserState::resultAvailable;
$orderparser->getResult();
En cuanto a sus preguntas iniciales sobre los estados objeto. Si diseña una clase de tal manera que el constructor solo obtenga datos de inicialización mientras haya métodos que reciban y procesen los datos, entonces no hay funciones separadas para almacenar los datos y analizar los datos que pueden olvidarse de llamar, sin estados Se necesitan. Si aún necesita estados para objetos de larga duración que recopilan o suministran los datos de forma secuencial, puede usar el tipo de enumeración como en el ejemplo anterior. Mi ejemplo es en lenguaje abstracto, no en un lenguaje de programación en particular.
Compruebe que a continuación puede ser de ayuda completa
En el patrón estatal, una clase cambiará su comportamiento cuando cambien las circunstancias.
En este ejemplo, la clase BookContext tiene una implementación de BookTitleStateInterface, comenzando con BookTitleStateStars. BookTitleStateStars y BookTitleStateExclaim se reemplazarán entre sí en BookContext dependiendo de cuántas veces se llamen.
<?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/>";
}
?>
Salida
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
referencia de reference
Dirigiendo la pregunta en el título, siempre he visto que un objeto se encuentra en un estado válido cuando puede realizar su trabajo sin ningún problema; Es decir, funciona como se espera.
Al mirar el artículo vinculado, lo que me llamó la atención fue que la lógica del constructor estaba creando una gran cantidad de objetos: conté 7. Todos estos objetos estaban estrechamente relacionados con la clase en cuestión (ActiveProduct) ya que se mencionaban directamente y el constructor Pasaba este puntero a los otros constructores de objetos:
VirtualCalculator = new ProgramCalculator(this, true);
DFS = new DFSCalibration(this);
En este caso, ActiveProduct aún no ha completado su inicialización. Sin embargo, ProgramCalculator y DFSCalibration pueden volver a llamar a ActiveProduct a través de métodos y propiedades y causar todo tipo de travesuras, por lo que, por este motivo, el código es altamente sospechoso. En general, en OOP desea pasar objetos al constructor y no crear instancias en el constructor. También desea emplear el Principio de inversión de dependencia y utilizar interfaces o clases virtuales abstractas / puras al pasar objetos a constructores que permitirían la inyección de dependencia .
En el caso de su clase OrderHtmlParser, esto no parece ser un problema, ya que la lógica compleja en cuestión no parece salir de la clase OrderHtmlParser. Tenía curiosidad por saber por qué la función doComplexLogic se definía como protegida, lo que implicaba que las clases heredadas pueden llamarlo.
Dicho esto, cómo tratar con la inicialización puede ser tan simple como hacer que el método Parse estético y usarlo para construir la instancia de la clase OrderHtmlParser y hacer que el constructor sea privado, de modo que el llamador tenga que llamar al método Parse para obtener una instancia:
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;
}
}
En general, es un olor a código para realizar el trabajo en un constructor, pero la razón detrás de la práctica tiene más que ver con el lenguaje de programación que con una opinión sobre las mejores prácticas. Hay casos reales de borde que introducirán errores.
En algunos idiomas, las clases derivadas tienen sus constructores ejecutados desde abajo hacia arriba y en otros idiomas desde arriba hacia abajo. En PHP, se les llama de arriba a abajo e incluso puede detener la cadena al no llamar a parent::__construct()
.
Esto crea expectativas de estado desconocido en las clases básicas, y para empeorar las cosas, PHP le permite llamar al padre primero o al último en un constructor.
Por ejemplo;
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";
}
}
En el ejemplo anterior, la clase B
tiene su constructor llamado en diferentes órdenes y si B
estaba haciendo mucho trabajo en el constructor, entonces podría no estar en el estado que el programador esperaba.
Entonces, ¿cómo resuelves tu problema?
Necesitas dos clases aquí. Uno contendrá la lógica del analizador y el otro los resultados del analizador.
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();
En el ejemplo anterior, puede hacer que el método parse()
devuelva un null
si no puede extraer el número de pedido o lanzar un ejemplo. Pero ninguna clase entra en un estado inválido.
Hay un nombre para este patrón, donde un método produce otro objeto como resultado para encapsular información de estado, pero recuerdo cómo se llama.
Estoy totalmente de acuerdo con el comentario de @trincot:
Cuando crea el analizador con el constructor, no es necesario pasar el html.
Tal vez usted quiera usar el Objeto Analizador por segunda vez con otra entrada.
Así que para tener un constructor limpio, uso una función reset (), que también se llama en el principio y que restablece el estado inicial del objeto.
Ejemplo:
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){}
}
Así, el objeto de análisis puede hacer, lo que se supone que debe hacer:
Analiza tantas veces como quieras:
$parser = new OrderHtmlParser();
$result1 = $parser->parse($html1);
$parser->reset();
$result2 = $parser->parse($html2);
Uno de los problemas comunes que se producen cuando el constructor hace "demasiado" es que dos objetos que están estrechamente vinculados necesitan hacer referencia entre sí (Sí, la vinculación estrecha es un mal olor, pero sucede).
Si el Objeto A y el Objeto B deben hacer referencia entre sí para ser "Válido", ¿cómo se crea uno de los dos?
La respuesta suele ser que su constructor crea objetos que no son totalmente "válidos", agrega la referencia al otro objeto no válido y luego llama a algún tipo de método de finalización / inicialización / inicio para finalizar y hacer que su objeto sea válido.
Si aún desea estar "seguro", puede proteger sus métodos comerciales lanzando una excepción no inicializada si se llama antes de que el objeto sea "Válido".
La inyección de dependencia tiene una versión generalizada de este problema, ¿qué sucede si tiene un ciclo circular de clases inyectadas? Seguir el patrón de construcción / inicialización resuelve el caso general también, por lo que DI siempre usa ese patrón.