Variazioni di oggetti immutabili in lotti?

4

Con i modelli immutabili, quale sarebbe il modo migliore per raggruppare diverse modifiche?

Ad esempio, abbiamo un Book che è immutabile. Devo modificare sia title che year . Potrei cambiare uno per uno, ma ciò creerebbe solo un oggetto aggiuntivo che non verrà usato. C'è un modo migliore per questo?

Posso immaginare solo qualcosa con il costruttore:

new Book(oldBook, newTitle, newYear);

o per avere uno strumento esterno per la clonazione di libri, che possiamo usare.

    
posta lawpert 19.11.2014 - 23:30
fonte

3 risposte

5

A seconda del numero di campi, potrebbe essere più semplice utilizzare il modello Builder per combinare queste modifiche piuttosto che creare un costruttore separato per ogni possibile insieme di modifiche. Richiede un oggetto che non verrà utilizzato (restituito), ma è possibile utilizzare un'istanza del builder per tutti i libri che è necessario modificare.

public final class Book {
    private final String title;
    private final Integer year;
    private final List<String> authors;

    public static final class Builder {
        private String title = null;
        private Integer year = null;
        private List<String> authors = null;

        public Book build() {
            return new Book(this);
        }

        public Builder from(Book other) {
            withTitle(other.title);
            withYear(other.year);
            withAuthors(other.authors);

            return this;
        }

        public Builder withTitle(String title) {
            this.title = title;

            return this;
        }

        public Builder withYear(Integer year) {
            this.year = year;

            return this;
        }

        public Builder withAuthors(List<String> authors) {
            if (authors.isEmpty())
                this.authors = Collections.emptyList();
            else
                this.authors = new ArrayList<String>(authors);

            return this;
        }
    }

    private Book(Builder builder) {
        this.title = builder.title;
        this.year = builder.year;
        this.authors = builder.authors;
    }

    public String getTitle() {
        return title;
    }

    public Integer getYear() {
        return year;
    }

    public List<String> getAuthors() {
        return Collections.unmodifiableList(authors);
    }
}

Per creare un nuovo oggetto da uno vecchio:

Book oldBook = new Book.Builder()//
        .withTitle("Hello")//
        .withYear(1900)//
        .withAuthors(Arrays.asList("Alice"))//
        .build();
Book newBook = new Book.Builder()//
        .from(oldBook)//
        .withTitle("World")//
        .withYear(1901)//
        .build();
    
risposta data 22.11.2014 - 23:18
fonte
2

Uno schema utile è definire un'interfaccia pubblica che possa riportare tutte le proprietà associate alla classe, nonché un'interfaccia pacchetto-privata che la estende con alcuni membri aggiuntivi. Una versione pacchetto-privata della classe dovrebbe essere mutabile, ma quella pubblica dovrebbe essere immutabile. La classe public-facing dovrebbe contenere un riferimento del tipo di interfaccia privata e implementare i suoi membri in termini di tale interfaccia privata. Inoltre, tutte le classi nel pacchetto devono mantenere invariato il fatto che nessuna istanza del tipo mutabile possa mai essere modificata dopo che un riferimento ad esso è stato memorizzato in un campo che potrebbe essere raggiungibile su un altro thread.

Invocare un metodo come WithName(String name) sulla faccia pubblica della classe potrebbe indurlo a chiamare WithName sull'oggetto incapsulato, che nella maggior parte dei casi costruirà un oggetto NameAdder che contiene il nuovo valore Name lungo con un riferimento all'oggetto incapsulato stesso. Tutti i metodi diversi da GetName e WithName sarebbero concatenati all'oggetto incapsulato; mentre GetName() restituirebbe il nuovo Name e WithName restituirebbe un nuovo oggetto NameAdder che contiene il nuovo Name e un riferimento all'oggetto incapsulato in NameAdder (piuttosto che un riferimento al NameAdder stesso).

Un ovvio problema con questo approccio è che sempre più operandi vengono eseguiti su un oggetto, le catene di invocazioni del metodo diventeranno sempre più lunghe. Per attenuarlo, l'interfaccia interna (e forse anche quella pubblica) dovrebbe includere un metodo Flatten . Chiamare Flatten su un'implementazione dell'interfaccia dovrebbe produrre un nuovo oggetto che applica tutte le modifiche che sono state aggiunte ad esso tramite metodi come WithName . Un modo per ottenere ciò sarebbe costruire un nuovo oggetto dove il valore di ogni campo è stato calcolato chiamando Get su tutti gli oggetti coinvolti. Ciò potrebbe essere costoso e inefficiente, tuttavia, se viene invocato su un oggetto che è molto "profondo".

