TIE-20200 Ohjelmistojen suunnittelu Luento 7: SOLID ja olioiden rakentelumalleja TIE-20200 Samuel Lahtinen 1
Ajankohtaista Harjoitustyössä suunnittelusessioiden ajanvaraus auki Viikkoharjoituksissa tehtaaseen/rakennuttajiin tutustumista (ja periytymisen käyttöä)
Ohjelmassa tänään Hippusellinen lisätarinaa MVC:stä (yms. jutuista) Harkkatöiden suunnitteluvaiheeseen soveltuvaa kamaa Olioiden luomista, Abstrakti tehdas yms. SOLID, yleisiä olio-ohjelmoinnin suunnitteluperiaatteita
SOLID, hyviä periaatteita Viisi olio-ohjelmoinnin perusperiaatetta, muistisääntö S Single responsibility principle O Open/closed principle L Liskov substitution principle I Interface segregation principle D Dependency inversion principle Ei estä ketterää kehitystä tai muutakaan mukavaa, päin vastoin helpottaa näitä Muutokset, laajentaminen, päivittäminen helpompaa, voidaan toimia ketterästi ilman loppuunasti hiottua suunnitelmaa
Single responsibility principle Single responsibility principle (SRP) a class should have only a single responsibility. Vain yksi vastuualue/luokka Pohjautuu reason-to-change ajetelmaan, jokaisella luokalla pitäisi olla vain yksi huolehdittava asia, vain yksi syy, miksi luokan toteutusta pitäisi muuttaa. Jos luokalla kaksi tai useampi muutossyy, voi muutoksella yhden vastuualueen toimiin olla helposti vaikutusta toisen toimintaan (korjataan yhtä, rikotaan toinen) Uudelleenkäyttö ja eriyttäminen vaikeampaa, kun mukana tulee ylimääräistä kuormaa
Single responsibility principle Saman luokan kaksi vastuuta (reason for change), muutos toiseen voi rikkoa toisen puolen toiminnallisuuden Vastuualueiden jako (esim. GUI-versio käyttää GeometrisenSuorakaiteen palveluita, laskentapuolella käytetään vain geom. osaa) Suorakaide +laskeala(): double +piirraruudulle() GUI Matemaattinen laskentaosuus GUIsovellus Vaarat: turha kompleksisuuden lisääminen, jos vastuualueet muuttuvat aina yhdessä, ei niiden eriyttämisestä ole iloa TIE-20200 Samuel Lahtinen Yleinen kompromissiratkaisu, tehdään erilliset rajapinnat, sama toteuttaja. Helpompi muuntaa ja korjata, jos tarve ilmenee.
Open-closed principle Software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification You should be able to extend a classes behavior, without modifying it. Moduulit ja luokat: Ovat avoimia laajennuksille, toiminnallisuutta voidaan lisätä, rajapintaa laajentaa uusilla palveluilla Suljettu muutoksilta: moduulin tai luokan toteutusta ei saa muuttaa Koodi testattu, katselmoitu jne, sitä ei enää muuteta Kuulostaa ristiriitaiselta, ideana hyödyntää abstraktioita Voidaan käyttää hyväksi perintää, (abstrakteja) kantaluokkia Ohjelmat, joissa hyödynnetään open-closed periaatetta, muutetaan lisäämällä uutta koodia, ei muuttamalla olemassa olevaa Ei realistisesti mahdollista päästä tilanteeseen, jossa 100% koodista suljettuna muutoksilta pitää tehdä päätöksiä siitä, mihin panostetaan, missä ohjelman osissa tulee todennäköisimmin muutoksia? Abstraktiot, ja osan ohjelmakoodin sulkeminen.
Open-closed principle Vinkkejä noudattamisen helpottamiseen: Ei julkisia tai protected jäsenmuuttujia, ei globaaleja muuttujia Vältä: Olioiden omistaminen arvoina, arvoparametrien käyttö (C++) Viitteet, osoittimet, voidaan antaa käyttöön erikoistettu versio jne. Älä käytä dynamic_castiä tyyppitarkastusten tekemiseen, vain laajennettuun rajapintaan kiinni pääsemiseen (tai toteutetun rajapinnan toteuttamisen tarkastamiseen), esimerkki huonosta tavasta(taululle)
Liskov substitution principle Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program. Derived classes must be substitutable for their base classes Mikä tahansa aliluokan instanssi kelpaa kantaluokkaolion tilalle (ilman että ohjelman toiminta kärsii/muuttuu virheelliseksi) C++-maailmassa: mikä tahansa viitteen tai osoittimen luokkaan ottavan funktion on pystyttävä käyttämään mitä tahansa luokan aliluokkaa ilman että funktion toteutuksessa täytyy tietää todellinen tyyppi (taas vääränlaiset tyyppimuunnokset jne.) Toiminnalliset aliluokat: Toiminnallinen lisävaatimus pelkän syntaksivaatimuksen lisäksi (semantiikka), ei pelkkä syntaktinen yhteensopivuus (samat funktion protyypit jne.)
Liskov substitution principle, Rikkomisen seuraamuksia Otetaan esimerkkinä (taas) suorakaide ja siitä peritty neliö, vektori ja järjestetty vektori, perustietovarasto ja tilaoptimoiva tietovarasto Päästään takaisin is-a-suhteen merkitykseen Is-a-suhdetta ja Liskovin periaatetta ei pidä rikkoman koodirivejä säästääkseen tai pelkän käsitteellisen sukulaisuuden takia Sopimussuunnittelu ja Liskov: aliluokat eivät saa rikkoa kantaluokan sopimuksia Vain periaatetta noudattamalla on mahdollista kirjoittaa luokkia käyttävää koodia ja luottaa siihen, että uudet lisäykset (uudet luokat) eivät hajota olemassa olevan koodin toimintaa void testaa( Suorakaide& s ) { s.asetakorkeus( 6 ); s.asetaleveys( 3 ); assert( s.leveys()*s.korkeus() == 18 ); } class Suorakaide { public: virtual void asetakorkeus( int k ); virtual void asetaleveys( int l ); int leveys() const; int korkeus() const; private: int korkeus_; int leveys_; }; class Nelio: public Suorakaide { public: virtual void asetakorkeus( int k ); virtual void asetaleveys( int l ); };
Interface segregation principle many client-specific interfaces are better than one general-purpose interface. Make fine grained interfaces that are client specific Rajapinnan käyttäjän ei pidä olla riippuussuhteessa palveluihin/funktioihin joita se ei tarvitse/käytä Jaetaan isommat rajapinnat pienemmiksi roolien mukaan, saadaan roolirajapintoja (role interfaces) Isompi ohjelma, jossa paljon keskinäisiä riippuvuuksia ja isoja möhkälerajapintoja pienikin muutos heijastuu joka puolelle muutoksen tekijä joutuu tonkimaan muutettavat asiat muun turhan sälän seasta yksittäiseen asiaan liittyvän toteutuksen vaihtaminen vaatii copy-pastea vanhoista koodeista Uusien ominaisuuksien lisääminen vaatii uuden asian lisäämisen lisäksi vanhan huomioimista
Interface segregation Anti-versio periaatteesta, yksi iso möhkäleluokka, joka tarjoaa lähes kaikki toiminnot. Vaikea hallita, päivittää, muuttaa, jos rinnakkaisuutta, saadaan suorituskykyongelmat mukaan Lihavat rajapinnat (fat interfaces), rajapintojen saastuminen (interface pollution)
Asiakas 1 Asiakas 2 palvelu Asiakas 3 Asiakas 1 Asiakas 2 Asiakas 3 Rajapinta 1 Rajapinta 2 Rajapinta 3 Palvelun toteutus
Interface segregation principle Rajapintojen saastuminen ja perintä: Ajastus, ovi, ajastettu ovi Tuote, Peli, CD, housut Käyttäjä (sisäänkirjautumisessa): aliluokat, vieras, normi, admin Mitä kantaluokkaan, mitä erillisiin rajapintoihin, mitä aliluokkiin?
Dependency inversion principle A) High-level modules should not depend on low-level modules. Both should depend on abstractions. B) Abstractions should not depend upon details. Details should depend upon abstractions. http://www.objectmentor.com/resources/articles/dip.pdf Riippuvuudet abstraktioihin, ei konkreettisiin toteutuksiin Huonon suunnittelun kriteeristöä: Ei siirrettävää koodia, ohjelman rakenne täynnä riippuvuuksia, komponentteja ei voi uudelleen käyttää tai siirtää näiden takia Jäykkyys, yksittäinen muutos vaikuttaa moneen muuhun osaan Hauraus, muutos yhteen osaan rikkoo asioita muualla
Korkea taso (sovelluslogiikan kannalta) Abstraktio(kerros) Rajapinnat Matalamman tason toteutus
Dependency inversion principle Korkeamman tason komponentit eivät saa olla riippuvaisia matalan tason toteutusyksityiskohdista, jotain ratkaisutapoja: Dependency injection Adapter-pattern, fasadit, sillat yms. Kerrosarkkitehtuurit Rajapinnat C++ ja toteutus- ja otsikkotiedoston erottaminen ei tarkoita rajapinnan tai toteutusriippuvuuden poistumista Tärkeimpiä tapoja tehdä koodista: Muunneltavampaa, kestävämpää muutoksille (toteutusten muuttaminen ei hajota koko ohjelmaa) Ylläpidettävämpää Helpommin uudelleen käytettävää
Olioiden luominen Uuden olion luominen, normaalissa tilanteessa pitää tietää oikea rakentaja, oikeat parametrit, muuttuu työläämmäksi erityisesti, jos paljon eri vaihtoehtoja koodi riippuvaista toteutuksesta, jokainen uusi lisätty luokka vaatii toteutuskoodin puukottamista Ongelmaan tarjolla erilaisia ratkaisuja, rakentajametodit, rakentajaluokat, erilaiset tehtaat
Ongelma: Miten varmistaa yhdenmukaiset oliot? Alusta TAI MUTTA EI:
Abstrakti tehdas suunnittelumalli AbsFactory createproducta(): ProductA createproductb(): ProductB ProductA2 opera ProductA opera ProductA1 opera Alusta ProductB operb ProductB2 ProductB1 operb operb Factory1 <<create>> Factory2 <<create>>
Abstract Factory abstrakti tehdas Tehdasmallin idea, käyttäjälle annetaan abstrakti tehdas (tai tehdasrajapinta) jonka avulla rakentaa komponentteja Erilaisia konkreettisia tehtaita, jotka toteuttavat varsinaisen rakentamisen Rakennettavat komponentit, abstrakti kantaluokka/rajapinta ja konkreettiset vastineet Tehtaan käyttäjä pyytää rakentamaan esim. uuden nappulan, tehdas Vertailu ideatasolla: olioiden kopiointi, virtuaalinen kopiointifunktio Fokus komponenttijoukon rakentamisessa
Abstract Factory abstrakti tehdas Erikoistetut tehtaat, eri ympäristöön, eri käyttöliittymään, eri käyttäjäprofiilille, jne. oma tehtaansa. Pyydetään haluttua tehdasta luomaan ohjelman käyttämät komponentit. Tehtaan käyttö samanlaista riippumatta rakennettavien olioiden tyypistä (esim. käyttöliittymäkomponenttitehdas ja eri ympäristöt, profiilit) Esimerkki: käyttöliittymäperhe ja eri ympäristöt/kirjastot Käyttöliittymätehdas rakentaa ikkunan ja sen eri osat (luox,y,z) Ikkuna kantaluokkana, erikoistetut versiot jokaiselle ympäristölle erikseen Käyttöliittymätehtaan erikoistetut versiot (vastaavat kuin komponenttien erikoistus) Mainissa tms. tarkistus, mitä versiota käytetään ja luodaan/otetaan käyttöön oikea tehdas. Muualla käytetään annettua tehdasta ja komponentteja, muu koodi toimii samoin riippumatta käytössä olevasta versiosta
Tehdasmetodi (Factory method) Editori, jonka pitäisi osata avata oikeanlainen tiedosto, eri tiedostojen avaamista ja käsittelyä varten omat tiedostolukijansa. Perinteinen tapa, jokainen eri tiedostotyyppi pitää rakentaa erikseen, if-elsejä tai vastaavia hässäköitä. Uuden tiedostotyypin lisääminen vaatii Editorin koodin puukottelua. Sama tapahtuu tiedoston muuntamisessa muodosta toiseen Olisi huomattavasti mukavampaa, jos saman voisi tehdä automaattisesti ja uusien tyyppien lisääminen ei vaatisi olemassa olevan koodin käpistelyä FileConverter Editor FileReader.rtf.doc.txt.rtf.doc.txt.docx TIE-20200 Samuel Lahtinen.docx
Tehdasmetodi (Factory method) Yksinkertaisin rakentajametodi, tehdään kantaluokkaan luokkafunktio (static member function) joka hoitaa rakentamisen. Osaa päättää parametrin perusteella, mikä hierarkian olioista halutaan rakentaa Esimerkki tiedostojuttujen kanssa: Hifistelyä, jokainen aliluokka osaa rekisteröityä (automaattisesti) rakentajalle ja kertoa esim. parametrien sisällöstä (kuten tiedostotyyppi), rakentajafunktio/olio kutsuu parametrin perusteella oikeaa rakennusmetodia ja uusi oikeantyyppinen oli syntyy Funktion käyttäjän ei tarvitse tietää mitään toteutuksesta tai luokkahierarkiasta FileConverter Editor FileReader.rtf.doc.txt.rtf.doc.txt.docx TIE-20200 Samuel Lahtinen.docx
Rakennuttajien automaattisesta rekisteröinnistä Automaattisesti tehtaalle/rakennuttajalle rekisteröityvien luokkien tekeminen Uusi luokka vaatii vain käännöksen ja kaikki toimii kivasti toteutustapoja luokkamuuttujat, luokkafunktiot static-muuttujat, joiden arvo alustetaan rekisteröintifunktion avulla Template-hässäkät C++11 ja lambdat Ongelmia: Kääntäjät turhan innokkaita optimoimaan ja saattavat jättää esim. muuttujien alustuksen tekemättä ennen kuin niitä käytetään ensimmäistä kertaa Lisää kompleksisuutta
Object Pool (olioiden kierrättämistä) Olioiden luominen ja tuhoaminen voi olla raskasta erityisesti jos niihin liittyy vähänkään monimutkaisempia alustustoimenpiteitä Uusien luomisen sijaan tarjotaankin olioita käytettäväksi olioaltaasta Kun tarve tulee, object poolista voi noutaa olion ja käytön jälkeen palauttaa sen takaisin (kuten keilahalli ja keilakengät) Voidaan kontrolloida käytössä olevien olioiden määrää, saadaan kapseloitua olioiden luominen Käyttäjä ei huomaa eroa, kutsuna pyydetään olioita ja tuloksena voi olla joko uusi olio, tai jo jonkun aikaisemmin palauttama
Resource Acquisition is Initialization (RAII) Programming idiom (C++), ei siis suunnittelu/toteutusmalli (yleistettävyys) Idea: resurssin vapauttamisesta huolehtii purkaja (ja varaamisesta rakentaja) Resurssin varaus näkyvyysalueen mukaan, automaattinen vapauttaminen, ei muistamisongelmaa/poikkeustilanneongelmaa jne.
Yhteenveto SOLID-periaatteet, samoja tavoitteita ja ideoita kuin muissakin kurssilla opituissa tekniikoissa. Esim. suunnittelumallit tarjoavat hyväksi havaittuja ratkaisuja näiden saavuttamiseen Olioiden luomiseen liittyvät patternit, helpottavat laajennettavuutta, vähentävät suoria toteutusriippuvuuksia Lisäävät koodin määrää, tekevät ohjelmasta hieman monimutkaisemman Usein lähes pakollisia ohjelman muokattavuuden ja laajennettavuuden säilyttämiseksi Lähdemateriaalia ja lisäluettavaa: http://www.objectmentor.com/resources/articles/principles_and_patterns.pdf TIE-20200 Samuel Lahtinen 30