Model-view-controller (MVC) -arkkitehtuuri

Www-sovelluksissa sotketaan usein keskenään ohjelmakoodi ja selaimessa näkyvä HTML-koodi. Hiemankaan suuremmissa ohjelmissa tämä vaikeuttaa koodin pitämistä selkeänä ja tekee ohjelmasta virhealttiin. Toimintatapaa näkee erityisesti PHP-tyyppisissä ympäristöissä, joissa HTML-koodia tulostetaan selaimelle sitä mukaa kun suoritus etenee: ohjelmassa tulostetaan sopivaa HTML-koodia tarpeen mukaan, ja näin muodostetaan lopullinen dokumentti. Dokumentin eri osia on silloin hankala muuttaa eri aikaan, ja lopputuloksena on entistäkin sekavampaa ohjelmakoodia.

Parempi ratkaisu on pitää erillään vähintään ohjelmakoodi ja HTML-koodi. Kun HTML tuotetaan template-moottorilla, ohjelman rakenne selkiytyy ja yksinkertaistuu samaan tapaan kuin HTML:n ja CSS:n eriyttämisessä. Sovellusta voi selkiyttää edelleen erottamalla toisistaan ne osat, jotka käsittelevät tietoa, ja sen osan, joka tuottaa lopullisen dokumentin. Päälogiikka tuottaa sopivasti käsiteltyä ja järjesteltyä tietoa, jonka toinen ohjelman osa muuttaa dokumentiksi — tai joksikin aivan muuksi. Käyttöliittymän vaihtaminen on silloin helppoa.

Kolme osaa

MVC-arkkitehtuurin osat
OsaVastuu
Malli (model) Sovelluksen tieto ja siihen liittyvät säännöt: mikä on kelvollinen arvo, miten tietoa haetaan, suodatetaan ja lasketaan. Malli ei tiedä mitään siitä, miltä tieto näyttää käyttäjälle.
Ohjain (controller) Vastaanottaa käyttäjältä tulevat pyynnöt, tarkistaa syötteet, kutsuu mallia ja valitsee näkymän. Sisältää sovelluksen kulkua koskevan logiikan mutta ei esitysasua eikä varsinaisia tietosääntöjä.
Näkymä (view) Määrittää, miltä tiedot näyttävät. Yksinkertaisimmillaan näkymä on HTML-dokumentti tai template. Näkymä ei hae tietoja itse eikä muuta niitä.

Olennaista on, että malli ja näkymä eivät ole suoraan yhteydessä toisiinsa. Ohjain on niiden välissä: se hakee tiedot mallilta ja antaa ne näkymälle valmiiksi käsiteltyinä.

Kaavio: selain lähettää HTTP-pyynnön ohjaimelle. Ohjain pyytää mallilta            tiedot ja saa ne takaisin. Ohjain antaa tiedot näkymälle, joka muodostaa            valmiin dokumentin ja palauttaa sen selaimelle. Mallin ja näkymän välillä ei            ole yhteyttä.
Pyynnön kulku MVC-mallin mukaisessa sovelluksessa.

MVC Flask-sovelluksessa

Flask ei pakota mihinkään rakenteeseen, joten työnjako on tehtävä itse. Osat löytyvät kuitenkin luontevasti:

OsaFlaskissaMitä sinne kuuluu
Malli Omat funktiot, luokat ja moduulit Tietorakenteet, niiden luominen ja muokkaaminen, kelpoisuussäännöt ja laskenta
Ohjain @app.route-funktio Parametrien lukeminen ja tarkistaminen, mallin kutsuminen, statuskoodin ja näkymän valinta, uudelleenohjaukset
Näkymä Jinja-template Merkkaus, silmukat listojen yli, yksinkertaiset ehdot ja muotoilufiltterit

Näin ei kannata tehdä. Alla oleva funktio on yhtä aikaa ohjain, malli ja näkymä:

@app.route("/kurssit")
def kurssit():
    html = "<h1>Kurssit</h1><ul>"
    for kurssi in hae_kurssit():
        if kurssi["op"] >= 5:
            html += "<li>" + kurssi["nimi"] + "</li>"
    return html + "</ul>"

