Kompositsioon ehk "HAS-A" seos
Sissejuhatus
Pärilikkuse peatükis tutvusime "IS-A" seosega.
Pärilikkus on õige metoodika siis, kui üks klass on tõepoolest teise alamtüüp: Car on Vehicle, Truck on Vehicle.
Mõlema näite puhul antud seos kehtib.
Kuid iga klasside vaheline seost ei saa IS-A seosega õigustada. See kehtib juhul, kui üks klass sisaldab või kasutab teist klassi, mitte ei laienda otseselt. Sellist seost nimetatakse HAS-A seoseks ning seda rakendatakse kompositsiooni kaudu.
Pärilikkuse halb kasutus
Meenutame halba näidet pärilikkuse peatükist: ParkingLot IS-A Vehicle.
Parkla ei ole sõiduk, see hoiab sõidukeid.
Parkla pärilikkuse kaudu modelleerimise tulemusena tekib klass, mis pärib välju ja meetodeid (brand, speed, drive()), mis parkla puhul ei ole loogilised.
// Wrong — ParkingLot is not a Vehicle
class ParkingLot extends Vehicle {
private int capacity;
// inherits brand, speed, drive() — none of which make sense here
}
Õige metoodika antud juhul oleks kompositsioon: ParkingLot HAS-A Vehicle ehk parkla on kogumik, mis hoiab endas sõidukeid.
Kompositsioon
Kompositsiooni puhul hoitakse klassis viidet teisele klassile:
class Engine {
private int horsepower;
public Engine(int horsepower) {
this.horsepower = horsepower;
}
public void start() {
System.out.println("Engine started (" + horsepower + " hp)");
}
}
class Car {
private String brand;
private Engine engine; // Car HAS-A Engine
public Car(String brand, int horsepower) {
this.brand = brand;
this.engine = new Engine(horsepower);
}
public void start() {
System.out.println(brand + " is starting");
engine.start();
}
}
Car car = new Car("Toyota", 150);
car.start();
// Toyota is starting
// Engine started (150 hp)
Car ei laienda klassi Engine, sest auto ei ole mootor.
Selle asemel sisaldab Car mootorit ja kasutab selle funktsionaalsust enda käitumise osana.
Sellist lähenemist nimetatakse kompositsiooniks: klass ei päri teiselt klassilt käitumist, vaid koosneb teistest objektidest ning delegeerib neile vastutuse.
Kui Car käivitatakse, kutsub see välja oma Engine objekti start() meetodi, selle asemel et ise mootori käivitamise loogikat implementeerida.
Kompositsioon vs. agregatsioon
Nii kompositsioon kui ka agregatsioon kirjeldavad HAS-A seost, kus üks objekt sisaldab teist objekti. Nende peamine erinevus seisneb omandisuhtes ja elutsüklis ehk kas sisaldatav objekt eksisteerib iseseisvalt või mitte.
Kompositsiooni puhul on sisaldatav objekt tugevalt seotud teda sisaldava objektiga. Tavaliselt luuakse see sisaldava klassi sees ning selle elutsükkel on otseselt seotud sisaldava objektiga. Kui sisaldav objekt hävib, hävib koos sellega ka sisaldatav objekt.
class Car {
private Engine engine; // Engine is created inside Car and lives with it
public Car(int horsepower) {
this.engine = new Engine(horsepower); // Car owns the Engine
}
}
Agregatsiooni puhul eksisteerib sisaldatav objekt iseseisvalt ning seda võib kasutada mitme erineva objekti poolt korraga. Seda ei looda sisaldava klassi sees, vaid see antakse tavaliselt väljastpoolt sisendina objekti loomise ajal.
class Driver {
private String name;
public Driver(String name) {
this.name = name;
}
}
class Car {
private Driver driver; // Driver is passed in from outside and exists independently
public Car(Driver driver) {
this.driver = driver;
}
}
Driver alice = new Driver("Alice");
Car car1 = new Car(alice); // Alice drives car1
Car car2 = new Car(alice); // Alice also drives car2 — she exists independently
Kompositsioon väljendab tugevat omandisuhet, kus sisaldatav objekt on osa tervikust. Agregatsioon väljendab nõrgemat seost, kus sisaldatav objekt on iseseisev ja võib olla jagatud mitme objekti vahel.
Praktikas on vahe kõige olulisem omandisuhte ja elutsükli mõistmiseks. Javas näevad mõlemad koodis ühesugused välja, erinevus seisneb selles, kuidas objekt luuakse ja kes selle eest vastutab.
ParkingLot näite parandamine
Kompositsiooni kasutades muutub ParkingLot klassiks, mis hoiab sõidukeid, selle asemel et teha nägu, et see on sõiduk:
class ParkingLot {
private String address;
private List<Vehicle> vehicles = new ArrayList<>();
public ParkingLot(String address) {
this.address = address;
}
public void park(Vehicle vehicle) {
vehicles.add(vehicle);
System.out.println(vehicle.getBrand() + " parked at " + address);
}
public int getOccupancy() {
return vehicles.size();
}
}
ParkingLot lot = new ParkingLot("Akadeemia tee 15");
lot.park(new Car("Toyota", 120));
lot.park(new Truck("Volvo", 90, 20.0));
System.out.println(lot.getOccupancy()); // 2
Kompositsioon või pärilikkus?
Praktikas levinud arusaam on, et reeglina tuleks pärilikkuse asemel eelistada kompositsiooni.
Pärilikkus loob klasside vahel tugeva seose. Alamklass sõltub otseselt ülemklassi implementatsioonist, mistõttu ülemklassi muutmine võib mõjutada kõiki selle alamklasse. See muudab süsteemi jäigemaks ja raskemini muudetavaks.
Kompositsioon on paindlikum, sest klass kasutab teisi objekte nende liidese kaudu, mitte ei sõltu nende sisemisest ehitusest. See võimaldab komponente hõlpsamini asendada või muuta, ilma et seda kasutav klass peaks muutuma.
Kasuta pärilikkust siis, kui eksisteerib selge IS-A seos ja alamklass on tõepoolest ülemklassi spetsialiseeritud vorm.
Kasuta kompositsiooni siis, kui klass lihtsalt kasutab teise klassi funktsionaalsust või sisaldab seda osana oma käitumisest.
| Pärilikkus (IS-A) | Kompositsioon (HAS-A) | |
|---|---|---|
| Seose tähendus | alamklass on ülemklassi tüüp | klass sisaldab teist objekti |
| Realiseerimine | extends | välja kaudu viitamine |
| Sidestus | tugev | nõrgem |
| Taaskasutamine | pärimise kaudu | delegeerimise kaudu |
| Paindlikkus | väiksem | suurem |
| Mitme tüübi kasutamine | Java ei toeta mitut ülemklassi | võib sisaldada mitut erinevat objekti |