Quale pattern posso usare per fare calcoli che coinvolgono sottotipi di oggetti simili ma diversi?

3

Come faccio a progettare un codice che ha due qualità:

  • utilizza sottotipi simili ma diversi di un oggetto
  • utilizza tipi di calcolo simili ma diversi sugli oggetti sopra

Esempio

Sto effettuando il refactoring da un codice legacy che contiene istruzioni if / then / else per diversi tipi di oggetto in un file e istruzioni if / then / else per eseguire varie attività di calcolo con gli oggetti in un altro file.

Usando OOP posso riscriverlo in qualche modo in questo modo:

class Pricing {
    public $base;
}

class PumpPricing extends Pricing {
    public $pumpCost;
    public $pumpOption1;
};
class MotorPricing extends Pricing {
    public $motorCost;
    public $motorOption1;
};
class PartPricing extends Pricing {
    public $partCost;
};

Più tardi. . .

//I can put proper object creation into Factory:
$pricing = (new PriceFactory())->getPriceObject("motor");

//but how do I compartmentalize computations?
//i.e. this:

if  (get_class($pricing) == "PumpPricing") {
    $json = array( //future json
        'total' => $pricing->pumpCost + $pricing->base + $pricing->pumpOption1,
        'pump_option_1' => $pricing->pumpOption1
    );
}
else if (get_class($pricing) == "MotorPricing") {
    $json = array(
        'total' => $pricing->motorCost + $pricing->base + $pricing->motorOption1,
        'motor_option_1' => $pricing->motorOption1
    );
}

Domanda

Considerato il mio obiettivo di sbarazzarmi delle istruzioni if / then / else nel codice mainstream e di incapsulare i calcoli, dove e come faccio?

Per esempio, posso raggruppare le formule di cui sopra in un singolo getter computazionale e inserirle nell'oggetto Pricing. per simulare la linea dall'alto:

'total' => $pricing->getTotal(),
'motor_option_1' => $pricing->getOption1()

Tuttavia, mi occupo ancora di diverse opzioni che desidero raggruppare in un array, ad esempio nomi e quantità delle opzioni sono diversi.

    
posta Dennis 17.11.2015 - 21:22
fonte

3 risposte

1

1) Puoi semplicemente aggiungere un metodo serialize a ogni classe. Quindi la tua classe base Pricing definirebbe un metodo astratto come:

abstract function getPriceObject();

Quindi ogni classe definirà un getPriceObject unico.

link

2) Detto questo, anche questo sembra un problema adatto al modello di visitatore . Se qualcun altro vuole scrivere una spiegazione dettagliata di questo potrebbe essere utile, ma la ricerca online offre molti esempi di PHP.

    
risposta data 17.11.2015 - 21:47
fonte
1

Come primo passo, puoi applicare questo comportamento ai metodi degli oggetti Price gerarchia. Poi:

class PumpPricing extends Pricing {
  ...
  function serializableRepresentation() {
    return array(
      'total' => $this->pumpCost + $this->base + $this->pumpOption1,
      'pump_option_1' => $this->pumpOption1
    );
  }
}

Questo semplifica il tuo codice principale per:

$json = $pricing->serializationRepresentation();

Sfortunatamente, questo probabilmente viola il Principio di Responsabilità Singola per comportamenti più complessi. In questo caso, possiamo usare il pattern Visitor per permetterci di definire i metodi di estensione sui tuoi tipi. L'unica cosa che deve essere modificata è l'aggiunta di un metodo acceptVisitor a ogni tipo di Pricing :

class PumpPricing extends Pricing {
  ...
  function acceptVisitor($v) {
    return $v->visitPumpPricing($this);
  }
}

Ora possiamo definire le operazioni nelle classi esterne:

class SerializationRepresentationVisitor {
  ...
  function visitPumpPricing($pricing) {
    return array(
      'total' => $pricing->pumpCost + $pricing->base + $pricing->pumpOption1,
      'pump_option_1' => $pricing->pumpOption1
    );
  }
}

function serializationRepresentation($pricing) {
  $visitor = new SerializationRepresentationVisitor();
  return $pricing->acceptVisitor($visitor);
}

Cambia il tuo codice principale in:

$json = serializationRepresentation($pricing);

Credo che l'utilizzo di un visitatore come questo per consentire l'aggiunta facile di nuove funzionalità sia il miglior compromesso tra la separazione delle responsabilità e la creazione di codice leggibile. Ciò è particolarmente consigliabile quando si opera su una gerarchia di tipi stupidi che non hanno molti comportamenti propri, ma molte operazioni che agiscono sull'intera gerarchia. La tua Pricing gerarchia è un esempio, come sarebbero gli alberi sintassi astratti in un compilatore.

    
risposta data 17.11.2015 - 21:58
fonte
1

