Icke-muterbara objekt gör livet enklare

Genom att representera mer komplicerade datatyper med klasser vars attribut aldrig kan ändras är det lättare att hålla koden fri från märkliga buggar.

Alla som har programmerat Java eller någon annat objektorienterat språk är vana att skriva klasser med attribut och set- och get-metoder för de samma. Ibland innehåller de även större eller mindre mängder funktionalitet, men ibland är ett objekt verkligen bara en gruppering av attribut, och eventuellt lite kontroller av attributens värden, gjorda för att representera någon form av värde som är för komplext för att rymmas i någon av de vanliga inbyggda typerna eller klasserna. Låt oss skilja på objekt som innehåller funktionalitet och de som inte gör det genom att införa begreppen "aktiva" och "passiva" objekt/klasser. Gränsen mellan dessa är förstås väldigt flytande, men låt oss bara för sakens skulle definiera att ett aktivt objekt innehåller funktionalitet utöver det som krävs för att innehålla, sätta och läsa attribut, medan ett passivt objekt inte gör det i någon större utsträckning.

Låt oss ta ett exempel. En klass som representerar likformiga polygoner i planet. Den innehåller set- och get-metoder för attributen samt en metod som räknar ut polygonens area.

package com.westbahr;

public class RegularPolygon {
    
    private double x;
    private double y;
    private double radius;
    private int sideCount;
    private double startingAngle;
    
    public RegularPolygon(
            double x,
            double y,
            double radius,
            int sideCount,
            double startingAngle) {
        super();
        this.x = x;
        this.y = y;
        this.radius = radius;
        this.sideCount = sideCount;
        this.startingAngle = startingAngle;
    }
    
    public double getX() {
        return x;
    }
    
    public void setX(double x) {
        this.x = x;
    }
    
    public double getY() {
        return y;
    }
    
    public void setY(double y) {
        this.y = y;
    }
    
    public double getRadius() {
        return radius;
    }
    
    public void setRadius(double radius) {
        this.radius = radius;
    }
    
    public int getSideCount() {
        return sideCount;
    }
    
    public void setSideCount(int sideCount) {
        this.sideCount = sideCount;
    }
    
    public double getStartingAngle() {
        return startingAngle;
    }
    
    public void setStartingAngle(double startingAngle) {
        this.startingAngle = startingAngle;
    }
    
    public double getArea() {
        int sideCount = getSideCount();
        double theta = Math.PI / sideCount;
        return sideCount
                * Math.pow(getRadius(), 2)
                * Math.cos(theta)
                * Math.sin(theta);
    }
    
}

På sistone har jag mer och mer gått över till att göra sådana klasser icke-muterbara, dvs att deras värden sätts i konstruktorn och sedan aldrig ändras. Klassen saknar alltså set-metoder och innehåller enbart get-metoder. Motsvarande klass som tidigare, fast utan muterande metoder, skulle då bli:

package com.westbahr;

public class RegularPolygon {
    
    private final double x;
    private final double y;
    private final double radius;
    private final int sideCount;
    private final double startingAngle;
    
    public RegularPolygon(
            double x,
            double y,
            double radius,
            int sideCount,
            double startingAngle) {
        super();
        this.x = x;
        this.y = y;
        this.radius = radius;
        this.sideCount = sideCount;
        this.startingAngle = startingAngle;
    }
    
    public double getX() {
        return x;
    }
    
    public double getY() {
        return y;
    }
    
    public double getRadius() {
        return radius;
    }
    
    public int getSideCount() {
        return sideCount;
    }
    
    public double getStartingAngle() {
        return startingAngle;
    }
    
    public double getArea() {
        int sideCount = getSideCount();
        double theta = Math.PI / sideCount;
        return sideCount
                * Math.pow(getRadius(), 2)
                * Math.cos(theta)
                * Math.sin(theta);
    }
    
    @Override
    public int hashCode() {
        final int prime = 31;
        int result = 1;
        long temp;
        temp = Double.doubleToLongBits(radius);
        result = prime * result + (int) (temp ^ (temp >>> 32));
        result = prime * result + sideCount;
        temp = Double.doubleToLongBits(startingAngle);
        result = prime * result + (int) (temp ^ (temp >>> 32));
        temp = Double.doubleToLongBits(x);
        result = prime * result + (int) (temp ^ (temp >>> 32));
        temp = Double.doubleToLongBits(y);
        result = prime * result + (int) (temp ^ (temp >>> 32));
        return result;
    }
    
    @Override
    public boolean equals(Object obj) {
        if (this == obj)
            return true;
        if (obj == null)
            return false;
        if (getClass() != obj.getClass())
            return false;
        RegularPolygon other = (RegularPolygon) obj;
        if (Double.doubleToLongBits(radius) != Double
                .doubleToLongBits(other.radius))
            return false;
        if (sideCount != other.sideCount)
            return false;
        if (Double.doubleToLongBits(startingAngle) != Double
                .doubleToLongBits(other.startingAngle))
            return false;
        if (Double.doubleToLongBits(x) != Double
                .doubleToLongBits(other.x))
            return false;
        if (Double.doubleToLongBits(y) != Double
                .doubleToLongBits(other.y))
            return false;
        return true;
    }
    
}

Inget konstigt alls. Jag har bara tagit bort set-metoderna, samt satt final-modifieraren på instansvariablerna. Och det senare är inte ens nödvändigt för att göra klassen icke-muterbar, jag bara tycker att det är en bra praxis att alltid sätta final på variabler när det går, då det är ett bra sätt att undvika fel.

Jag passade också på att lägga till en equals-metod och en hashCode-metod, som man kan få genererade automatiskt av Eclipse och andra liknande verktyg.

Vad är då skillnaderna mellan att göra klassen muterbar eller inte muterbar och varför är det ena en fördel gentemot det andra?

Det vi noterar är det uppenbara faktumet att objekt av den andra klassen aldrig någonsin kan ändras när de väl har skapats, vilket är en trivial observation. Det som inte är lika uppenbart, men icke desto mindre viktigt, är att detta innebär att när vi har en referens till ett RegularPolygon-objekt spelar det inte någon roll om vi har exakt samma objekt eller en kopia! Detta är en viktig observation, för den innebär att vi inte alls på samma sätt behöver vara noggranna med hur vi hanterar våra objekt som vi måste om de kan muteras. Om objekt kan muteras och vi ändrar någon parameter i ett objekt måste vi vara säkra på att inte samma objekt ligger refererat någon annanstans, där ändringen kan leda till icke-förväntade resultat.

Om objekten däremot är icke-muterbara måste vi byta ut hela objektet, och kan då garantera att ändringen inte påverkar någon annan del av programmet.

Om bloggaren

Marcus Björkander

Marcus Björkander

Schlagernörd och småbarnspappa med tvångstankar som jobbar som utvecklare på Westbahr i Göteborg. Favoritspråket är Java, som han tidigare har undervisat i vid Chalmers i många år.

 

Nyckelord/tag moln