Rompe SRP per avere preoccupazioni sulla gestione dei dati e sulla mappatura degli oggetti all'interno di una singola classe, come parte del pattern DataMapper?

3

Riepilogo

Voglio analizzare la preoccupazione di "creare un oggetto popolato dal database", da usare all'interno della mia applicazione. Il pattern DataMapper sembra fare proprio questo per me.

Definizione del pattern DataMapper: link

Tuttavia, all'interno dello stesso modello DataMapper ci sono dubbi su

  • gestione oggetti - creazione, popolazione, convalida (forse), ritorno al chiamante, distruzione, salvataggio, aggiornamento ecc.
  • caricamento dei dati acquisiti dal database (ovvero costruzione di SQL, invio di SQL a funzioni di DB nativo, ricezione di risorse di dati DB, o nel caso di PHP un array associativo di dati)
  • mappatura dei dati caricati dal database nelle variabili dell'oggetto (traduzione schema-oggetto, idratazione, ecc.)

Il mio obiettivo per quanto riguarda la mia applicazione è iniziare con "Voglio dati, dati dati" e DataMapper "qui è tutto pronto per te. E non solo i dati che volevi, ma un l'intero oggetto, già popolato con i dati, e mappato correttamente anche dallo schema DB! ". ("Wow, grazie a DataMapper!")

Domanda:

Il pattern DataMapper può contenere tutti i problemi sopra riportati in una singola classe , oppure deve suddividere ogni preoccupazione nelle proprie classi separate ?

Dettagli e amp; Qualche codice

Qui di seguito mi concentrerò sulle preoccupazioni relative a "dati" e "creazione e popolazione di oggetti". Considerando concetti come la separazione di preoccupazioni, l'iniezione di dipendenza e le best practice di programmazione orientata agli oggetti, è appropriato che un modello DataMapper contenga preoccupazione per la gestione dei dati? Possono interessare i dati (ad esempio SQL) e la preoccupazione di creare-n-populare-un-oggetto coesistono all'interno di una classe?

Nell'esempio seguente, ho SQL (noto anche come dato sui dati) nella mia classe DataMapper

class MyProductMapper
{
    function getProduct()
    {
        $sql = "SELECT name FROM product limit 1";
        $result = db_query($sql);
        $data = db_fetch_array($result);

        $product = new MyProduct($data);

        return $product;
    }
}  

Il problema dei dati dovrebbe essere separato?

Considera il codice di seguito come alternativa al codice sopra (SQL estratto da DataMapper e spostato nella propria classe di dati):

//class that concerns itself with receiving some data
//creating an object, and populating that object with the data
//returning that object
class MyProductMapper
{
    function getProduct()
    {
        $data = (new ProductData())->getNameData();  
        $product = new MyProduct($data);

        // return product to the caller
        return $product;
    }
}  

//class concerns itself with data retrieval functionality
class ProductData
{
    function getNameData()
    {
        $sql = "SELECT name FROM product limit 1";
        $result = db_query($sql);
        $dataFromDB = db_fetch_array($result);//array('name'=>"ABC-1234");
        return $dataFromDB;
    }
}  

Ho separato il recupero dei dati dalla creazione dell'oggetto / dalla popolazione di oggetti. La preoccupazione dei dati dovrebbe essere separata dalla preoccupazione della classe DataMapper, o può coesistere con altri all'interno della stessa classe, o è importante, e un modo è altrettanto buono di un altro?

    
posta Dennis 29.06.2015 - 23:53
fonte

2 risposte

5

Non ho mai visto il punto di una classe che interroga solo il database e restituisce un array (o DataSet o DataTable per quelli in .NET). La mappatura delle colonne in un database su proprietà o campi su un oggetto appartiene alla sua classe, non al costruttore. L'intero punto di un mapper dei dati è quello di disaccoppiare lo schema del database dal modello a oggetti. Passando $data come argomento costruttore a MyProduct hai incorporato lo schema del database nel tuo modello a oggetti.

Un modello più chiaro e diretto è quello di creare una classe di repository che si occupa esclusivamente del modello di oggetto. Avrebbe un oggetto mapper dei dati che associa le colonne ai campi nel tuo modello a oggetti.

Innanzitutto, il tuo mapper:

class DataMapper {
    public function toMyProduct($data) {
        $product = new MyProduct();

        foreach ($data as $key => $value) {
            // Simple property injection:
            $product->$key = $value;

            // Or setter injection
            $setter = 'set' . ucfirst($key);
            $product->$setter($value);
        }

        return $product;
    }
}

Quindi la tua classe repository, che prenderebbe il mapper come argomento costruttore:

class MyProductRepository {
    private $mapper;

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

    public function find($id) {
        $sql = "...";
        // build parameterized query
        $result = db_query($sql);
        $data = db_fetch_array($result);//array('name'=>"ABC-1234");

        return $this->mapper->toMyProduct($data);
    }

    public function update(MyProduct $product) {
        // build parameterized query for UPDATE statement
    }
}

La vera pulizia di questo approccio diventa evidente quando inizi a utilizzare queste classi insieme:

$mapper = new DataMapper();
$products = new MyProductRepository($mapper);
$product = $products->find(10);
echo $product->getName();

Puoi fare un ulteriore passo avanti e implementare il modello di unità di lavoro usando un oggetto di contesto del database, che avvolge tutto le classi di repository per ogni tabella nel database:

class DbContext {
    public __construct() {
        $this->mapper = new DataMapper();
        $this->products = new MyProductRepository($this->mapper);
    }

    public $products;

    public function commit() {
        // issue COMMIT command to the database
    }

    public function rollback() {
        // issue ROLLBACK command to the database
    }
}

Ora il tuo codice diventa ancora più pulito, con l'ulteriore vantaggio della gestione delle transazioni:

$db = new DbContext();

try {
    $product = $db->products->find(10);
    // do stuff with $product

    $db->products->update($product);
    $db->commit();
} catch (Exception $ex) {
    $db->rollback();

    throw $ex;
}

Se veramente hai bisogno di un metodo che restituisce l'array raw dal database, questo diventa solo un altro metodo sulla tua classe repository:

class MyProductRepository {
    private $mapper;

    public __construct(DataMapper $mapper) {
        $this->mapper = mapper;
    }

    public function find($id) {
        $data = $this->findData($id);

        return $this->mapper->toMyProduct($data);
    }

    public function findData($id) {
        $sql = "...";
        // build parameterized query
        $result = db_query($sql);
        $data = db_fetch_array($result);//array('name'=>"ABC-1234");

        return $data;
    }

    public function update(MyProduct $product) {
        // build parameterized query for UPDATE statement
    }
}
    
risposta data 30.06.2015 - 22:37
fonte
2

Il codice di esempio n. 2 che hai fornito è preferibile, a mio parere.

Che cosa succede se domani vuoi avere CachedProductData, devi solo aggiornare la classe dei dati di prodotto, o piuttosto introdurre un altro layer.

Se hai entrambe le classi unite come nell'esempio 1, aggiorneresti entrambe le responsabilità.

Quindi, il Principio di Responsabilità Unica sarà rotto se scegli l'esempio # 1.

    
risposta data 30.06.2015 - 19:50
fonte