Olemme käsitelleet aikaisemmin blogissamme vastuita ohjelmistoarkkitehtuurin toteutuksessa sekä sitä, kuinka arkkitehtuuri vaikuttaa koko ohjelmistoprojektin onnistumiseen. Ohjelmistoarkkitehtuurilla tarkoitetaan ohjelmiston suunnittelua ja kehitystä kokonaisuutena. Se määrittelee muun muassa:
- Miten ohjelmisto jakautuu eri osiin kuten komponentteihin ja kerroksiin?
- Mitkä näiden osien vastuut ja riippuvuussuhteet ovat?
- Miten nämä osat kommunikoivat keskenään?
Vaikka ohjelmistoarkkitehtuuri on ylätason käsite, jossa on kysymys ohjelmistosta kokonaisuutena, se koskettaa myös matalan tason ratkaisuja. Esimerkiksi käytännöt, joilla koodia tuotetaan, ovat erottamaton osa arkkitehtuuria, sillä myös ne määrittävät komponenttien vastuita ja riippuvuussuhteita. Pelkällä kauniilla kuvalla täynnä nuolia ja laatikkoja ei voi siis kokonaisvaltaisesti esittää ohjelmiston arkkitehtuuria.
Ohjelmiston suunnittelu edellyttää luonnollisesti myös päätöksiä teknologioiden suhteen. Arkkitehtuurissa keskeisintä ei ole kuitenkaan teknologioiden valinta. Hyvin suunniteltu ohjelmistoarkkitehtuuri ottaa itse asiassa mahdollisimman vähän kantaa käytettäviin teknologioihin ja suhtautuu niihin mahdollisimman pitkälle toteutusyksityiskohtina. Arkkitehtuurissa onkin kysymys ennemmin abstraktioista.
Erityisesti ohjelmistoarkkitehtuuri pyrkii mahdollistamaan sen, että ohjelmistoon voidaan tehdä muutoksia sitä kehitettäessä ja myös jälkikäteen – muun muassa käytettävien teknologioiden osalta. Näin arkkitehtuuri pystyy vastaamaan myös ohjelmiston muuttuviin vaatimuksiin. Jos ohjelmistoa ei voi muokata jälkikäteen, softwaresta muodostuukin vähitellen hardwarea.
Muutosten väistämättömyys
Ennen kuin arkkitehtuuria voidaan alkaa määrittää, täytyy ohjelmistokehitystiimillä olla jonkinlainen käsitys ohjelmiston vaatimuksista. Vaatimuksia ohjelmistolle asettavat muun muassa
- Sovelluksen käyttäjät ja sidosryhmät
- Käyttötapaukset ja niistä seuraavat toiminnalliset vaatimukset
- Ei-toiminnalliset vaatimukset
Mikään ei ole kuitenkaan yhtä varmaa ohjelmistoprojektissa kuin muutos. Sellaisista ohjelmointiprojekteista, joissa vaatimukset olisi onnistuttu määrittelemään täydellisesti etukäteen, kuulee lähinnä kansantaruissa. Miksi vaatimukset sitten ovat alttiita muutoksille? Eikö niistä pystytä varmentumaan yksinkertaisesti tarpeeksi hyvällä etukäteissuunnittelulla?
Syitä vaatimusten muuttumisille on monia. Yritysten bisnestarpeet kehittyvät nimittäin jatkuvasti, jolloin sovellukselta saatetaan haluta ajan kuluessa eri asioita kuin alun perin. Esimerkiksi kilpailijoiden ratkaisut voivat asettaa paineita vaatimusten uudelleentarkastelulle. Samoin loppuasiakkailta ja -käyttäjiltä kantautuvat toiveet sekä palaute projektin aikana antavat usein aihetta uudelleenharkinnalle. Ohjelmistoa koskevissa laeissa ja sääntelyssä voi niin ikään tapahtua muutoksia sen elinkaaren aikana
Aluksi projektin tilaavalle osapuolelle eivät välttämättä ole edes kaikki omat tarpeet tai ratkottavan ongelman luonne selvillä, vaan tieto niistä kehittyy ja tarkentuu projektin edetessä. Tähän vaikuttaa myös se, että projektilla saattaa olla paljon eri sidosryhmiä sekä tilaajan oman organisaation sisällä että sen ulkopuolella, jolloin todellisten vaatimusten selvittämiselle tuottavat haasteita kommunikaatio ja byrokratia. Sidosryhmät saattavat vieläpä vaihtua projektin aikana, minkä lisäksi oman ongelmansa vaatimusmäärittelyyn tuottavat mahdolliset väärinymmärrykset.
Ohjelmiston tilaajaosapuolelle ei välttämättä ole tiedossa sekään, mihin teknologia taipuu hänen tarpeidensa suhteen ja mitä se mahdollistaa. Se ei ole välttämättä selvää ohjelmistokehitystiimille itselleenkään! Toteutusteknologia voi kehittyä projektin aikana, kuten myös tiimin ymmärrys siitä.
Vaatimusmuutoksia voi kummuta useasta eri lähteestä.
Konkreettinen esimerkki vaatimusmuutoksista
Kuvitellaan, että suunnittelemme globaalisti toimivaa web-lippukauppaa. Lippukaupassa tapahtumajärjestäjät voivat järjestää tapahtumia, joihin loppukäyttäjät pystyvät ostamaan lippuja eri puolilta maailmaa. Järjestelmän pitää pystyä palvelemaan vakaasti suuria määriä käyttäjiä pienin viivein ja mahdollisimman vähin katkoksin.
Tällaista lippukauppaa kehittäessä saatetaan kohdata monenlaisia vaatimusmuutoksia. Kesken projektin saatetaan huomata esimerkiksi, että kilpaileva sovellus on toteuttanut edistyneen tapahtumasuositteluominaisuuden, jonka loppukäyttäjät kokevat erityisen hyödylliseksi. Jotta kehitettävä sovellus pysyy kilpailukykyisenä, tähän voi olla välttämätöntä vastata.
Loppukäyttäjiltä saatetaan myös saada palautetta, että etukäteen kaavaillut maksuvaihtoehdot ovat liian rajoittavat, sillä kaikkia monissa maissa yleisesti käytettyjä luottokortteja ei tueta. Kenties tapahtumajärjestäjät tuovat vuorostaan pöydälle toiveen, että he haluavat mainostaa kaupassa muitakin tuotteitaan ja palveluitaan tapahtumien ohella, mitä ei olla osattu ottaa huomioon. Ehkäpä lippukauppaa kehittävä yritys taas itse tunnistaa tarpeet alkuperäistä kattavammalle analytiikalle, jotta se voi seurata luotettavasti käyttäjätrendejä.
Voi olla, että sovellusta kehittäessä havaitaan jonkin valitun teknologian olevan tarpeeseen riittämätön: mobiiliapplikaation toteutukseen valittu React Native saatetaan esimerkiksi kokea ominaisuustueltaan rajoittavaksi, minkä vuoksi on suoritettava migraatio natiivisovelluksiin. Myöhemmin tehdään puolestaan hintavertailu, jossa havaitaan, että sovelluksen vaatimat palvelut tulisivat sadoille tuhansille käyttäjille skaalautuessaan tuntuvasti halvemmaksi AWS-pilvessä kuin Google Cloudissa, johon järjestelmä on alun perin pystytetty. Tämän vuoksi palveluntarjoajaa halutaan vaihtaa.
Osassa lippukauppa-applikaation kohdemaista lainsäädäntö myös saattaa muuttua kesken kehitystyön, mikä vaikuttaa ohjelmiston datankäsittely- ja tietoturvavaatimuksiin. Samoin esimerkiksi tietoturva-auditoinnissa voi nousta esille puutteita, joita ei ole sovellusta suunnitellessa osattu ottaa huomioon.
Tällaiset muutokset ovat varmasti jokaiselle ohjelmistokehitystiimille tuttuja. Usein myös suurimmat ohjelmistokehitysprojektin haasteet liittyvät niihin. Jos sovellusta kehitettäessä onkin lukittauduttu jo alkuvaiheessa joustamattomiin teknisiin ja arkkitehtuurillisiin valintoihin, ohjelmiston sovittaminen vaatimusmuutoksiin voi tällöin vaatia huomattavaa lisätyötä.
Toisaalta täytyy muistaa, että muutos on myös ohjelmistokehityksen perimmäinen luonne: jos softwarea ei olisi pohjimmiltaan softia – eli sitä ei pystyisi muuttamaan ja sopeuttamaan uusiin vaatimuksiin –, se olisi käytännössä sama toteuttaa hardwarena.
Miten arkkitehtuurilla varaudutaan muutoksiin?
Hyvin suunniteltu arkkitehtuuri tekee ohjelmistosta joustavamman ja helpommin mukautettavan vastaamaan uusia tai muuttuvia vaatimuksia. Ohjelmistoarkkitehtuurin pitäisi vastata ainakin kolmeen seuraavaan kysymykseen:
- Mitä eri vastuita ohjelmistolla on ja miten ne jakautuvat?
- Missä ohjelmiston sisäiset ja ulkoiset rajapinnat kulkevat?
- Mitkä osat ohjelmistosta ovat todennäköisimmin alttiita muutokselle ja mitkä eivät?
Ohjelmiston vastuut
Ohjelmiston vastuilla tarkoitetaan sitä, että jokaisella ohjelmiston komponentilla on oma roolinsa ja tarkoituksensa sovelluksen ylätasolta aina pienimpiin koodipalikoihin, kuten luokkiin ja funktioihin. Ylätason vastuut voivat olla luonteeltaan abstraktimpia: yksittäisen ohjelmistokomponentin vastuu voi olla esimerkiksi käyttäjän autentikoiminen lippukauppaan. Matalalla tasolla yksittäinen funktio voi sen sijaan vastata esimerkiksi käyttäjän autentikaatio-tokenin validoimisesta. Tässä mielessä, kuten aiemmin todettua, myös koodin yksityiskohdat ovat arkkitehtuuria.
Vastuiden osalta olennainen periaate on niin sanottu single-responsibility principle eli yhden vastuun periaate. Se tarkoittaa, että kullakin ohjelmistokomponentilla, puhuttiin sitten korkean tai matalan tason komponentista, pitäisi olla vain yksi syy muuttua. Toisin tämä vastaava sääntö ilmaistaan myös nimityksellä separation of concerns eli huolenaiheiden eriyttäminen.
Nämä periaatteet mahdollistavat, että ohjelmistokomponentit pysyvät yksinkertaisina ja niitä on helpompi ylläpitää erillisinä kokonaisuuksina. Tällöin niitä on myös vaivatonta testata, eikä niiden muuttaminen aiheuta odottamattomia kertautuvia sivuvaikutuksia muihin komponentteihin. Vastaavasti näiden ohjenuorien noudattamisesti seuraa, ettei yksi muutos vaatimuksissa aiheuta muutostarpeita koodiin loputtoman moneen paikkaan.
Rajapintojen määrittely
Selkeä vastuiden määrittäminen edesauttaa sitä, että sovelluksen komponenteille voidaan määritellä yhtälailla selkeät rajapinnat. Rajapintojen tarkoituksena on mahdollistaa se, että ohjelmiston komponentit keskustelevat keskenään ennustettavasti ja sovitulla tavalla. Tällöin kukin komponentti voidaan lisäksi enkapsuloida omaksi kokonaisuudekseen, mikä tarkoittaa, että sen yksityiskohdat piilotetaan julkisten rajapintojen taakse. Näin ohjelmistokomponentteja voidaan käsitellä rajapintojen kautta abstraktioina välittämättä tarkasta toteutuksesta pinnan alla.
Huomionarvoista on, että rajapinnoilla ei arkkitehtuurin yhteydessä tarkoiteta vain REST- tai GraphQL-tyylisiä web-rajapintoja vaan mitä tahansa toteutustavasta riippumatonta komponenttien välistä rajapintaa. Rajapinnan muodostaa myös ohjelmiston vuorovaikutus ulkomaailman kanssa.
Lippukauppaesimerkissämme yksi korkean tason rajapinta voisi olla muun muassa lippujen ostorajapinta. Tämä voidaan toteuttaa esimerkiksi REST-rajapintana, johon client-ohjelma ottaa yhteyden asiakkaan selaimelta. Matalan tason rajapintana koodissa voisi kulkea puolestaan TicketInventoryInterface-tyypin olio, joka tarjoaa metodit tietynlaisten lippujen saatavuuden tarkistamiseen sekä niiden saatavuustilanteen päivittämiseen. Se myös enkapsuloi lippuinventaarion ja sen toteutusyksityiskohdat sisäänsä.
Hyvin määritellyillä rajapinnoilla sekä enkapsulaation hyödyntämisellä on monia etuja. Niiden ansiosta ohjelmistokomponentteja on helpompi kehittää itsenäisinä kokonaisuuksina sekä käsitellä modulaarisesti. Niitä on myös vaivattomampi testata niin yhdessä kuin erikseen.
Komponenttien irrottaminen toisistaan
Kun ohjelma on suunniteltu siten, että kunkin komponentin vastuut on mietitty tarkkaan ja niiden yksityiskohdat on enkapsuloitu huolella suunniteltujen rajapintojen taakse, vaatimusmuutosten edellyttämät koodimuutokset eivät kantaudu ketjureaktionomaisesti komponentista toiseen. Komponentit eivät nimittäin tällöin riipu tiukasti toisistaan. Tällaista prosessia, jossa ohjelmistokomponentit tehdään toisistaan riippumattomaksi, kutsutaan myös decoupling-periaatteeksi eli komponenttien toisistaan irrottamiseksi. Siitä on monia käytännön hyötyjä.
Esimerkiksi web-lippukauppaa toteuttaessa yhden ison teknologiapäätöksen muodostaa tietokannan valinta. Tietokannan pitää nimittäin pystyä varastoimaan suuria määriä dataa sekä lippuja ostettaessa kyselemään ja kirjoittamaan sitä nopeasti. Valinta ei ole kuitenkaan yksinkertainen tehtävä, sillä tietokantateknologioiden todellisia vahvuuksia sekä heikkouksia, kuten suorituskykyä tai skaalautuvuutta, voi olla vaikea arvioida konkreettisesti muuten kuin kokeilemalla ja kantapään kautta.
Jos skaalautumistarvetta ennakoidaan valitsemalla esimerkiksi tehokkaasti skaalautuva tietokanta, se saatetaankin huomata jälkikäteen kalliiksi ratkaisuksi tai vaikkapa kyselyjen toteuttamisen kannalta haastavaksi. Perinteinen SQL-pohjainen relaatiotietokanta ei taas välttämättä puolestaan hallitse yhtä helposti suuria globaaleja datamääriä.
Tulevaisuuden ongelmia on vaikea hyvällä suunnittelemisella ja etukäteistestauksellakaan ennustaa täydellisesti, joten teknologiavalintaan lukittautuminen voi osoittautua kalliiksi ratkaisuksi. Tietokanta muodostaa kuitenkin hyvin erottamattoman osan koko sovellusta, joten mikä neuvoksi?
Yksi tärkeimmistä periaatteista rajapintojen ja komponenttien toisistaan irrottamiseksi on niin sanottu dependency inversion -periaate eli riippuvuuksien kääntäminen. Sen mukaan korkean tason ohjelmistokoodin ei pitäisi koskaan riippua matalan tason ratkaisuista. Sen sijaan sen tulisi käsitellä niitä abstraktioiden ja niiden rajapintojen kautta.
Tietokanta edustaa tässä tapauksessa nimenomaan matalan tason ratkaisua. Sovelluksen ylemmän tason, kuten palvelukerroksen logiikan, ei pitäisi siis riippua käytössä olevasta tietokannasta tai niistä kirjastoista ja kehyksistä, joilla sitä käsitellään. Tällöin riippuvuudet tiettyyn tietokantaan eivät leviä ympäri muuta ohjelmistokoodia vaan ne eristetään siitä, mikä helpottaa tietokantatoteutuksen muuttamista ja vaihtamista kesken kehitystyön.
Perinteinen tapa toteuttaa dependency inversion -periaate on niin sanottu plugin-arkkitehtuuri. Sen sijaan, että ylemmän tason koodi riippuisi suoraan matalan tason koodista, se käsitteleekin sitä rajapintojen kautta. Alla on esitetty kuvitteellinen ja yksinkertaistettu esimerkki, miten web-lippukauppa voisi hyödyntää tällaista ratkaisua:
// TicketRepositoryInterface.ts export interface TicketRepositoryInterface { getAvailableTickets(eventId: string): Promise<number>; reduceTicketCount(eventId: string, quantity: number): Promise<void>; } // TicketPurchaseService.ts import { TicketRepositoryInterface } from './TicketRepositoryInterface'; export class TicketPurchaseService { private ticketRepository: TicketRepositoryInterface; constructor(ticketRepository: TicketRepositoryInterface) { this.ticketRepository = ticketRepository; } async purchaseTicket(eventId: string, quantity: number): Promise<void> { const tickets = await this.ticketRepository.getAvailableTickets(eventId); if (tickets < quantity) { throw new Error(`Not enough tickets available.`); } await this.ticketRepository.reduceTicketCount(eventId, quantity); } }
Ylläolevassa esimerkissä TicketPurchaseService ei ole suoraan riippuvainen matalan tason tietokantakoodista vaan sen sijaan TicketRepositoryInterface -rajapinnasta. Mikä tahansa matalan tason tietokantaan yhdistävä TicketRepository-objekti voi toteuttaa kyseisen rajapinnan. Siksi rajapinnan alla olevaa toteutusta voidaan myös helposti muokata tai vaihtaa se kokonaan.
Koodissa hyödynnetään myös niin sanottua dependency injection -menetelmää: TicketRepositoryInterfacen toteuttavaa oliota ei alusteta suoraan TicketPurchaseService-luokassa. Sen sijaan tämä riippuvuus välitetään sille konstruktorissa. Näin vältetään tiukan kytköksen muodostuminen objektien välille ja hyödynnetään sen sijaan loose coupling -periaatetta eli löyhää kytköstä.
Huomaamme samalla syyn sille, miksi teknologiavalinta (eli tässä tapauksessa tietokannan valinta) ei ollut keskeisintä ohjelmistoarkkitehtuurin kannalta. Huomattavasti kauaskantoisemman päätöksen muodostaa nimittäin itse teknologian sijaan se, miten ohjelmiston riippuvuuksia tietokantaa käsittelevään koodiin hallitaan.
Muutosalttiuden ennakointi
Muutoksiin varautuminen olisi helppoa, jos kaikkien yllä olevien periaatteiden noudattaminen olisi täysin yksinkertaista. Monesti voi olla kuitenkin vaikea hahmottaa etukäteen täydellisesti, miten esimerkiksi ohjelmiston vastuut jakautuvat ja missä rajapintojen pitäisi kulkea.
Löyhästi toisiinsa kytkeytyvien, hyvin mietittyjen rajapintojen työstäminen komponenttien välille ei ole ilmaista vaan usein aikaavievempää kuin tiukkojen kytkösten muodostaminen. Siksi jokaista mahdollista rajapintaa, jonka voisi toteuttaa, ei välttämättä aina kannata toteuttaa. Vaikka aika ja resurssit riittäisivät hieromaan ohjelmistosta loputtoman joustavan, yleensä viimeistään ohjelmoijien kaukonäköisyyden puute asettaa tälle rajansa.
Tärkeää muutoksien ennakoinissa onkin tunnistaa, mitkä ohjelmiston osat ovat alttiimpia muutoksille ja minkä osien muuttuminen on vähemmän todennäköistä. Esimerkiksi edellä tunnistimme, että globaalin web-lippukauppamme tapauksessa tietokannan kohdalla muutoksen riski on olemassa ja sen vaikutukset huomattavia, jos riippuvuuksia siihen ei hallita huolella.
Muitakin muutosalttiita osia lippukauppaohjelmistossa on helppo havaita. Käyttäjäroolien kohdalla kannattaa muun muassa varautua siihen, että kaikkia oleellisia sidosryhmiä tai käyttötapauksia sovellukselle ei ole tunnistettu, jolloin erilaisia käyttäjärooleja saatetaan kaivata lisätä. Sovellus pitää suunnitella siis niin, että uusien käyttäjäroolien toteuttaminen on yksinkertaista. Samoin sovellukselle voi ilmestyä tarve tukea ajan kuluessa erilaisia maksutapoja, lipputyyppejä, tapahtumatyyppejä, hinnoittelutapoja sekä markkinointikampanjoita. Eri maat ja säädökset voivat puolestaan tuottaa muutoksia muun muassa kielitukivaatimuksiin, lippujen uudelleenmyymiskäytäntöihin sekä verolaskelmiin. Myös sovelluksen käyttöliittymä on luonnollisesti hyvin altis muutoksille.
Toisaalta esimerkiksi lippujen ostoprosessin tai lippujen saatavuudenhallinnan peruslogiikka ovat hyvin suunniteltuna todennäköisemmin pysyviä. Tämä johtuu siitä, että ne ovat lippukaupan ydinbisnestä, joka ei ole niinkään riippuvaista maasta, jossa toimitaan, tai yksittäisistä sidosryhmistä.
Mahdottomia muutokset eivät toki ole niihinkään, sillä esimerkiksi ostoprosessi voi hyvin kehittyä: siihen saatetaan lisätä kohdennettuja tarjouksia, suositteluja, usean tapahtuman lippupaketteja, osamaksumahdollisuus ja niin edelleen. Todennäköisesti kuitenkin itse prosessi jossa käyttäjä
- valitsee tapahtuman,
- valitsee lipputyypin ja lisää ne ostoskoriinsa,
- maksaa ostoskorinsa sisällön,
- saa vahvistuksen maksutapahtumasta
pysyy pitkälti samana.
Muutosalttiuden ennakointi auttaakin ohjelmistokehitystiimiä hahmottamaan ohjelmiston vastuut paremmin sekä tunnistamaan, missä ohjelmiston rajapintojen pitäisi kulkea. Se myös havainnollistaa, mihin suuntaan komponenttien riippuvuuksien tulisi kulkea – eli mitkä asiat ohjelmistossa ovat ydinbisneslogiikkaa, josta toteutusyksityiskohtien tulisi riippua.
Se edesauttaa lisäksi kehitystyön priorisointia. Kehitystyössä on järkevää näet keskittyä ensin ydinbisneslogiikkaan sekä eristää rajapintojen taakse vähemmän keskeiset, muutosalttiit komponentit, jotka voidaan toteuttaa yksityiskohtaisesti myöhemmin.
Arkkitehtuuriesimerkkejä
Emme ole tässä blogitekstissä keskittyneet niinkään yksittäisiin arkkitehtuurimalleihin. Tämä johtuu siitä, että käytännössä jokaisen järkevän arkkitehtuurimallin pohjalla olevat perusajatukset ovat pitkälti samoja kuin yllä on esitelty. Vastuiden jakaminen, rajapintojen tunnistaminen ja määrittely, enkapsulointi ja abstrahointi, komponenttien tiukkojen kytkösten irrottaminen toisistaan sekä niiden riippuvuuksien suunnan kääntäminen siten, että toteutusyksityiskohdat ovat riippuvaisia sovelluksen ydinbisneslogiikasta, ovat kaiken ohjelmistoarkkitehtuurin ytimessä.
Esimerkkinä kuitenkin alla on esitelty kaksi erilaista mallia, jotka toteuttavat näitä periaatteita: Alistair Cockburnin Hexagonal Architecture sekä Robert C. Martinin Clean Architecture. Niiden välillä on havaittavissa huomattavia samankaltaisuuksia.
Esimerkki Alistair Cockburnin kuusikulmaisesta arkkitehtuurista eli Hexagonal Architecturesta.
Hexagonal Architecture -mallissa keskeistä ovat niin sanottujen porttien ja adapterien käsitteet. Sovelluksen ydinbisneslogiikka, joka on todennäköisimmin pysyvää, kytkeytyy muutosalttiisiin toteutusyksityiskohtiin, kuten tietokantaan ja käyttöliittymään, porttien kautta. Portit ovat käytännössä rajapintamääritys ohjelmiston ja ulkomaailman väliselle vuorovaikutukselle. Adapterit ovat puolestaan porttien määrittelemien rajapintojen konkreettinen toteutus, joka hallinnoi ulkomaailmasta ohjelmistolle saapuvia syötteitä (input) ja tuottaa ohjelmistosta ulos tulosteita (output).
Kuusikulmaisesta arkkitehtuurista kannattaa huomioida, ettei kuusikulmaisuudella ole itsessään varsinaista konseptuaalista merkitystä itse mallin suhteen. Sitä kutsutaan kuusikulmaiseksi yksinkertaisesti sen konvention vuoksi, että malli on tapana esittää kuusikulmion sisällä.
Siistin arkkitehtuurin malli Robert C. Martinin mukaan.
Robert C. Martinin Clean Architecture -mallissa puolestaan arkkitehtuuri jaetaan neljään eri kerrokseen: entiteetteihin, käyttötapauksiin, rajapinta-adaptereihin sekä ohjelmistokehyksiin ja ajureihin. Mallin idea on siinä, että mitä ulompi kerros kehällä on kyseessä, sitä alttiimpi se on muutoksille ja sitä vahvemmin sovelluksen arkkitehtuurin näkökulmasta sitä pitää kohdella toteutusyksityiskohtana. Kerrosten riippuvuudet kulkevat ulkoa kohti ydintä: ulompi kerros on aina riippuvainen sisemmästä kerroksesta.
Entiteeteissä on kyse sovelluksen ydinbisneslogiikasta ja keskeisistä datamalleista. Käyttötapauskerros taas vastaa sovelluslogiikasta, joka toteuttaa ohjelmiston käyttötapaukset. Rajapinta-adapterit, kuten web-pyyntöjen kontrollerit, käyttöliittymän näkymät sekä tietokantakyselykerros, puolestaan muokkaavat dataa ulkoisten järjestelmien, kuten asiakasohjelmistojen, selaimen tai tietokannan, ymmärtämään muotoon. Kanssakäymistä ulkoisten järjestelmien kanssa taas hallinnoivat erilaiset ohjelmistokehykset ja ajurit.
Huomionarvoisesti Clean Architecture -mallissa käyttötapauskerroksen ja rajapinta-adapterikerroksen välillä on havaittavissa sama porttien ja adapterien logiikka kuin Hexagonal Architecture -mallissa. Rajapinta-adapterit vastaavat heksagonaalisen mallin adaptereja. Käyttötapauskerros puolestaan kommunikoi rajapinta-adapterien kanssa ainoastaan rajapintojen eli porttien välityksellä.
Miksi arkkitehtuuri ei ratkaise kaikkea?
Buutilla olemme satojen ohjelmistoprojektien myötä havainneet, että ero onnistuneen ja epäonnistuneen projektin välillä kulkee usein siinä, kuinka hyvin projektin arkkitehtuuri on kyennyt vastaamaan ohjelmistoon kohdistuviin vaatimuksiin sekä varautumaan kehitystyön aikana (ja sen jälkeen) koittaviin vaatimusmuutoksiin. Arkkitehtuuri ei kuitenkaan ole yksin kaiken avain.
Vaikka arkkitehtuuri olisi kuinka hyvin suunniteltu, siitä ei ole hyötyä, jos sitä ei noudateta tai jos tieto siitä ei hajaudu tiimin keskuuteen. Samoin jos ohjelmistokehitystiimille ei valu ajantasainen ja selkeä tieto ohjelmistoon kohdistuvista vaatimuksista, arkkitehtuuria on vaikea suunnitella vaatimusten mukaisesti saati ennakoida niiden muutoksia.
Tästä syystä kehitystiimi pitää osallistaa jo varhaisessa vaiheessa vaatimustenmäärittelyprosessiin sen ylätasolta alkaen. Vastaavasti jos vaatimuksia määritellessä ei kuulla varhaisessa vaiheessa ja säännöllisesti olennaisia sidosryhmiä – pahimmassa tapauksessa loppukäyttäjiä – paraskaan arkkitehtuuri ei pysty vastaamaan sovelluksen todellisiin tarpeisiin.
Hyvän ohjelmistoarkkitehtuurisuunnittelun perusedellytys onkin organisaation toimiva kommunikaatio. Tiedon pitää kulkea selkeästi ja ajallaan sovellukselle vaatimuksia asettavilta sidosryhmiltä organisaation liiketoiminnasta vastaaville henkilöille sekä itse ohjelmistokehitystiimille. Jos ohjelmistoprojektin halutaan aidosti onnistuvan, tekniseen henkilöstöön ei voidakaan suhtautua ainoastaan suorittavana kerroksena, jota itse bisnes ei kosketa. Jotta he pysyvät aidosti perillä vaatimuksista, heidät pitää ottaa aktiivisesti mukaan keskustelemaan sovelluksen liiketoiminnasta!