Come rendere immutabili gli oggetti complessi?

3

The example I'm about to give is for the PHP language, but I think this scenario applies to most languages.

Diciamo che ho un oggetto chiamato Response e voglio che sia immutabile. Ogni metodo dovrebbe restituire una nuova istanza piuttosto che modificare lo stato dell'istanza corrente. Response s ha intestazioni, quindi creo un metodo chiamato withHeader($name, $value) che crea una copia del mio oggetto risposta, imposta l'intestazione e restituisce la nuova istanza. È piuttosto semplice da usare:

$newResponse = $oldResponse->withHeader('Content-Type', 'application/json');

Quindi decido che le intestazioni in realtà abbiano alcune proprietà e preferisco separarle in un oggetto separato chiamato HeaderBag . Ora voglio che il mio Response tenga un HeaderBag ma voglio comunque che sia immutabile.

Non posso rendere la borsa una proprietà pubblica, come $response->headers perché anche se l'oggetto $headers è immutabile, la proprietà può ancora essere riassegnata. Quindi forse aggiungo qualcosa come $response->getHeaders() che restituisce il mio oggetto HeaderBag , ma ora come lo manipolo?

Se sposto il mio metodo withHeader in questo sottooggetto, allora finisco con qualcosa come $response->getHeaders()->withHeader(...) . Il metodo withHeader restituirebbe presumibilmente un nuovo HeaderBag poiché anch'esso è immutabile, ma allora? Credo che finirei con qualcosa di simile:

$newResponse = $oldResponse->withHeaders($oldResponse->getHeaders()->withHeader($name, $value));

Che sembra un po 'ridicolo; per aggiungere un singolo valore dobbiamo clonare due oggetti e scrivere questo abominio di una linea. Certo, possiamo scrivere un metodo di supporto sull'oggetto Response per accorciarlo, ma l'idea era di spostare tutti i metodi relativi all'intestazione nella classe HeaderBag .

Domande:

  1. C'è un modo migliore per farlo?
  2. Se non c'è, vale anche lo sforzo / il costo per rendere gli oggetti immutabili?
  3. Se potessimo progettare un nuovo linguaggio con immutabilità in mente, come sarebbe questo?
posta mpen 19.04.2015 - 20:44
fonte

3 risposte

7

Una risposta http può essere naturalmente ritenuta immutabile, ma solo dal momento in cui è stata completamente costruita e emessa . Prima di quel momento, varie implementazioni di classi di "risposta" in vari ambienti sono generalmente usate come builder (si veda Pattern Builder ) che vengono scambiate in modo che le varie routine aggiungano loro frammentarie tutte le informazioni che costituiscono la risposta finale. Quindi, durante la vita della risposta come costruttore, è per sua stessa natura mutabile .

Ora, è ancora possibile prendere una classe che è mutabile per sua natura e trasformarla in una classe immutabile, ma quello che vorrei suggerire è che, anche se così facendo, potrebbe essere un favoloso esercizio sulla carta, tutta la serie di articoli di Erik Lippert sull'immutabilità era a dir poco affascinante,) non è qualcosa che vorresti fare in pratica con qualsiasi classe non banale, perché finisci con abomini proprio come quello che hai scoperto. Inoltre, non vale sicuramente la pena, ed è incredibilmente dispendioso in termini di risorse informatiche.

Quindi, il mio suggerimento sarebbe che se ti piace davvero l'idea di una risposta immutabile, (mi piace anche a me), rinomina la tua risposta attuale a ResponseBuilder , lascia che sia mutabile, e quando è completa, crea una risposta immutabile e scarta il costruttore.

    
risposta data 19.04.2015 - 21:03
fonte
3

Cercherò di rispondere alle tre domande che hai posto in fondo.

1) C'è un modo migliore per farlo?

Il modo migliore per farlo è costruire una biblioteca o un linguaggio generico in grado di gestire in modo efficiente la creazione e l'aggiornamento di oggetti immutabili. Cercherò di risolvere questo problema nella risposta alla terza domanda.

2) Se non c'è, vale anche lo sforzo / il costo per rendere gli oggetti immutabili?

