Ajatusjohtajat
SOLID-periaatteiden käyttäminen Android-kehityksessä
Ohjelmistojen luominen on luovuuden teko, ja Android-kehitys ei ole poikkeus. Se on enemmän kuin vain saada jotain toimimaan. Se on suunnittelemista sovelluksia, jotka voivat kasvaa, sopeutua ja pysyä hallitettavina ajan myötä.
Android-kehittäjänä, joka on kohdannut lukuisia arkkitehtuurin haasteita, olen löytänyt, että SOLID-periaatteiden noudattaminen voi muuttaa jopa sekaannukselliset koodipohjat puhdaksi järjestelmiksi. Nämä eivät ole abstrakteja periaatteita, vaan tuloksentekoisia ja toistettavissa olevia tapoja kirjoittaa vankkaa, skaalautuvaa ja ylläpidettävää koodia.
Tämä artikkeli antaa näkymän siihen, miten SOLID-periaatteita voidaan soveltaa Android-kehitykseen käytännön esimerkkien, tekniikoiden ja Meta WhatsApp -tiimin kokemusten kautta.
SOLID-periaatteiden ymmärtäminen
SOLID-periaatteet, jotka Robert C. Martin on ehdottanut, ovat viisi suunnitteluperiaatetta olio-ohjelmoinnille, jotka takaa puhdas ja tehokas ohjelmistojen arkkitehtuuri.
- Yksittäinen vastuu -periaate (SRP): Luokalla tulisi olla vain yksi syy muutokseen.
- Avoin/suljettu -periaate (OCP): Ohjelmistoyksiköt tulisi olla avoimia laajennuksille, mutta suljettuja muutoksille.
- Liskovin korvausperiaate (LSP): Alaluokat tulisi olla korvattavissa emoluokkien kanssa.
- Rajapinnan erottamisperiaate (ISP): Rajapinnat tulisi olla asiakaskohtaisia eikä niiden tulisi pakottaa toteuttamaan käyttämättömiä menetelmiä.
- Riippuvuuden kääntämisperiaate (DIP): Korkean tason moduulit tulisi riippua abstraktioista, ei matalan tason moduuleista.
Integroimalla nämä periaatteet Android-kehitykseen, voimme luoda sovelluksia, jotka ovat helpompia skaalata, testata ja ylläpitää.
Yksittäinen vastuu -periaate (SRP): Vastuiden sujuvoittaminen
Yksittäinen vastuu -periaate on ylläpidettävän koodin kirjoittamisen perusta. Se määrää, että kullakin luokalla tulisi olla vain yksi asia, josta se vastaa. Yleinen vastaperiaate on käsittää Toimintoja tai fragmentteja “jumalaluokiksi”, jotka käsittelevät vastuita alkaen käyttöliittymän renderöinnistä, sitten tietojen hakemisesta, virheiden käsittelystä jne. Tämä lähestymistapa tekee testaamisesta ja ylläpidosta painajaisen.
Yksittäisen vastuu -periaatteen mukaan eri asiat tulisi erottaa eri komponentteihin: esimerkiksi uutissovelluksessa luoda tai lukea uutisia.
class UutisRepository {
fun haeUutiset(): List {
// Käsittelee tietojen hakemisen logiikkaa
}
}
class UutisViewModel(private val uutisRepository: UutisRepository) {
fun lataaUutiset(): LiveData<List> {
// Käsittelee käyttöliittymän tilan ja tietovirran
}
}
class UutisToiminto : AppCompatActivity() {
// Käsittelee vain käyttöliittymän renderöintiä
}
Jokaisella luokalla on vain yksi vastuu; siten se on helppo testata ja muuttaa ilman sivuvaikutuksia.
Nykyisessä Android-kehityksessä SRP toteutetaan usein Jetpackin suositun arkkitehtuurin kanssa. Esimerkiksi logiikka, joka liittyy tietojen manipulointiin, saattaa olla ViewModelissä, kun taas Toiminnot tai fragmentit tulisi vain huolehtia käyttöliittymästä ja vuorovaikutuksista. Tietojen hakeminen saattaa olla siirtynyt erilliseen Repositoryyn, joko paikallisista tietokannoista kuten Room tai verkkokerroksista kuten Retrofit. Tämä vähentää käyttöliittymäluokkien turvotusta, koska kunkin komponentin saa vain yhden vastuun. Samalla koodisi on paljon helpompi testata ja ylläpitää.
Avoin/suljettu -periaate (OCP): Laajennuksen suunnittelu
Avoin/suljettu -periaate määrää, että luokka tulisi olla avoin laajennuksille, mutta suljettu muutoksille. Se on järkevämpää Android-sovelluksille, koska ne kehittyvät jatkuvasti ja lisäävät uusia ominaisuuksia.
Paras esimerkki siitä, miten OCP-periaatetta voidaan soveltaa Android-sovelluksiin, on rajapinnat ja abstraktit luokat. Esimerkiksi:
interface Maksutapa {
fun prosessoiMaksu(määrä: Double)
}
class Luottokorttimaksu : Maksutapa {
override fun prosessoiMaksu(määrä: Double) {
// Luottokorttimaksujen toteutus
}
}
class PayPalMaksu : Maksutapa {
override fun prosessoiMaksu(määrä: Double) {
// PayPal-maksujen toteutus
}
}
Uusien maksutapojen lisääminen ei vaadi muutoksia olemassa oleviin luokkiin; se vaatii uusien luokkien luomista. Tässä järjestelmästä tulee joustava ja skaalautuva.
Sovelluksissa, jotka on luotu Android-laitteille, Avoin/suljettu -periaate on hyvin hyödyllinen, kun on kyse dynaamisista määrityksistä ja konfiguraatioista. Esimerkiksi, jos sovelluksessa on AnalyticsTracker-rajapinta, joka raportoi tapahtumia eri analytiikka-palveluihin, Firebaseen ja Mixpaneliin ja mukautettuihin sisäisiin seurantoihin, jokainen uusi palvelu voidaan lisätä erillisenä luokkana ilman muutoksia olemassa olevaan koodiin. Tämä pitää analytiikkamoduulin avoimena laajennuksille – voit lisätä uusia seurantoja – mutta suljettuna muutoksille: et kirjoita olemassa olevia luokkia uudelleen joka kerta, kun lisäät uuden palvelun.
Liskovin korvausperiaate (LSP): Korvattavuuden varmistaminen
Liskovin korvausperiaate määrää, että alaluokat tulisi olla korvattavissa emoluokkien kanssa, eikä sovelluksen käyttäytyminen saa muuttua. Androidissa tämä periaate on perustava osa uudelleenkäytettävien ja ennustettavien komponenttien suunnittelussa.
Esimerkiksi piirrossovelluksessa:
abstract class Muoto {
abstract fun laskePintaAla(): Double
}
class Suorakulmio(private val leveys: Double, private val korkeus: Double) : Muoto() {
override fun laskePintaAla() = leveys * korkeus
}
class Ympyra(private val säde: Double) : Muoto() {
override fun laskePintaAla() = Math.PI * säde * säde
}
Molemmat Suorakulmio ja Ympyra voidaan korvata toisillaan ilman, että järjestelmä epäonnistuu, mikä tarkoittaa, että järjestelmä on joustava ja noudattaa LSP:ä.
Käytännössä Androidin RecyclerView.Adapter alaluokat. Kukin adapterin alaluokka laajentaa RecyclerView.Adapter<VH> ja ohittaa ydinfunktiot kuten onCreateViewHolder, onBindViewHolder ja getItemCount. RecyclerView voi käyttää mitä tahansa alaluokkaa korvattavasti, kunhan nämä menetelmät on toteutettu oikein eikä riko sovelluksen toimintaa. Tässä LSP on ylläpidetty, ja RecyclerView voi korvata minkä tahansa adapterin alaluokan halutessaan.
Rajapinnan erottamisperiaate (ISP): Ohuet ja keskittyneet rajapinnat
Suuremmissa sovelluksissa on yleistä määritellä rajapintoja, joilla on liian paljon vastuuta, erityisesti verkko- tai tietokantayhteyksien yhteydessä. Sen sijaan tulisi jakaa ne pienempiin, kohdennettuihin rajapintoihin. Esimerkiksi ApiAuth-rajapinta, joka on vastuussa käyttäjän todennusreiteistä, tulisi olla erillinen kuin ApiPosts-rajapinta, joka on vastuussa blogiposteista tai sosiaalisen median syötteistä. Tämä erottelu estää tilanteita, joissa asiakkaat, jotka tarvitsevat vain postien liittyviä menetelmiä, joutuvat riippuvaiseksi ja toteuttamaan todennussoittoja, mikä pitää koodin sekä testikattavuuden ohuena.
Rajapinnan erottamisperiaate tarkoittaa, että sen sijaan, että olisi iso rajapinta, tulisi käyttää useita pienempiä, kohdennettuja rajapintoja. Periaate estää tilanteita, joissa luokat toteuttavat tarpeettomia menetelmiä.
Esimerkiksi, sen sijaan, että olisi yksi iso rajapinta, joka edustaa käyttäjien toimia, kannattaa harkita Kotlin-koodia:
interface Todennus {
fun kirjauduSisään()
fun kirjauduUlos()
}
interface ProfiilinHallinta {
fun päivitäProfiili()
fun poistaTili()
}
Luokat, jotka toteuttavat nämä rajapinnat, voivat keskittyä vain siihen toiminnallisuuteen, jota ne tarvitsevat, mikä puhdistaa koodin ja tekee siitä ylläpidettävämmän.
Riippuvuuden kääntämisperiaate (DIP): Riippuvuuksien abstrahointi
Riippuvuuden kääntämisperiaate edistää irtikytkentää varmistamalla, että korkean tason moduulit riippuvat abstraktioista eikä konkreettisista toteutuksista. Tämä periaate sopii täydellisesti Androidin nykyisiin kehityskäytäntöihin, erityisesti riippuvuuden injektiokehyksillä kuten Dagger ja Hilt.
Esimerkiksi:
class KäyttäjäRepository @Inject constructor(private val apiPalvelu: ApiPalvelu) {
fun haeKäyttäjätiedot() {
// Haetaan käyttäjätietoja abstraktiosta
}
}
Tässä KäyttäjäRepository riippuu abstraktiosta ApiPalvelu, mikä tekee siitä joustavan ja testattavan. Tämä lähestymistapa mahdollistaa toteutuksen korvaamisen, kuten käytön mock-palvelua testauksen aikana.
Kehyket kuten Hilt, Dagger ja Koin helpottavat riippuvuuden injektiota tarjoamalla tavan toimittaa riippuvuuksia Android-komponenteille, poistamalla tarpeen niiden suoran instantiaation. Esimerkiksi repositoryssa ei tulisi instantioida Retrofit-toteutusta suoraan, vaan injektoida abstraktio – esimerkiksi ApiPalvelu-rajapinta. Tällöin voit helposti vaihtaa verkko-toteutuksen – esimerkiksi paikalliseen muistiin perustuvaan mock-palveluun paikallisen testauksen aikana – ja et tarvitse muuttaa mitään repository-koodia. Käytännön sovelluksissa luokat voivat olla annotoitu @Inject tai @Provides tarjoamaan nämä abstraktiot, mikä tekee sovelluksestasi modulaarisen ja testiystävällisen.
SOLID-periaatteiden käytännön hyödyt
SOLID-periaatteiden soveltaminen Android-kehitykseen tuottaa konkreettisia hyötyjä:
- Parannettu testattavuus: Keskittyneet luokat ja rajapinnat tekevät yksikkötestaamisesta helpompaa.
- Parannettu ylläpidettävyys: Selkeä vastuiden erottelu yksinkertaa virheiden etsintää ja päivittämistä.
- Skaalautuvuus: Modulaarinen suunnittelu mahdollistaa uusien ominaisuuksien helpon lisäämisen.
- Yhteistyö: Hyvin järjestetty koodi helpottaa tiimien työskentelyä ja vähentää uusien kehittäjien perehdytysaikaa.
- Suorituskyvyn optimointi: Tehokkaat arkkitehtuurit minimoi tarpeetonta prosessointia ja muistinkäyttöä.
Todelliset sovellukset
Ominaisuusrikkaiden sovellusten, kuten kaupankäynti- tai sosiaalisen median sovellusten, kohdalla SOLID-periaatteiden soveltaminen voi vähentää merkittävästi regressioiden riskiä joka kerta, kun uusi ominaisuus tai palvelu lisätään. Esimerkiksi, jos uusi vaatimus edellyttää sovelluksen sisäistä ostovirtaa, voit esittää erillisen moduulin, joka toteuttaa vaaditut rajapinnat (Maksu, Analytiikka) ilman, että kosket olemassa oleviin moduuleihin. Tällainen modulaarinen lähestymistapa, jota ohjaa SOLID, mahdollistaa Android-sovelluksen nopean sopeutumisen markkinoiden vaatimuksiin ja pitää koodipohjan järjestyksessä ajan myötä.
Työskennellessäsi suurella projektilta, joka vaatii useiden kehittäjien yhteistyötä, on suositeltavaa pitää monimutkainen koodipohja SOLID-periaatteiden mukaisena. Esimerkiksi erottaminen tietojen hakemisesta, liiketoimintalogiikasta ja käyttöliittymän käsittelystä chat-moduulissa auttoi vähentämään regressioiden mahdollisuutta skaalattaessa koodia uusilla ominaisuuksilla. Vastaavasti DIP:n soveltaminen oli ratkaisevaa abstrahoida verkko-operaatiot, jotta voitiin vaihtaa verkko-asiakas ilman suurta häiriötä.
Johtopäätös
Enemmän kuin teoreettinen opas, SOLID-periaatteet ovat käytännön filosofia luotettavan, sopeutuvan ja ylläpidettävän ohjelmistojen luomiseksi. Android-kehityksen nopeasti muuttuvassa maailmassa, jossa vaatimukset muuttuvat lähes yhtä usein kuin teknologiat, näiden periaatteiden noudattaminen tarjoaa vakaan pohjan, jolle menestyksen voidaan perustaa.
Hyvä koodi ei ole vain siitä, että saa jotain toimimaan – se on järjestelmän luominen, joka voi jatkua toimimista ja kasvaa kehittyvien tarpeiden mukana. Omaksumalla SOLID-periaatteet voit kirjoittaa parempaa koodia ja luoda sovelluksia, jotka ovat ilo kehittää, skaalata ja ylläpitää.












