2010-10-18 17 views
5

Necesito crear un patrón de estrategia donde un usuario selecciona cuatro estrategias de una lista de veinte o treinta objetos de estrategia únicos. La lista de estrategias se ampliará a medida que el proyecto madure, y los usuarios pueden cambiar su estrategia seleccionada en cualquier momento.¿Este código es demasiado frágil?

Planeo almacenar los nombres de estrategia que han seleccionado como cadenas, y luego usar un método como este para cargar las clases de estrategia que corresponden a las cadenas que seleccionaron.

class StrategyManager { // simplified for the example 
    public $selectedStrategies = array(); 
    public function __construct($userStrategies) { 
     $this->selectedStrategies = array(
      'first' => new $userStrategies['first'], 
      'second' => new $userStrategies['second'], 
      'third' => new $userStrategies['third'], 
      'fourth' => new $userStrategies['fourth'] 
     ); 
    } 

    public function do_first() { 
     $this->selectedStrategies['first']->execute(); 
    } 

    public function do_second() { 
     $this->selectedStrategies['second']->execute(); 
    } 

    public function do_third() { 
     $this->selectedStrategies['third']->execute(); 
    } 

    public function do_fourth() { 
     $this->selectedStrategies['fourth']->execute(); 
    } 
} 

Estoy tratando de evitar una gran declaración de cambio. Mi preocupación es que esto parece un poco Stringly Typed. ¿Hay una mejor manera de lograr este objetivo sin utilizar una declaración de cambio condicional o grande?

BTW: El usuario no ingresa una cadena al seleccionar las cuatro estrategias. Tendría que mantener una lista de cadenas para presentar al usuario en un cuadro de selección y agregar nuevas a la lista a medida que agregue nuevos objetos de estrategia.

Explicación
ircmaxell expresaron un poco de confusión en cuanto a qué es lo que estoy tratando de hacer. En el ejemplo anterior, el usuario ha seleccionado cuatro estrategias de una lista y las pasa al constructor de StrategyManager como una matriz de cadenas. Los objetos de estrategia correspondientes se crean y almacenan en una matriz interna, $this->selectedStrategies

"primero", "segundo", "tercero" y "cuarto" son las teclas de matriz de la matriz interna para las cuatro estrategias diferentes seleccionadas. Después de que se haya generado el objeto StrategyManager, la aplicación utiliza el método execute de las cuatro estrategias durante varios momentos durante la vida del proceso.

Entonces, en pocas palabras ... cada vez que la aplicación necesita ejecutar el método de Estrategia número "uno" lo hace, y los resultados son diferentes dependiendo de qué estrategia fue seleccionada por el usuario para la Estrategia "uno"

+0