Questa è una domanda difficile, ma direi che, a meno che PHP non disponga di una robusta libreria per questo (che può o no, non ho familiarità con PHP), la risposta è probabilmente no. Innanzitutto, sarebbe inefficiente e soggetto a errori se si clonavano i propri oggetti. In secondo luogo, se stai lavorando in una lingua come PHP, dove l'intera lingua e comunità sono costruite e utilizzate per la mutabilità, può essere fonte di confusione per altri programmatori quando una libreria è immutabile.

3) Se potessimo progettare un nuovo linguaggio con immutabilità in mente, come sarebbe questo?

Fortunatamente non avresti dovuto progettare una nuova lingua per sapere come sarebbe stata, perché è già stata eseguita. Puoi guardare quelli esistenti come Haskell o Clojure. Queste lingue sono state progettate pensando all'immutabilità. Quasi tutto in queste lingue è immutabile: elenchi, vettori, mappe, insiemi, record ecc. E sono implementati in un modo che rende gli aggiornamenti molto più veloci della copia dell'intero oggetto.

Clojure per istanze usa Hash array mapped trie link per implementare i suoi Vettori, Mappe, Set e Record che danno O ( log (n)) inserire / aggiornare la velocità e l'implementazione sottostante non è mai visibile all'utente finale (infatti i vettori o le mappe di piccole dimensioni possono utilizzare una rappresentazione completamente diversa).

Quando quasi tutto nella tua lingua è immutabile, alcuni dei problemi che hai dovuto affrontare vanno via. Ad esempio, se la risposta tiene una borsa, non c'è un grosso problema nell'esporre la borsa perché anche la borsa è immutabile. Inoltre, quando crei una nuova risposta in cui il codice di stato è cambiato, ma le intestazioni sono le stesse, non è necessario copiare le intestazioni Borsa, puoi semplicemente creare un puntatore perché la borsa e tutto ciò che contiene sono immutabili ! Consentire al compilatore, al runtime e alle librerie di formulare supposizioni sull'immutabilità di altre cose nella lingua è parte di ciò che consente alle lingue progettate con strutture di dati immutabili di essere più efficienti e più utilizzabili rispetto al tentativo di aggiungere l'immutabilità come ripensamento a un linguaggio iniziato mutevole.

Questo è in realtà il modo in cui il popolare framework Ring link funziona in Clojure. Una risposta è solo una mappa:

{:status 200
  :headers {"Content-Type" "text/html"}
  :body "Hello World"}

Come puoi vedere, la risposta è una mappa e le intestazioni sono solo un'altra mappa. Come apparirebbe per produrre una nuova mappa con Content-Type="text / xml" sarebbe:

(assoc-in response [:headers "Content-Type"] "text/xml")

La funzione assoc-in proviene dalla libreria standard che fa il lavoro dall'esempio "ridicolo" di sapere come ottenere la mappa nidificata e ricostruire le strutture dati di cui faceva parte in modo che il programmatore non avesse bisogno per effettuare chiamate esplicite alle funzioni withHeader e withHeaders . Esistono anche numerose altre tecniche e funzioni che semplificano le operazioni comuni. Ad esempio, se avessimo più di un aggiornamento che avremmo voluto inserire nella risposta, potremmo utilizzare -> o macro thread-first.

(-> response
   (assoc-in [:headers "Content-Type"] "text/xml")
   (assoc-in [:headers "Content-Disposition"] "attachment; filename=\"data.xml\""))

Ci sono anche molti metodi nelle librerie standard per queste lingue che potrebbero applicare una funzione a un valore in una struttura nidificata, unire due mappe immutabili insieme, confrontare Mappe per l'uguaglianza, ecc.

    
risposta data 19.04.2015 - 22:11
fonte
1

Disclaimer: I wrote this as I was coming up with it

Penso che idealmente avremmo proprietà di sola lettura, come in C #. Quindi potremmo esporre $response->headers senza preoccuparci che venga nuovamente assegnato. Quindi ciò di cui abbiamo bisogno è un modo per HeaderBag::withHeader di restituire un oggetto (nuovo) Response , ma solo quando l'accesso avviene tramite l'oggetto risposta - non se stiamo lavorando con esso da solo. ciò restituirebbe un nuovo oggetto Response ,

