-
Notifications
You must be signed in to change notification settings - Fork 1
Palkanlaskenta malli
Luokkakaavio:
Sekvenssikaavio:
Kaavioon on nyt merkattu myös joitain Palkanlaskennan ja Henkilön sisäisiin ArrayListeihin kohdistuvia operaatioita.
Sekä Henkilo
että Palkanlaskenta
rikkovat single responsibility -periaatetta.
Henkilön vastuulla on nyt ainakin 2 asiaa:
- tuntea henkilöön liittyvät 'perustiedot' kuten nimi ja osoite
- laskea palkka ja pitää kirjaa henkilön tekemistä työtunneista joista ei ole maksettu palkkaa
Palkanlaskennalla on vastuita vieläkin enemmän
- kirjata työtunteja henkilöille ja pyytää työntekijöitä laskemaan palkkansa
- muotoilla metodien
tyontekijat
jamaksuhistoria
palauttamat tulokset CSV-muototoon - suorittaa palkanmaksu verkkorajapinnan avulla (luokalta
MaksupalveluRajapinta
peritty toiminnallisuus)
Favor composition over inheritance rikkoutuu myös ikävästi, sillä Palkanlaskenta perii Maksupalvelurajapinnan saadakseen sillä olevan tilisiirron tekemiseen liittyvän toiminnallisuuden. Tämä ei todellakaan ole järkevää perinnän hyödyntämistä!
Perintä aiheuttaa myös ikävän ja täysin tarpeettoman konkreettisen riippuvuuden Palkanlaskennan ja Maksupalvelurajapinnan välille.
Koodihajuista mainittakoon ainakin seuraavat:
- Primitive obsession henkilön osoite on talletettu merkkijonoksi, myös rahamäärän tallettaminen int-arvona on huono tapa. Edelliset olisi parempi hoitaa omana luokkanaan.
- Long method ainakin muutama Palkanlaskennan metodeista on aivan liian pitkä
- Duplicate code tuloksen muotoilu CSV:nä tapahtuu lähes samalla tavalla kahdessa eri palkanlaskennan metodissa
- Divergent change tuloksen muotoilu CSV:nä edellyttäisi muutoksia moneen paikkaan luokan Palkanlaskenta koodia, vastaavasti metodin uusiTuntipalkka toteuttaminen vaatisi muutoksia moneen kohtaan Henkilo luokkaa
Eriytetään Henkilöltä vastuu palkkaan ja työtunteihin liittyvistä asioista omaan luokkaansa Palkkaus
:
public class Palkkaus {
private int tuntipalkka;
private List<Tuntikirjaus> maksamattomat;
public Palkkaus(int tuntipalkka) {
this.tuntipalkka = tuntipalkka;
this.maksamattomat = new ArrayList<>();
}
public int getTuntipalkka() {
return tuntipalkka;
}
public void setTuntipalkka(int tuntipalkka) {
this.tuntipalkka = tuntipalkka;
}
public void lisaaTunnit(int tunnit) {
maksamattomat.add(new Tuntikirjaus(tunnit, tuntipalkka));
}
public int laskeMaksettavaPalkka(){
int palkkaYht = 0;
for (Tuntikirjaus kirjaus : maksamattomat) {
palkkaYht += kirjaus.getPalkka();
}
maksamattomat = new ArrayList<>();
return palkkaYht;
}
}
Palkkaus tallentaa jokaisen tuntikirjauksen luokkan Tuntikirjaus
olioksi. Olio tietää myös tuntikirjauksen hetkellä voimassa olleen tuntipalkan:
public class Tuntikirjaus {
private int tunnit;
private int tuntipalkka;
public Tuntikirjaus(int tunnit, int tuntipalkka) {
this.tunnit = tunnit;
this.tuntipalkka = tuntipalkka;
}
public int getPalkka() {
return tuntipalkka*tunnit;
}
}
Henkilö yksinkertaistuu seuraavaan muotoon:
public class Henkilo {
private int tunnus;
private String nimi;
private String osoite;
private String email;
private String puhnro;
private String tili;
private Palkkaus palkkaus;
public Henkilo(int tunnus, String nimi, String tili, String osoite, String email, String puhnro, int tuntipalkka) {
this.tunnus = tunnus;
this.nimi = nimi;
this.osoite = osoite;
this.email = email;
this.puhnro = puhnro;
this.tili = tili;
this.palkkaus = new Palkkaus(tuntipalkka);
}
public int getTunnus() {
return tunnus;
}
public String getNimi() {
return nimi;
}
public String getTili() {
return tili;
}
public int getTuntipalkka() {
return palkkaus.getTuntipalkka();
}
public int maksettavaPalkka(){
return palkkaus.laskeMaksettavaPalkka();
}
public void setTuntipalkka(int palkka) {
this.palkkaus.setTuntipalkka(palkka);
}
public void lisaaTunnit(int tunnit) {
palkkaus.lisaaTunnit(tunnit);
}
}
Jokaiseen henkilöön siis liittyy luokan Palkkaus
olio, jolle henkilö delegoi tekemiensä työtuntien kirjanpitoon sekä palkanmaksuun liittyvät toimenpiteet.
Palkanlaskennassa on kaksi muutosta. Luokka ei enää peri maksupalvelurajapintaa, vaan tuntee rajapinnan (eli noudattaa periaatetta favour composition over inheritance), jolle se delegoi tilisiirtojen tekemisen. Palkanlaskenta saa maksupalveluolion konstruktorin parametrina. Tämä mahdollistaa sen, että palkanlaskennalle annetaan testeissä "valemaksupalvelu", joka ei tee todellisia tilisiirtoja.
Toisena muutoksena on CSV-muotoilun eriyttäminen omaan luokkaan CsvBuilder
. Tulevia laajennuksia varten on määritelty myös rajapinta OutputBuilder
jonka CsvBuilder
toteuttaa. Tämän ansiosta Palkanlaskennalle on tulevaisuudessa helppo lisätä esim. JSON-tuki määrittelemällä uusi luokka JsonBuilder
. Muotoilusta vastaavat luokat annetaan Palkanlaskennalle konstruktorin parametrina
Map
-olioon talletettuna.
public class Palkanlaskenta {
private List<Henkilo> tyontekijat;
private List<Maksusuoritus> maksut;
private MaksupalveluRajapinta pankki;
private Map<String, OutputBuilder> builders;
public Palkanlaskenta(MaksupalveluRajapinta pankki, Map<String, OutputBuilder> builders) {
this.tyontekijat = new ArrayList<>();
this.maksut = new ArrayList<>();
this.pankki = pankki;
this.builders = builders;
}
public boolean lisaaTyontekija(int tunnus, String nimi, String tilinro, String osoite, String email, String puhnro, int tuntipalkka){
Henkilo henkilo = haeHenkilo(tunnus);
if (henkilo!=null) {
return false;
}
tyontekijat.add(new Henkilo(tunnus, nimi, tilinro, osoite, email, puhnro, tuntipalkka));
return true;
}
public boolean lisaaTunnit(int tunnus, int tunnit) {
Henkilo henkilo = haeHenkilo(tunnus);
if (henkilo==null) {
return false;
}
henkilo.lisaaTunnit(tunnit);
return true;
}
public boolean uusiTuntipalkka(int tunnus, int tuntipalkka) {
Henkilo henkilo = haeHenkilo(tunnus);
if (henkilo==null) {
return false;
}
henkilo.setTuntipalkka(tuntipalkka);
return true;
}
public void maksaPalkat(){
for (Henkilo henkilo : tyontekijat) {
Maksusuoritus maksu = new Maksusuoritus(
henkilo.getNimi(),
henkilo.getTili(),
henkilo.maksettavaPalkka());
pankki.suoritaTilisiirto(maksu);
maksut.add(maksu);
}
}
public List<String> tyontekijat(String format) {
varmistaFormaatinTuki(format);
List<String> output = new ArrayList<>();
for (Henkilo henkilo : tyontekijat) {
output.add(builders.get(format).build(
henkilo.getTunnus(),
henkilo.getNimi(),
henkilo.getTili(),
henkilo.getTuntipalkka()));
}
return output;
}
public List<String> maksuhistoria(String format) {
varmistaFormaatinTuki(format);
List<String> output = new ArrayList<>();
for (Maksusuoritus maksusuoritus : maksut) {
output.add(builders.get(format).build(
maksusuoritus.getNimi(),
maksusuoritus.getTili(),
maksusuoritus.getSumma()
));
}
return output;
}
private Henkilo haeHenkilo(int tunnus){
for (Henkilo henkilo : tyontekijat) {
if (tunnus==henkilo.getTunnus()) {
return henkilo;
}
}
return null;
}
protected void varmistaFormaatinTuki(String format) throws IllegalArgumentException {
// TODO: add support for JSON
if (!builders.keySet().contains(format)) {
throw new IllegalArgumentException("supported formats: "+builders.keySet());
}
}
}
Muotoiluluokkien toteuttama rajapinta on seuraava:
public interface OutputBuilder {
String build(Object... parts);
}
CSV-muotoilun toteuttama luokka on melko yksinkertainen:
public class CsvBuilder implements OutputBuilder {
public String build(Object... parts) {
String csv = "";
for (int i = 0; i < parts.length-1; i++) {
csv += parts[i]+";";
}
return csv+parts[parts.length-1];
}
}
Refaktoroitu palkanlaskentaolio luodaan seuraavasti:
Map<String, OutputBuilder> outputBuilders = new HashMap<>();
outputBuilders.put("csv", new CsvBuilder());
Palkanlaskenta p = new Palkanlaskenta(new MaksupalveluRajapinta(), outputBuilders);
Ohjelma on jo melko paljon parempi kuin alkuperäinen, muttei kuitenkaan täydellinen. Tehtävässä 5 mainittu primitive obsession vaivaa edelleen ohjelmaa, ja ainakin palkan tallettamisessa olisi syytä siirtyä alkeistyypin int käytöstä oman luokan Raha
käyttöön. Tulostuksen muotoilusta vastaavat OutputBuilderit kaipaisivat myös pienen muotoksen. Nykyinen versio ei nimittän ole kovin toimiva, jos ohjelman tulokset olisi saatava myös XML-muotoisena.