Недеља 4 — Односи између објеката и композиција
Код: GitHub · преузми пројекат (ZIP) · задатак за самостални рад
До сада смо углавном посматрали један објекат изоловано. У реалним програмима објекти скоро увек сарађују са другим објектима.
Ове недеље користимо поједностављен систем за поручивање хране. Домен је намерно једноставан: купац има адресу, поруџбина има ставке, а свака ставка се односи на производ. Фокус није на прављењу апликације за доставу, већ на питању:
Како расподелити податке и одговорности између више објеката?
Циљеви
После вежби студент треба да уме да објасни:
- да поље објекта може да буде другог класног типа;
- значење односа HAS-A;
- шта представља граф објеката;
- разлику између објекта и референце на објекат;
- да више објеката може да дели исти објекат;
- основну идеју композиције;
- шта значи делегирање одговорности;
- зашто класа не треба да зна детаље који припадају другом објекту;
- како мутабилност и дељење референце могу да направе неочекиване последице;
- зашто су мали непроменљиви објекти често једноставнији за безбедно дељење.
Важна напомена
Ове недеље не уводимо наслеђивање. Желимо да студенти прво стекну природан осећај да се сложен објекат веома често гради од других објеката.
Касније ћемо моћи прецизно да упоредимо:
IS-A— наслеђивање;HAS-A— композиција.
Редослед примера
primer01_objekat_kao_polje
Kupac има Adresu.
Први пут експлицитно посматрамо један објекат као део стања другог објекта. Дискутујемо шта се заправо чува у пољу типа Adresa: објекат или референца на објекат.
primer02_porudzbina_i_stavke
Градимо мали граф објеката:
Porudzbina
|
+-- Kupac
| |
| +-- Adresa
|
+-- StavkaPorudzbine[]
|
+-- Proizvod
Пошто колекције још нису обрађене, користимо низ фиксног капацитета. Тема примера није ArrayList, већ односи између објеката.
primer03_delegiranje
Питање није само које податке класа има, већ и ко је одговоран за коју операцију.
Porudzbina не рачуна цену појединачне ставке тако што завирује у сва њена поља. Она пита ставку:
ukupno += stavka.izracunajCenu();
То је једноставан пример делегирања одговорности.
primer04_deljena_promenljiva_adresa
Намерно правимо проблем. Kupac и Porudzbina деле исти променљиви објекат Adresa.
Ако купац промени адресу, може неочекивано да се промени и адреса већ направљене поруџбине.
Пример повезује градиво ове недеље са aliasing-ом из претходне недеље.
primer05_nepromenljiva_adresa
Adresa постаје мали непроменљиви објекат: сва поља су final, нема settera.
Ако купац промени своју тренутну адресу, добија нову Adresa референцу. Раније направљена поруџбина задржава стару адресу.
Ово није правило да сваки објекат треба да буде immutable. Циљ је да студент види да је мутабилност дизајнерска одлука, а не подразумевано стање сваке класе.
primer06_record_adresa
После класичне непроменљиве Adresa класе показујемо савремени Java тип:
public record Adresa(
String ulica,
String grad,
String postanskiBroj
) {
}
record је и даље класа, али је намењен пре свега малим објектима чији је главни посао да носе скуп података као једну вредност.
Java аутоматски генерише:
konstruktor
accessor metode
equals
hashCode
toString
Accessor методе немају класичан get префикс:
adresa.grad()
уместо:
adresa.getGrad()
Record компоненте не могу да се мењају након креирања објекта.
Зато је Adresa природан пример:
Adresa("Glavna 10", "Zemun", "11080")
представља једну вредност.
Ако се купац пресели, не мењамо стару адресу поље по поље. Направимо нову вредност:
kupac.preseliSe(
new Adresa("Nova 5", "Beograd", "11000")
);
Record није замена за сваку класу
Не треба претварати сваку класу у record.
На пример, Novcanik, Porudzbina или Vozilo имају променљиво стање и понашање које штити то стање. Обична класа је ту природнији модел.
Добро почетно питање је:
Да ли овај тип првенствено представља малу непроменљиву вредност?
Ако је одговор да, record може бити добар избор.
Record може да има валидацију и методе
У примеру користимо компактни конструктор:
public Adresa {
if (ulica == null || ulica.isBlank()) {
throw new IllegalArgumentException(...);
}
}
и обичну методу:
public String punaAdresa()
Дакле, record није само “struct са аутоматским getterima”.
Централне поруке
1. Сложен објекат се често гради од мањих објеката
Уместо једне огромне класе:
Porudzbina
- imeKupca
- prezimeKupca
- ulica
- broj
- grad
- nazivProizvoda1
- cenaProizvoda1
- ...
желимо објекте са јасним одговорностима:
Porudzbina HAS-A Kupac
Kupac HAS-A Adresa
Porudzbina HAS-A StavkaPorudzbine
StavkaPorudzbine HAS-A Proizvod
2. Објекат не мора све да ради сам
Ако StavkaPorudzbine зна производ и количину, она је природно место за рачунање цене те ставке.
Porudzbina само координира објекте које садржи.
3. Дељење објекта је важно
Две референце могу показивати на исти објекат. Зато треба размишљати:
Да ли желим да се промене овог објекта виде на свим местима која га користе?
Понекад је одговор да. Понекад није.
Питања за дискусију
- Да ли
KupacјестеAdresaили имаAdresu? - Шта се физички налази у пољу
private Adresa adresa? - Може ли исти објекат
Proizvodда се појави у више поруџбина? - Колико објеката постоји ако пет ставки показује на исти
Proizvod? - Ко треба да рачуна цену једне ставке —
PorudzbinaилиStavkaPorudzbine? - Зашто није добро да
Porudzbinaдиректно познаје унутрашњу структуру сваке ставке? - Шта се дешава када два објекта деле исти променљиви објекат
Adresa? - Да ли је непроменљив објекат аутоматски бољи? Када јесте користан?
- Ако купац промени адресу након што је поруџбина послата, да ли стара поруџбина треба да промени адресу испоруке?
- Који објекти у овом примеру имају јасан идентитет, а који више личе на вредности?
Мали задатак
У фајлу ZADATAK.md налази се задатак са биоскопском резервацијом. Домен је другачији, али студент треба сам да препозна HAS-A односе, одговорности и објекте који могу безбедно да буду непроменљиви.
Границе примера и рецорд
Овде реч композиција користимо у ширем смислу састављања објеката. UML композиција у ужем смислу подразумева и однос власништва/животног циклуса; није свака HAS-A веза аутоматски таква композиција.
record обезбеђује final компоненте, али не копира дубоко њихове објекте. Record са
List компонентом није аутоматски дубоко непроменљив. У примеру Адреса компоненте
су String вредности. Валидација компактног конструктора користи throw: одбија се
неисправно креирање; обраду изузетака систематично радимо у недељи 11.