Koodi on jo tässä vaikealukuista, mutta pahempaa on se, mitä siitä puuttuu: kurssin nimeä ei koodata mitenkään, joten yksikin &-merkki nimessä rikkoo dokumentin. Ulkoasun muuttaminen tarkoittaa Python-koodin muokkaamista, eikä samaa tietoa voi esittää toisessa muodossa kirjoittamatta kaikkea uudelleen.

Sama toiminnallisuus osiin jaettuna. Malli tietää kursseista ja niiden säännöistä:

def hae_kurssit():
    """Palauttaa kaikki kurssit."""
    ...

def laajat_kurssit(kurssit, vahintaan=5):
    """Suodattaa kurssit opintopisteiden perusteella."""
    return [kurssi for kurssi in kurssit if kurssi["op"] >= vahintaan]

Ohjain lukee pyynnön, kutsuu mallia ja valitsee näkymän. Se ei tiedä HTML:stä mitään:

@app.route("/kurssit")
def kurssit():
    raja = request.args.get("op", 5, type=int)
    kurssit = laajat_kurssit(hae_kurssit(), raja)
    return render_template("kurssit.html", kurssit=kurssit, raja=raja)

Näkymä päättää, miltä tiedot näyttävät:

{% extends "base.html" %}

{% block sisalto %}
  <h1>Kurssit, vähintään {{ raja }} op</h1>
  <ul>
    {% for kurssi in kurssit %}
      <li>{{ kurssi.nimi }} ({{ kurssi.op }} op)</li>
    {% else %}
      <li>Ei kursseja.</li>
    {% endfor %}
  </ul>
{% endblock %}

Nyt ulkoasun voi vaihtaa koskematta Python-koodiin, Jinjan autoescape hoitaa erikoismerkit, ja suodatussäännön voi testata ilman www-palvelinta. Sama ohjain voi myös valita eri näkymän eri tilanteessa — esimerkiksi palauttaa JSONia rajapintakutsulle ja sivun selaimelle.

Nyrkkisääntö

Tee Python-koodissa kaikki mahdollisimman valmiiksi ja pidä template yksinkertaisena. Jos templateen ilmestyy laskentaa, monimutkaisia ehtoja tai tiedon suodatusta, se kuuluu mallin tai ohjaimen puolelle. Toisaalta jos Python-koodiin ilmestyy kulmasulkeita, se kuuluu templateen.

Mistä mallin tieto tulee?

Tässä vaiheessa kurssia malli on yksinkertaisimmillaan joukko funktioita ja tietorakenteita: listoja, sanakirjoja tai omia luokkia. Myöhemmin kurssilla mallin rinnalle tulee pysyvä tallennus eli tietokanta.

Työnjako ei silloin muutu. Tietokantakyselyt kuuluvat malliin, eivät reittifunktioon eivätkä varsinkaan templateen. Ohjain saa mallilta valmiiksi käsitellyn tuloksen eikä tiedä, tuliko se muistista, tiedostosta vai tietokannasta. Juuri tämä tekee mallin vaihtamisesta myöhemmin helppoa.

Nimien sekaannus

Kirjallisuudessa ja sovelluskehyksissä samat sanat tarkoittavat eri asioita, ja tämä hämmentää lähes jokaista MVC:hen tutustuvaa.

Flaskin "view function" ei ole näkymä

Flaskin dokumentaatio kutsuu @app.route-dekoraattorilla merkittyä funktiota nimellä view function. MVC-mielessä se on kuitenkin ohjain, ei näkymä — näkymä on Jinja-template. Nimitys on peräisin kehyksen omasta sanastosta, ei MVC-mallista.

Vastaavia eroja on muuallakin: osa kehyksistä antaa näkymän tarkkailla mallin muutoksia suoraan, kun taas toisissa ohjain hoitaa kaiken päivittämisen. MVC:stä on myös muunnelmia, kuten MVP (model–view–presenter) ja MVVM (model–view–viewmodel), joissa osien rajat kulkevat hieman eri kohdissa.

Älä siis jumiudu liikaa miettimään, mikä osa on tarkalleen mikäkin missäkin sovelluksessa. Tärkeämpää on pohtia, mikä on järkevä tehtävänjako sovelluksen osien välillä ja miten ohjelman saa pidettyä selkeänä ja hyvin ylläpidettävänä. Monet sovelluskehykset ohjaavat automaattisesti oikeaan suuntaan.

Lisätietoa

Käyttäjien kommentit

Kommentoi Lisää kommentti