Skip to content

Palkanlaskenta malli

Matti Luukkainen edited this page Dec 12, 2016 · 4 revisions

3

Luokkakaavio:

4

Sekvenssikaavio:

Kaavioon on nyt merkattu myös joitain Palkanlaskennan ja Henkilön sisäisiin ArrayListeihin kohdistuvia operaatioita.

5

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 ja maksuhistoria 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

6 ja 7

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.