Niemutowalność cz.2

Ostatnio dowiedzieliście się czym jest niemutowalność! W tym wpisie przytoczę obiecany przykład mutowania poprzez metodę klasy Cargo. Następnie przejdziemy do przykładów, które pozwolą nam zapobiec mutowaniu i łamaniu zasad enkapsulacji. Zapobiegniemy wyciekającym referencjom….. !

Niemutowalność a metody

Ostatnim elementem klasy, który może być definiowany, jako final jest metoda. Użycie final przy metodzie działa podobnie jak użycie go dla klasy. Metoda nie może wtedy być nadpisana w subklasie. Oracle podaje, że tak na dobrą sprawę każda metoda wywoływana bezpośrednio z konstruktora powinna być finalna, tak aby nie można było jej nadpisać w subklasie.

Przykład 🚊 🚊 🚊

Zerknijmy na przykład gdzie klasa Cargo posiada metodę calculateCapacity(), a subklasa FakeCargo ją nadpisuje (overriding). 

public class Cargo {

   private String id;
   private Owner owner;
   private CargoType type;
   private int capacity;

  //konstruktor

   public int getCapacity() {
       return capacity;
   }

   public void calculateCapacity(int currentLoad) {
       this.capacity = this.capacity - currentLoad;
   }

   public class FakeCargo extends Cargo {
       public FakeCargo() {
           super(id, owner, type, capacity);
       }
 

       public void calculateCapacity() {
           capacity = 0;
       }
   }

Wywołanie następującego programu pokazuje, że jesteśmy w stanie mutować pola obiektu Cargo poprzez nadpisanie metody mutującej w subklasie. 

public class Main {

   public static void main(String[] args) {
       Owner owner = new Owner("John", 20);
       Cargo cargo = new Cargo("12", owner, CargoType.BIG, 20);
       Cargo.FakeCargo fakeCargo = cargo.new FakeCargo();
       System.out.println(cargo.getCapacity()); // capacity 20
       cargo.calculateCapacity(5);
       System.out.println(cargo.getCapacity()); //capacity 15
       fakeCargo.calculateCapacity();
       System.out.println(cargo.getCapacity()); //cargo capacity 0 zmutowane metoda z klasu FakeCargo
   }
}

Jeżeli do metody calculateCapacity() w klasie Cargo dodamy klucz final  wtedy nie będziemy mogli podpisać tej metody w ciele subklasy, a tym samym uchronimy się od możliwości zmiany stanu z jej poziomu – zaaplikujemy niemutowalność.

Wyciekające referencje 💦 💦

Jak wiadomo w Javie wykorzystane jest przekazywanie parametrów przez wartość (a nie przez referencje) (tutaj więcej na temat tego procesu).

W przypadku odwołań do przekazywania obiektów tak naprawdę przekazujemy wartość, która jest referencją (tutaj często zachodzi pomyłka tych, którzy widzą w tym przekazywanie przez referencję).

Przykład

Weźmy pod lupę następujący przykład:

public class Cargo {
   private String id;
   private Owner owner;
   private CargoType type;
   private int capacity;
   private List<CargoDestination> destinationList;

   public Cargo(String id, Ownership ownership, CargoType type, int capacity, List<CargoDestination> destinationList) {
       this.id = id;
       this.owner = owner;
       this.type = type;
       this.capacity = capacity;
       this.destinationList = destinationList;
   }