$newResponse = $oldResponse->headers->withHeader('Content-Type', 'text/xml')

ma ciò restituirebbe un oggetto HeaderBag :

$newHeaders = $standaloneHeaders->withHeader('Content-Type', 'text/xml');

Questo naturalmente sarebbe super confuso se il metodo restituisse oggetti diversi a seconda di come veniva chiamato, quindi abbiamo bisogno di sistemare la sintassi un po 'per renderla più chiara ...

E se avessimo un nuovo operatore "with" (ω) che si comporta un po 'come assignment (=) tranne che restituisce una nuova istanza dell'oggetto a sinistra con la nuova serie di proprietà. Ad esempio, invece di

obj.x = 5

Scriveresti

obj2 = obj ω x = 5

Ancora meglio, puoi manipolare diverse proprietà contemporaneamente mentre fai una sola copia dell'oggetto:

obj2 = obj ω x = 5, y = 6

Tornando al nostro esempio sopra, potremmo scrivere:

$newResponse = $oldResponse ω headers = $oldResponse->headers->withHeader('Content-Type', 'text/xml');

Ma non è molto meglio. Forse possiamo aggiungere un altro simbolo per rappresentare l'oggetto che viene manipolato? Ad esempio, se si desidera incrementare un valore, è possibile scrivere:

obj2 = obj ω x = obj.x + 1

o

obj2 = obj ω x = τ + 1

Dove τ si riferisce alla proprietà subito dopo ω . Ora possiamo scrivere:

$newResponse = $oldResponse ω headers = τ->withHeader('Content-Type', 'text/xml');

Migliorare, ma penso che possiamo ancora migliorarlo. Supponiamo che il nostro HeaderBag implementa un'interfaccia dell'array, in modo tale da ottenere un'intestazione tramite $response->headers['Content-Type'] .

$newResponse = $oldResponse ω headers = τ ω τ['Content-Type'] = 'text/xml';

Vediamo ora come impostare tutte le proprietà contemporaneamente:

$newResponse = $oldResponse 
    ω headers = τ ω τ['Content-Type'] = 'text/xml', 
                    τ['Content-Disposition'] = 'attachment; filename="data.xml"',
      status = 200,
      body = '<root>text</root>'

Penso che potrei aver bisogno di alcune parentesi, o forse dovrei rimuovere l'operatore , e richiedere più ω s, non ne sono sicuro, ma questa è l'idea generale.

Potremmo voler sostituire ω con qualcosa di più semplice da scrivere, forse w/ ? Per τ stavo pensando "questo" ma questo ha un significato diverso all'interno delle classi.

Questo richiede un po 'più di riflessione, ma accolgo con favore il tuo feedback. WuHoUnited ha fornito alcuni esempi di manipolazione di oggetti nidificati in Clojure, ma non sono sicuro che sia flessibile come la mia soluzione che consente di chiamare qualsiasi metodo in qualsiasi punto della catena.

Tenendo presente questa soluzione semi-ideale, possiamo provare a convertirla in PHP sintatticamente valido:

$newResponse = $oldResponse->withProp(['headers' => function($headers) {
    return $headers->withKey(['Content-Type' => 'application/json']);
]);

Qualcosa del genere. L'implementazione sarà simile a:

<?php
trait WithTrait {

    public function withProp($data) {
        $new = clone $this;

        foreach($data as $prop=>$cb) {
            $new->{$prop} = $cb($this->{$prop});
        }

        return $new;
    }

    public function withKey($data) {
        $new = clone $this;

        foreach($data as $key=>$cb) {
            $new[$key] = $cb($this[$key]);
        }

        return $new;
    }
}

Penso che questo potrebbe funzionare, ma non credo di voler seguire questa strada quando la soluzione di Mike è molto più semplice.

    
risposta data 20.04.2015 - 00:11
fonte

Leggi altre domande sui tag