Korduma kippuvad vead OOP ülesannetes
Kui teie kood töötab, siis see on hea. Kuid meie eesmärk on õpetada teid kirjutama mitte ainult töötavat, vaid ka ilusat ja puhast koodi, mida on lihtsam hiljem parandada ja täiendada. Siin on toodud kõige tihedamad vead, mida tehakse vaba OOP ülesannetes.
Klassid ei ole jagatud pakettideks
Esineme asi, mida me näeme, kui teeme teie ülesande lahti, on see, kuidas teie projekt on struktueeritud. Üldiselt on paketid mõeldud selleks, et vältida nimede kokkupõrget (name collision), aga neid kasutatakse ka selleks, et jagada projekti plokkideks. Teie klassid peavad olema loogiliselt pakettideks jagatud. Kindlasti ei tasu iga klassi jaoks luua eraldi paketti. Näiteks Room
ja RoomType
saavad küll olla ühes paketis nimega room
. Aga kui te panete klasse Room
ja OrderProcessor
ühte paketti, siis see on juba kahtlane.
Sellest tasub mõelda juba enne koodi kirjutamist, sest järgmine viga on pakettidega tihedalt seotud.
Nähtavused
Väljade nähtavused peavad olema võimalikult madalad.
OOP disaini järgi peavad klassid teineteisest teadma võimalikult vähe. Üks oluline printsiip OOP disainis on kapseldamine (encapsulation
), mille järgi on kõikidel väljadel nähtavus private
ning nendele saab ligi läbi getteri/setteri. Kõik väljad ei pea tingimata olema private
. Kui teil on vaja muuta välja väärtust alamklassis, siis võib panna väljale nähtavus protected
või package-private
(kui ülem- ja alamklassid on ühes paketis). Aga ärge pange kunagi väljadele nähtavust public
! public
'ud saavad olla ainult static final
muutujad ja selle tingimusega, et te kasutate neid teistes klassides.
IntelliJ analüüsib teie koodi ja kui väljal/meetodil on liiga kõrge nähtavus, siis ta kohe värvib seda kollaseks ning ütleb, et nähtavus võiks olla madalam.
Nimed
Meetodite/muutujate/klasside nimetamine on ka oluline asi. Nimed peavad olema võimalikult selged, et teised inimesed (kes ei pruugi olla arendajad) saaks ka nendest aru. Ärge kasutage lühendeid, kui need pole tuntud.
Head muutujate nimed: price
, priceInDollars
, currentValue
Halvad muutujate nimed: p
, pricedollars
, cur
Nimetamise konventsioon: Koodistiil
Üldised reeglid:
Paketi nimi: ainult väikesed tähed lubatud. Kui sisaldab mitu sõna, siis neid kirjutatakse kokku.
Näited: student
, taltech
, superhero
, studyprogramme
Klassi nimi: peab olema nimisõna. Tuleb vältida üldiseid nimesid nagu "Processor", "Parser", "Converter" jms.
Näited: Account
, RoomPriceCalculator
, ComplexNumber
, EmployeeDataProcessor
.
Meetodi nimi: peab sisaldama tegusõna.
Näited: save
, delete
, getCurrentPrice
, findLastRecord
, isActive
.
Liideste nimed: peab olema kas nimisõna (kui kasutate liidest nagu ülemtüüpi) või omadussõna.
Näited: Comparable
, Fixable
, Drawable
või Engine
, CoffeeMachine
, StudyProgramm
.
Liidese nimi peab olema abstraktsem kui selle liidese implementatsioonil:
Liidese nimi: CoffeeMachine
-> Implementatsioonide nimed: AutomaticCoffeeMachine
, StandardCoffeeMachine
Erindi (exception) nimi: nimi järgi peab olema lihtne aru saada, mis probleemiga on tegu. Lõpus peab olema sõna
Exception
.
Näited: CannotMakeCoffeeException
, NoMoreActionsAllowedException
, GotIncorrectOutputFromAPIException
Testi nimi: peab olema selline, et kui see failib, siis kohe saab aru miks.
Halvad nimed: testGetAge
, createPerson
, testBuyItem
Paremad nimed: testThrowsExceptionIfPersonIsNot18YearsOld
, testTicketIsFreeIfClientIsAChild
, testNoMoreActionsIfMoneyIsGone
, testMoneyIsTakenWhenItemIsBought
Siin (https://dzone.com/articles/7-popular-unit-test-naming) on välja toodud mõned nimetamise konventsioonid. Valige see, mis teile kõige rohkem meeldib ja kasutage seda igalpool.
Tüübid klassidena vs enumiga
Kui ülesandes on antud realiseerida mõnda objekti alamtüüpe ja mõnel alamtüübil on lisaomadus, mida teistel pole, siis realiseerige need klassidena.
Näide: Hotellis on kahte tüüpi toad: tavaline tuba ja ärituba. Äritoal on punane nupp, millega saab personaali tuppa kutsuda.
Halb:
public class HotelRoom {
private HotelType type;
...
public void callStaff() {
if (type == BUSINESS_ROOM) {
...
}
}
}
Parem näide:
public class HotelRoom {
...
}
public class BusinessRoom extends HotelRoom {
public void callStaff() {
...
}
}
Alamtüübid peavad olema realiseeritud niimoodi, et uue tüübi lisamiseks poleks vaja vana koodi ümber kirjutada.
Enum'it saab kasutada siis, kui tüübist ei sõltu olemasolev funktsionaalsus ning ei ole vaja uut funktsionaalsust lisada.
Näide: Seadme kohta peab olema võimalik teada saada tema tüüpi.
public class Device {
private DeviceType deviceType;
}
public enum DeviceType {
SMARTPHONE, LAPTOP, TABLET;
}
Kui teie teete enumiga ja näete oma koodis sellist asja:
public class HotelRoom {
private int roomSize;
private RoomType roomType;
private boolean hasAdditionalBed; // only for luxury room
public int getPrice() {
if (roomType == BUSINESS) {
price = 0.8 * roomSize;
} else if (roomType == LUXURY) {
price = 0.9 * roomSize + (hasAdditionalBed ? 10 : 0);
} else {
price = roomSize;
}
}
}
Siis te ilmselt teete midagi valesti.
Parem lahendus:
public abstract class HotelRoom {
int roomSize;
public abstract int getPrice();
}
public class StandardRoom extends HotelRoom {
@Override
public int getPrice() {
return roomSize;
}
}
public class BusinessRoom extends HotelRoom {
@Override
public int getPrice() {
return 0.8 * roomSize;
}
}
public class LuxuryRoom extends HotelRoom {
private boolean hasAdditionalBed;
@Override
public int getPrice() {
return 0.9 * roomSize + (hasAdditionalBed ? 10 : 0);
}
}
Integer vs int, Float vs float, Boolean vs boolean jne
Igal primitiivsel tüübil Javas on olemas oma analoog klassina:
int -> Integer
double -> Double
float -> Float
boolean -> Boolean
char -> Character
Kui teil on valik, kas kasutada primitiivset andmetüüpi või selle klassi, siis väga suure tõenääosusega peate kasutama ikkagi primitiivset andmetüüpi.
Kui kasutate klasse primitiivse tüübi asemel, siis peate silmas pidama:
Objekt võib olla
null
.Objekte ei soovitata võrrelda == operaatoriga.
Need klassid on põhimõtteliselt wrapper'id:
public class Integer {
private int value;
...
}
Ainuke koht, kus saab kasutada ainult primitiivsete tüüpide klasse on Generic'ud. Näiteks listid, mapid, optionalid jms. Te ei saa kirjutada nt List<int>
ja peate kirjutama List<Integer>
.
Implementatsiooni kasutamine liidese asemel tüübina
Klass peab olema disainitud niimoodi, et teised klassid teaks nii vähe kui võimalik sellest, kuidas see klass sisemiselt töötab. (abstraheerimine) Kui te valite välja või meetodi tüübiks liidese implementatsiooni liidese asemel, siis te rikute seda reeglit. Lisaks tekib teil probleeme, kui te hiljem otsustate implementatsioon vahetada teise vastu. Teiste sõnadega annab liidese kasutamine tüübina teile rohkem vabadust.
Halb:
public class Student {
private ArrayList<Grade> grades = new ArrayList<>();
private HashMap<String, Integer> grants = new HashMap<>();
public ArrayList<Grade> getGrades() {
return grades;
}
public HashMap<String, Integer> getGrants() {
return grants;
}
}
Parem:
public class Student {
private List<Grade> grades = new ArrayList<>();
private Map<String, Integer> grants = new HashMap<>();
public List<Grade> getGrades() {
return grades;
}
public Map<String, Integer> getGrants() {
return grants;
}
}
Erand: Implementatsioon sobib välja tüübiks, kui te kasutate selle implementatsiooni spetsiifilisi meetodeid. Getteri tüübiks jätke pigem liides.
public class Student {
private ArrayList<Grade> grades = new ArrayList<>();
public void doSmartThings() {
...
// List<...> does not have ensureCapacity method. Only ArrayList<...> does.
// If grades list type was just List<Grade>, then you would need to cast
// grades to ArrayList<Grade> to call this method.
grades.ensureCapacity(...);
...
}
public List<Grade> getGrades() {
return grades;
}
}