   public List<CargoDestination> getDestinationList() {
       return destinationList;
   }
}

Wszystko wygląda porządnie. Niestety tak nie jest!
Zerknijmy co można zrobić wywołując metodę getDestinationList()

public class Main {
   public static void main(String[] args) {
       Ownership ownership = new Ownership("John");
       CargoDestination plWarszawa = new CargoDestination("Warszawa", "521356N", "210030E");
       CargoDestination plPoznan = new CargoDestination("Poznan", "5224530N", "165603E");
       List<CargoDestination> destinations = new ArrayList<>();
       destinations.add(plWarszawa);
       destinations.add(plPoznan);
       Cargo cargo = new Cargo("12345", ownership, CargoType.BIG, 500, destinations);
      
       System.out.println(cargo.getDestinationList().size());
       cargo.getDestinationList().forEach(dest -> System.out.println(dest.getName()));
      
       List<CargoDestination> myCargoDestinations = cargo.getDestinationList();
       myCargoDestinations.clear();  /// usuwam wszystkie dane z listy destinations myCargoDestinations
      
       cargo.getDestinationList().forEach(dest -> System.out.println(dest.getName()));
       System.out.println(cargo.getDestinationList().size()); // dane z Cargo  z pola destinations tez zostaly usuniete :O
   }
}

Totalna mutacja stanu i to nie przez setter, a przez metody dostępne dla interfejsu kolekcji.

Czym uratować niemutowalność? 🛠

Na szczęście w takim przypadku gdzie referencja uciekła nam na zewnątrz klasy w której była enkapsulowana możemy ratować się w następujący sposób.
Należy udostępnić kopie danych, a nie poprzez referencje!!
Zamiast metody:

public List<CargoDestination> getDestinationList() {
       return destinationList;
   }

Powinniśmy użyć np:

public List<CargoDestination> getDestinationList() {
   return Collections.unmodifiableList(destinationList);
}

Po ponownym uruchomieniu przykładu dostaniemy soczystą informację w konsoli:

Exception in thread "main" java.lang.UnsupportedOperationException
	at java.base/java.util.Collections$UnmodifiableCollection.clear(Collections.java:1079)
	at Main.main(Main.java:20)

Obiekty w kolekcjach

Zauważmy, że lista punktów podróży Cargo zawiera obiekty.
Jeżeli obiekt CargoDestination posiada metody mutujące stan to można dobrać się do poprzez gettery. 😱

Przykład

Dodajmy do klasy Cargo metodę:

…
public CargoDestination findDestinationByIndex(int index) {
   return destinationList.get(index);
}
...

a do klasy uruchomieniowej Main:

...
cargo.getDestinationList().forEach(dest -> System.out.println(dest.getName()));
CargoDestination firstDestination = cargo.findDestinationByIndex(1);
firstDestination.setName("Rzeszow");
cargo.getDestinationList().forEach(dest -> System.out.println(dest.getName()));
….

Udało się nadpisać wartości klasy CargoDestination poprzez pobranie setterem listy kierunków docelowych Cargo!!!

Interfejs na ratunek

Na ratunek w tym przypadku przychodzi interfejs. Wystarczy, że utworzymy interfejs Destination, który definiuje metodę getName() jako wymaganą do implementacji przez klasę. Tym samym, jeżeli obiekt CargoDestination zaimplementuje tę metodę to będziemy mogli zwracać też typ Interfejsu! (pisałem co nie co o tym, we wpisie o interfejsach)

A jak to wygląda? Mamy więc interfejs:

public interface Destination {
   String getName();
}

W metodzie getName() klasy CargoDestination musimy dodać adnotacje @Override

@Override
public String getName() {
   return name;
}

W klasie Main:

cargo.getDestinationList().forEach(dest -> System.out.println(dest.getName()));
Destination firstDestination = cargo.findDestinationByIndex(1);// zwracamy typ interfejsu
firstDestination.setName("Rzeszow"); /// nie da sie tego uzyc
cargo.getDestinationList().forEach(dest -> System.out.println(dest.getName()));

I tak się nie uda! 🙃

Jeżeli myślisz, że damy  rade utrzymać niemutowalne w ryzach, to niestety musze Cię zmartwić. W Javie istnieje jeszcze mechanizm refleksji, który za nic sobie ma wszelkie zabiegi prowadzące do zachowania stanu obiektów. Nie jest to jednak łatwe, a naszym zadaniem jest zrobienie wszystkiego, co możemy aby zabezpieczyć aplikacje!

Do następnego wpisu! 👋

Dodaj komentarz

Twój adres email nie zostanie opublikowany. Pola, których wypełnienie jest wymagane, są oznaczone symbolem *