Un approccio alternativo sarebbe avere l'interfaccia privata includendo un metodo AsFlattenedMutable che sarebbe un'istanza della classe mutabile a cui erano state applicate tutte le modifiche appropriate e a cui nessun riferimento era mai stato memorizzato in alcun campo classe ovunque . La classe mutabile potrebbe implementare AsFlattenedMutable per restituire un clone di se stesso; una classe come NameAdder implementerebbe AsFlattenedMutable per invocare AsFlattenedMutable sull'oggetto incapsulato, modificare il nome memorizzato in quell'oggetto mutabile e restituire l'oggetto dopo la mutazione. Ciò consentirebbe di risolvere abbastanza rapidamente anche una catena di modifiche piuttosto profonda.

L'unico lieve "trucco" con questo approccio è se il codice modifica un oggetto e poi memorizza un riferimento in un campo non%% co_de, il codice generato potrebbe potenzialmente rieseguire la sequenza delle operazioni in modo che il riferimento all'oggetto venga memorizzato prima tutte le modifiche sono complete. Per risolvere correttamente ciò può richiedere che un nuovo oggetto memorizzi un riferimento all'oggetto mutabile in un campo final e quindi copi il riferimento da quel campo finale a quello non finale. Dalla mia comprensione delle specifiche JVM, l'archiviazione di un riferimento a un oggetto in un campo final di un oggetto in costruzione impedirebbe il differimento di eventuali richieste in sospeso oltre il ritorno del costruttore, e la lettura del valore di quel campo impedirebbe il il campo non finale viene assegnato fino a dopo l'esecuzione del costruttore [se il codice non ha letto il riferimento dal nuovo oggetto, ma invece ha semplicemente usato una copia preesistente, quindi la chiamata al costruttore, nonché qualsiasi mutazioni in sospeso per l'oggetto di interesse - potrebbe essere differito fino a dopo l'assegnazione].

    
risposta data 22.11.2014 - 22:05
fonte
1

Ecco una variante della risposta di Matt , utilizzando il modello di builder combinato con le nuove funzionalità funzionali di Java 8:

Book.java

import java.util.*;
import java.util.function.Consumer;

public class Book
{
    private String title;
    private List<String> authors;
    private int year;

    public Book(String title, List<String> authors, int year) {
         this.title = title;
         this.authors = new ArrayList<>(authors);
         this.year = year;
    }

    /*
     * Start with the current Book, create a BookBuilder with all the same properties, then send that through a functional lambda for changing what needs changed.
     */
    public Book cloneWith(Consumer<BookBuilder> bookRebuilder) {
        BookBuilder bookBuilder = new BookBuilder(getTitle(), getAuthors(), getYear());
        bookRebuilder.accept(bookBuilder);
        return new Book(bookBuilder.getTitle(), bookBuilder.getAuthors(), bookBuilder.getYear());
    }

    public String getTitle() {
        return title;
    }

    public int getYear() {
        return year;
    }

    public List<String> getAuthors() {
        return new ArrayList<String>(authors);
    }

    public String toString() {
        StringBuilder str = new StringBuilder();

        str.append(title);

        str.append(", by ");
        for(String author : authors) {
            str.append(author);
            str.append(", ");
        }

        str.append("published ");
        str.append(year);

        return str.toString();
    }
}

BookBuilder.java (potrebbe essere altrettanto facilmente una classe interna statica)

import java.util.*;

public class BookBuilder
{
    private String title;
    private List<String> authors;
    private int year;

    public BookBuilder(String title, List<String> authors, int year) {
        this.title = title;
        this.authors = authors;
        this.year = year;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public List<String> getAuthors() {
        return authors;
    }

    public void setAuthors(List<String> authors) {
        this.authors = authors;
    }

    public int getYear() {
        return year;
    }

    public void setYear(int year) {
        this.year = year;
    }
}

Main.java

import java.util.*;

public class Main
{
    public static void main(String[] args) {
        Book book1 = new Book("Pride and Prejudice", Arrays.asList("Jane Austen"), 1813);

        Book book2 = book1.cloneWith((baseBook)-> {
            baseBook.setTitle("Pride and Prejudice and Zombies");
            baseBook.setYear(2009);
            List<String> authors = baseBook.getAuthors();
            authors.add("Seth Grahame-Smith");
            baseBook.setAuthors(authors);
        });

        System.out.println(book1.toString());
        System.out.println(book2.toString());
    }
}

Una grande differenza è che, invece di dover utilizzare una catena di chiamate fluide che potrebbero essere difficili da violare, possiamo intervenire con altre logiche all'interno della chiusura lambda. Qui BookBuilder potrebbe anche essere reso fluido in modo che possa essere altrettanto preciso.

    
risposta data 23.11.2014 - 17:45
fonte

Leggi altre domande sui tag