Puoi considerare di consentire ai chiamanti di passare in DoubleDuple
. (Per risparmiare un po 'di digitazione, di seguito userò Vec2
al posto di DoubleDuple
.)
Esempio:
public class Vec2 {
public final double x;
public final double y;
public Vec2(double x, double y) {
this.x = x;
this.y = y;
}
public Vec2 add(Vec2 other) {
return new Vec2(this.x + other.x, this.y + other.y);
}
// ... there should be other methods applicable to 2D vectors.
}
Anche se è ragionevole rendere immutabili gli oggetti di piccole dimensioni (per consentire l'accesso in sola lettura condiviso ai getter senza doverli clonare in modo difensivo), non sono sicuro di rendere immutabili gli oggetti di grandi dimensioni, a causa della necessità di clonare ogni uno dei suoi campi quando è necessario apportare una modifica.
In altre parole, se l'oggetto è di grandi dimensioni (con molti campi) e se il suo uso tipico (semanticamente) spesso richiede modifiche e se non è necessario mantenere lo stato dell'oggetto precedente (come nel modello di memento), quindi penserei che modificare l'oggetto sul posto sarebbe un modo più naturale di programmare in Java.
Nota che questo parere non è applicabile ad altre lingue, perché le lingue progettate per la programmazione funzionale avrebbero altri meccanismi per risolvere questo problema.
Pertanto, implementerei Player
come segue:
public class Player
{
// ... same fields and constructors as yours
// ... but they will be mutable.
public void setPosition(Vec2 newPos) {
// Vec2 being immutable means you do not have to worry
// about the caller mutating newPos after calling
// Player.setPosition(Vec2)
// Meanwhile, Player is mutable.
this.pos = newPos;
}
public void addPosition(Vec2 deltaPos) {
// The cost we pay for making Vec2 immutable
// (in terms of lines of code) is minimal. However,
// same cannot be said of the cost of making Player
// immutable. My suspicion is that it could greatly
// increase the lines of code for Player.
this.pos = this.pos.add(deltaPos);
}
public Vec2 getPosition() {
// Vec2 being immutable means you do not have to worry
// about the caller mutating the returned Vec2, which
// would have caused havoc on the Player.pos field
// if Vec2 isn't immutable.
return this.pos;
}
// ... and others
}
Infine, puoi considerare di mettere insieme pos
e mom
, in una classe Motion
, e poi aggiungerlo come un campo alla classe Player
.
// Motion can be mutable or immutable;
// shown below is the mutable implementation.
public class Motion
{
private Vec2 pos;
private Vec2 mom;
public Motion(Vec2 pos, Vec2 mom) { ... }
public void setPosition(Vec2 newPos) { ... }
public void addPosition(Vec2 deltaPos) { ... }
public Vec2 getPosition() { ... }
public void setMomentum(Vec2 newMom) { ... }
public void addMomentum(Vec2 deltaMom) { ... }
public Vec2 getMomentum() { ... }
// ... add other methods as needed.
}
public class Player {
private Motion motion;
// ... constructors and other fields
// ... Now, we face a dilemma. Motion has a lot of methods,
// ... but we don't want to copy all those methods here.
// Option 1 - bite the bullet. Each method will delegate
// to the method of same name on the Motion instance.
public void setPosition(Vec2 newPos) { ... }
public void addPosition(Vec2 deltaPos) { ... }
public Vec2 getPosition() { ... }
public void setMomentum(Vec2 newMom) { ... }
public void addMomentum(Vec2 deltaMom) { ... }
public Vec2 getMomentum() { ... }
// Option 2 - getters and setters
// As discussed above, getters and setters benefit from
// immutable objects by not having to defensively clone them.
public Motion getMotion() { return motion; }
public void setMotion(Motion newMotion) { motion = newMotion; }
// Option 3
// Requires Java 8 Lambda.
// Requires the MotionUpdater interface below.
// Motion can be mutable or immutable; doesn't care.
public void changeMotion(MotionUpdater motionUpdater) {
this.motion = motionUpdater.update(this.motion);
}
}
// Used by Option 3 above. It can be implemented by a class
// (class, inner class, inline class, etc.), or Java 8 Lambda.
public interface MotionUpdater
{
Motion update(Motion oldMotion);
}
Facendo questo refactoring (classe extract), ora la classe Motion
ha solo due campi, e quindi si qualifica per il trattamento di immutabilità, cioè la clonazione di una Motion
per modificare i suoi valori implica solo copiare due riferimenti a Vec2
immutabili.
Per riassumere, gli oggetti immutabili sono adatti quando:
- Quando è necessario dalla logica dell'applicazione (richiede record permanenti di oggetti, occorrono copie congelate di oggetti condivisi da più thread, ecc.) o ...
- Quando gli oggetti sono molto piccoli (2-4 campi) e non fanno parte di alcuna catena ereditaria. Nota che le implementazioni dell'interfaccia sono ancora consentite.