Hai due problemi qui:

  1. È necessario serializzare un modello di oggetto con differenze nei nomi delle chiavi, ma contenente gli stessi dati di base (un prezzo e un'opzione)

  2. Devi eseguire alcuni calcoli specifici in modo generico

Questo richiede due soluzioni.

Per prima cosa, affrontiamo i calcoli di esecuzione, perché questa soluzione ci consentirà di risolvere il nostro problema di serializzazione dei dati. Quello che vuoi veramente è il Pattern di strategia :

In computer programming, the strategy pattern (also known as the policy pattern) is a software design pattern that enables an algorithm's behavior to be selected at runtime. The strategy pattern

  • defines a family of algorithms,
  • encapsulates each algorithm, and
  • makes the algorithms interchangeable within that family.

Questo è esattamente ciò che stai cercando di fare. Innanzitutto, vuoi definire un'interfaccia pubblica per i tuoi oggetti strategia che:

  • Calcola il prezzo
  • Restituisce una sorta di "nome" per la strategia (utile per creare l'array JSON)
  • Restituisce il prezzo "opzione" (ancora utile per la creazione dell'array JSON)
interface PricingStrategy
{
    public function calculatePrice();
    public function getName();
    public function getOption();
}

Poiché l'algoritmo per la strategia di determinazione dei prezzi è lo stesso nel tuo esempio di codice e cambiano solo alcuni valori, possiamo implementare una classe base astratta e quindi estenderla per fornire valori specifici:

abstract class ProductPricingStrategy implements PricingStrategy
{
    private $name;
    private $basePrice;
    private $cost;
    private $option;

    protected function __construct($name, $basePrice, $cost, $option) {
        $this->name = $name;
        $this->basePrice = $basePrice;
        $this->cost = $cost;
        $this->option = $option;
    }

    protected function getBasePrice() {
        return $this->basePrice;
    }

    protected function getCost() {
        return $this->cost;
    }

    public function getOption() {
        return $this->option;
    }

    public function calculatePrice() {
        return $this->basePrice + $this->cost + $this->option;
    }

    public function getName() {
        return $this->name;
    }
}

Questo ti dà la struttura di base per creare facilmente strategie di prezzo specifiche:

class PumpPricingStrategy extends ProductPricingStrategy
{
    public function __construct() {
        parent::__construct('pump', 1000, 250, 49.99);
    }
}

class MotorPricingStrategy extends ProductPricingStrategy
{
    public function __construct() {
        parent::__construct('motor', 5000, 1500, 350);
    }
}

class PartPricingStrategy extends ProductPricingStrategy
{
    public function __construct() {
        parent::__construct('part', 50, 20, 4.99);
    }
}

Ogni classe concreta chiama il costruttore genitore protected e fornisce tutti i valori necessari per la strategia. Ora, abbiamo solo bisogno di un oggetto "fabbrica" per fornire un accesso semplice e parametrato a queste strategie:

class PricingStrategyFactory
{
    private $strategies;

    public function __construct() {
        $this->strategies = array(
            new PumpPricingStrategy(),
            new MotorPricingStrategy(),
            new PartPricingStrategy()
        );
    }

    public function find($name) {
        foreach ($this->strategies as $strategy) {
            if ($strategy->getName() == $name) {
                return $strategy;
            }
        }

        throw new Exception("Pricing Strategy '$name' not found");
    }
}

Infine, possiamo rifattorizzare il codice in questione e ridurlo a solo 6 righe di codice che non richiedono alcun if s, switch s, and s (o buts):

$factory = new PricingStrategyFactory();
$pricing = $factory->find('motor');
$json = array(
    'total' => $pricing->calculatePrice(),
    "{$pricing->getName()}_option_1" => $pricing->getOption()
);

Il metodo getName viene utilizzato per generare la chiave di un array JSON univoco che la tua risposta è in attesa. Il metodo calculatePrice diventa una scatola nera che incapsula l'algoritmo, quindi il resto della tua base di codice non ha bisogno di saperlo.

Noterai inoltre che l'interfaccia PricingStrategy non fa riferimento a nessun altro posto se non alla definizione dell'interfaccia e alla classe ProductPricingStrategy . Nell'esempio del codice breve sopra non vedrai il vantaggio dell'interfaccia. Quando devi trasferire questo oggetto della strategia di prezzo, ne vedrai i vantaggi.

In primo luogo, facciamo finta di avere un framework MVC fittizio e abbiamo creato una classe "controller" chiamata "ProductPricesController". Il framework MVC indirizza le richieste GET per /product_prices/calculate?type=ABC al metodo ProductPricesController#calculate :

class ProductPricesController extends Controller
{
    // GET: /product_prices/calculate?type=motor

    public function calculate() {
        $type = $_GET['type'];
        $factory = new PricingStrategyFactory();
        $pricing = $factory->find($type);
        $json = $this->getPriceJson($pricing);

        echo json_encode($json);
    }

    private function getPriceJson(PricingStrategy $pricing) {
        return array(
            'total' => $pricing->calculatePrice(),
            "{$pricing->getName()}_option_1" => $pricing->getOption()
        );
    }
}

Il metodo getPriceJson ha un suggerimento tipo PHP: PricingStrategy , che è il nome della nostra interfaccia. Questo aiuta lo sviluppatore perché fornisce informazioni su ciò che questo metodo si aspetta e aiuta l'applicazione perché l'interfaccia diventa un "contratto" tra il controller e la strategia di pricing in modo che i due possano interagire in modo prevedibile e modulare.

    
risposta data 18.11.2015 - 16:11
fonte