Estoy confundido. Son las estrategias posibles "primero", "segundo", "tercero" y "cuarto", o son una serie de comandos para la estrategia seleccionada (que se eligen antes de construir el administrador). Y si es así, ¿funcionaría mejor un patrón [Chain of Responsibility] (http://sourcemaking.com/design_patterns/chain_of_responsibility) o [Command] (http://sourcemaking.com/design_patterns/command)? ¿Podría explicar qué es exactamente lo que está tratando de hacer (y qué hace el código, por qué existen las diferentes estrategias)? – ircmaxell

+0

Actualizaré la pregunta. – Stephen

+0

¿Siempre hay 4 estrategias? ¿Y siempre se ejecutan en secuencia? ¿O son cuatro estrategias que de otro modo no estarían relacionadas y que están tratando de manejar juntas? – ircmaxell

Respuesta

1

Según sus comentarios y actualizaciones, no creo que este código sea demasiado frágil. Será más difícil de mantener si modificas la cadena de llamadas para un tipo de estrategia (do_one, do_two, etc.) o añades estrategias. Lo que en cambio recomendaría es usar un abstract factory para crear las "estrategias". Luego, en el código donde necesita la estrategia, busque el objeto de la estrategia en sí ...

La razón por la que me gusta más este método es doble. En primer lugar, solo crea las estrategias bajo demanda, por lo que no está creando objetos que no necesita. En segundo lugar, encapsula la elección de los usuarios, ya que es el único lugar que necesita buscarlo (puede construirlo con inyección de dependencia, pero también necesitaría otro lugar para administrar el edificio).

class StrategyFactory { 

    protected $strategies = array(); 

    //If you like getter syntax 
    public function __call($method, $arguments) { 
     $method = strtolower($method); 
     if (substr($method, 0, 3) == 'get') { 
      $strategy = substr($method, 3); 
      return $this->getStrategy($strategy); 
     } 
     throw new BadMethodCallException('Unknown Method Called'); 
    } 

    public function getStrategy($strategy) { 
     if (isset($this->strategies[$strategy])) { 
      return $this->strategies[$strategy]; 
     } elseif ($this->makeStrategy($strategy)) { 
      return $this->strategies[$strategy]; 
     } 
     throw new LogicException('Could not create requested strategy'); 
    } 

    protected function makeStrategy($name) { 
     //pick strategy from user input 
     if ($strategyFound) { 
      $this->strategies[$name] = new $strategy(); 
      return true; 
     } else { 
      return false; 
     } 
    } 
} 

A continuación, utilice este modo:

$strategy = $factory->getSomeStrategyName(); 
$strategy->execute(); 

o incluso con chaning:

$factory->getSomeStrategyName()->execute(); 

o sin métodos mágicos:

$factory->getStrategy('strategyName')->execute(); 
+0

Me encanta la implementación de fábrica que ha proporcionado aquí. ¡Gracias! – Stephen

2

Hmm, bueno, no creo que sea demasiado frágil. Aunque no necesitas los hilos. Simplemente podría usar una matriz ordenada ya que la denominación corresponde a 0,1,2,3 de todos modos. Si le preocupa que se proporcionen estrategias o clases no válidas, puede poner alguna validación en el administrador.

public function __construct() { 
    $this->selectedStrategies = array(
     /* could add some default strategies */ 
    ); 
} 
public function load(array $userStrategies) { 
    for($i=0; $i<3; $i++) { 
     try { 
      $rc = new ReflectionClass($userStrategies[$i]); 
      if($rc->implementsInterface('Iterator')) { 
       $this->selectedStrategies[$i] = new $userStrategies[$i]; 
      } else { 
       throw new InvalidArgumentException('Not a Strategy'); 
      } 
     } catch(ReflectionException $e) { 
      throw new InvalidArgumentException('Not a Class'); 
     } 
    } 
} 

Y en lugar de llamar a las estrategias asociativas con sus llaves, sólo tiene que

$this->selectedStrategies[0]->execute(); 

y así sucesivamente.


Otro enfoque sería utilizar

class StrategyCollection 
{ 
    protected $strategies; 

    public function __construct() { 
     $this->strategies = new SplFixedArray(4); 
    } 

    public function add(IStrategy $strategy) { 
     $this->strategies[] = $strategy; 
     return $this; 
    } 
} 

y luego llenar el Gerente/Colección desde el exterior. Con el tipo de letra para IStrategy, puede estar seguro de que solo las clases que implementan la interfaz de estrategia terminan en el administrador. Eso le ahorra las llamadas de Reflexión algo costosas al crear las estrategias. El SplFixedArray se asegura de que haya una Excepción de tiempo de ejecución cuando intenta agregar más de cuatro estrategias.


En una nota al margen, no confíes en la entrada de un selectbox. El hecho de que una casilla de selección ofrezca opciones fijas no significa que los usuarios malintencionados no puedan manipular la solicitud.Todos los datos de solicitud deben ser desinfectados y doblemente chequeados.

+1

+1 para la nota al margen y la clase StrategyCollection, que es simple y está bien construida. – Iiridayn

+1

+1 para SplFixedArray. Bonito. – Stephen

0

Si las funciones de estrategia don' Necesito estado, puedes cambiar a un funcional estilo de programación y reemplazar toda la clase con: call_user_func($strategy['first']); (segundo, etc.). Si le preocupa el espacio de nombres global, pueden almacenarse como miembros estáticos de una clase, es decir, call_user_func(array('Strategies', $strategy['first']));. A continuación, puede obtener una lista de todas las estrategias válidas (para generar y probar la selectbox) usando get_class_methods('Strategies');, lo que podría simplificar el código teniendo simplemente la única lista global de estrategias válidas.

Si necesitas estado almacenado con las funciones de estrategia - que podría utilizar algún tipo de función de llamada de almacenamiento en caché - algo así como

function doStrategy($class) { 
    static $selectedStrategies = array(); 

    if (!isset($selectedStrategies[$class])) { 
     $selectedStrategies[$class] = new $class; 
    } 

    $Strategy = $selectedStrategies[$class]; 
    $Strategy->execute(); // some versions of PHP require two lines here 
} 

Por supuesto que aún se podía utilizar una clase más de una función para hacer esto también :PAG.

El epíteto "Stringly Typed" no es aplicable para PHP ya que está débilmente tipeado y ya internamente usa cadenas para almacenar símbolos (nombres de clases y funciones, variables, etc.). Como tal, para la reflexión, el tipo de datos de cadena suele ser el más adecuado. No entraremos en lo que eso significa para el lenguaje como un todo.