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
| Osa | Vastuu |
|---|---|
| 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ä.
MVC Flask-sovelluksessa
Flask ei pakota mihinkään rakenteeseen, joten työnjako on tehtävä itse. Osat löytyvät kuitenkin luontevasti:
| Osa | Flaskissa | Mitä 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.
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 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
- Model–view–controller (Wikipedia)
- MVC (MDN Web Docs -sanasto)
- MVC-arkkitehtuuri (suomenkielinen Wikipedia)
- The Model View Controller pattern in web applications
- Understanding Model-View-Controller
- Flask-luennon Jinja-osuus
Käyttäjien kommentit