Skip to content

Instantly share code, notes, and snippets.

@juvuorin
Last active April 24, 2020 15:42
Show Gist options
  • Save juvuorin/5ed4a07b9369dcf1c969f8d81a9c045c to your computer and use it in GitHub Desktop.
Save juvuorin/5ed4a07b9369dcf1c969f8d81a9c045c to your computer and use it in GitHub Desktop.
koiramainenOhjelmointikisa2020
// Ideal Learning Oy:n käyttöön tarkoitettu esimerkkikoodi. Koodin on tuottanut Riku K
// Haluan tuoda esimerkkiratkaisullani esiin modulaarisen ja geneerisen koodin merkityksen.
// Kategoriat ja kouluaineet voivat ajan saatossa muuttua, kuten sekin, mikä kouluaine kuuluu mihinkin kategoriaan.
// Siispä ohjelmakoodin on hyvä välttää ottamasta kantaa lähdearvoihin silloin kuin se on mahdollista.
// Geneerisyys monimutkaistaa koodia hieman, mutta tekee kokonaisuudesta helpommin ylläpidettävän.
// Tosielämän ratkaisussa kouluaineet ja kategoriat olisiviat omia olioitaan, jolloin niiden välillä voisi helposti olla suora linkki.
// Tätä esimerkkiä varten päädyin kuitenkin yksinkertaistettuun tietorakenteeseen, jotta esimerkki pysyy mahdollisimman selkeänä.
// Tässä esimerkissä oletetaan, että käsitellään vain yksi todistus kerrallaan, eikä aiempia todistuksia tarvitse tallettaa.
/* Alustukset */
let kategoriat;
let kouluaineet;
let kouluaine_kategoria_map;
let arvosanat;
let arvioitavanNimi;
/**
* Funktio alustaa uuden todistuksen tyhjentämällä arvosanat ja asettamalla uuden arvioitavan nimen.
* @param {String} nimi arvioitavan nimi
*/
function luoUusiTodistus(nimi) {
varmistaEiTyhjaMerkkijono(nimi);
arvosanat = [];
arvioitavanNimi = nimi;
}
/* Tiedon esitys */
/**
* Funktio kokoaa todistuksen tulostamiseen tarvittavat funktiot
*/
function tulostaTodistus() {
// Seuraava suoritusrivi on kommentoitu pois tarkoituksella.
// Arvioitavan tiedot voi tulostaa todistukselle poistamalla kommentit seuraavalta riviltä.
//tulostaArvioitavanTiedot();
// Otetaan kategorioiden pituudet talteen
let pituudet = kategoriat.map(k => k.length);
// Luetaan listasta suurin pituus, jota käytetään tulostuvien tietojen tasaamiseen
// Math.max() ottaa syöttöparametrit pilkulla erotettuina
// esim. Math.max(1, 2, 3);
// listasta elementit saa tuotua syöttöparameteriksi lisäämällä listan eteen kolme pistettä
// Voidaan myös kirjoittaa ilman apumuuttujaa:
// Math.max(...kategoriat.map(k => k.length));
let leveys = Math.max(...pituudet);
// Käydään läpi jokainen kategoria ja tulostetaan kategoriaan kertyneiden arvosanojen summat.
kategoriat.forEach(kategoria => tulostaKategoria(kategoria, leveys));
}
/**
* Funktio summaa kategoriaan kertyneen pistemäärän ja tulostaa kategorian nimen ja pistemäärän.
* @param {String} kategoria kategorian nimi
* @param {Integer} leveys sarakkeen leveys
*/
function tulostaKategoria(kategoria, leveys) {
// Tarkistetaan, että pyydetty kategoria on olemassa.
if (!onkoOlemassa(kategoria, kategoriat)) {
// Kategoriaa ei ole olemassa. Nostetaan ohjelman suorituksesta virhe
throw `Kategoriaa '${kategoria}' ei ole olemassa.`;
}
// Alustetaan kategorian arvosanojen summa
let summa = 0;
// Nuolifunktiot toimivat kaikilla nykyaikaisilla selaimilla
// Yhteensopivuusratkaisuna voisi käyttää perinteistä anonyymiä funktiota
arvosanat
// Suodatetaan vain haetun kategorian arvosanat käyttäen suodatusfunktiona nuolifunktiota
// vrt. function(arvosana){ return arvosana.kategoria === kategoria; }
.filter(arvosana => arvosana.kategoria === kategoria)
// Käydään arvosanalista läpi arvosana kerrallaan.
// vrt. function(arvosana){ summa += arvosana.numvero; }
.forEach(arvosana => { summa += arvosana.numero });
tulosta(kategoria.padEnd(leveys + 2) + summa);
}
/**
* Funktio tulostaa arvioitavan tiedot.
* Kun arvioitavan tietojen tulostukseen on erillinen funktio, voidaan sitä kutsua helposti,
* ja näin riittää, että tiedon esittämisen muotoilu tehdään vain yhdessä paikassa.
*/
function tulostaArvioitavanTiedot() {
tulosta("Arvioitava: " + arvioitavanNimi);
}
/**
* Tulostaa rivin. Näin voidaan helposti vaihtaa erilaiseen tulostustapaan ilman, että tarvitsee muuttaa muuta koodia.
* @param {*} rivi (vapaaehtoinen)
*/
function tulosta(rivi) {
// Jos rivi-parametria ei ole annettu, tai jos sen arvo on tyhjä, asetataan rivin arvoksi tyhjä merkkijono ("").
// Muuten määrittämätön tai tyhjä rivi tulostuisi muodossa "undefined" tai "null"
if (rivi === undefined || rivi === null) {
rivi = "<br />";
}
//Suoritetaan varsinainen tulostus
//console.log(rivi);
document.write("<div>" + rivi + "</div>");
}
/* Tiedon tallennus ohjelmaan */
/**
* Funktio lisää kouluaineelle arvioinnin
* @param {String} kouluaine kouluaineen nimi
* @param {Integer} numero aineelle annettu arvosana
*/
function arvioi(kouluaine, numero) {
if (!onkoOlemassa(kouluaine, kouluaineet)) {
// Kouluainetta ei ole olemassa. Nostetaan ohjelman suorituksesta virhe
throw `Kouluainetta '${kouluaine}' ei ole olemassa.`;
}
if (numero < 4 || numero > 10) {
// Numero on sallittujen raja-arvojen ulkopuolella. Nostetaan ohjelman suorituksesta virhe
throw `Virhe syötettäessä kouluainetta '${kouluaine}'. Numeron tulee olla välillä 4-10.`;
}
if (onkoArvioitu(kouluaine)) {
// Kouluaineeseen on jo annettu arvosana. Tässä esimerkissä kouluaineita ei voi uusia, joten vain yksi arvionti ainetta kohden sallitaan.
throw `Kouluaine '${kouluaine}' on jo arvioitu.`;
}
// Lisätään arvosanojen listaan objekti, joka sisältää kouluaineen nimen ja arvosanaan annetun numeron
// Kouluaine voisi hyvin olla olio, joka sisältäisi tiedot kouluaineen nimestä ja kategoriasta.
// Tässä esimerkissä kuitenkin kategoria liitetään suoraan arvosanaan.
arvosanat.push({
kouluaine: kouluaine,
kategoria: haeKategoria(kouluaine),
numero: numero
});
}
/* Apufunktiot */
/**
* @param {String} kouluaine kouluaineen nimi
* @returns palauttaa kouluaineelle liitetyn kategorian
*/
function haeKategoria(kouluaine) {
// Huom! Tämä apufunktio on tarkoitettu vain sisäiseen käyttöön, joten tässä ei itsessään ole virheen käsittelyä.
// Jos kouluaine-parametrin arvoa vastaavaa avainta ei löydy kouluaine_kategoria_map-muuttujasta, funktio ei palauta mitään, vaan nostaa poikkeuksen.
// Voidaan olettaa, että apufunktiota kutstuaan vain jo virhetarkistetun parametrin kera, tai että poikkeuskäsittely on tehty funktion kutsujan toimesta.
return kouluaine_kategoria_map[kouluaine];
}
/**
* @param {*} elementti mikä tahansa haettava asia. Voi olla esimerkiksi merkkijono, kokonaisluku tai vaikka olio
* @param {List} lista lista mitä tahansa elementtejä
* @returns palauttaa true, jos elementti löytyy listasta. Muuten palauttaa false.
*/
function onkoOlemassa(elementti, lista) {
// Array.includes toimii kaikilla nykyaikaisilla selaimilla.
// Yhteensopivuusratkaisuna voisi käyttää Array.indexOf(elementti) -funktiota, joka palauttaa listan kohdan, josta elementti löytyy, tai -1, jos sitä ei löydy.
return lista.includes(elementti);
}
/**
*
* @param {String} kouluaine kouluaineen nimi
* @returns palauttaa true, jos kouluainetta vastaava arviointi on jo olemassa. Muuten palauttaa false.
*/
function onkoArvioitu(kouluaine) {
return arvosanat
// Kerää listan arvosanoista, joiden kouluaine täsmää haettuun kouluaineeseen.
.filter(arvosana => arvosana.kouluaine === kouluaine)
// Tarkastelee kerätyn listan piitta
.length > 0;
}
/**
* Varmistaa, että muuttuja on merkkijono, joka ei ole tyhjä. Nostaa poikkeuksen, mikäli ehdot eivät täyty
* @param {*} arvo
*/
function varmistaEiTyhjaMerkkijono(arvo){
if (typeof arvo !== "string" || arvo === "") {
throw arvo + " ei ole merkkijono.";
}
}
// Seuraavat kolme alustusfunktiota saattavat vaikuttaa turhilta funktioilta; tekeväthän ne vain yhden asian.
// Alustukseen voi kuitenkin myöhemmin tulla myös muita tarpeita, jolloin toiminnallisuuden laajentaminen on helppoa.
/**
* Alustaa kategoriat tyhjällä listalla
*/
function alustaKategoriat() {
kategoriat = [];
}
/**
* Alustaa kouluaineet tyhjällä listalla
*/
function alustaKouluaineet() {
kouluaineet = [];
}
/**
* Alustaa kouluaine_kategoriat_map-kentän tyhjällä oliolla
*/
function alustaKouluaine_kategoriat_map() {
kouluaine_kategoria_map = {};
}
/**
* Lisää kouluaineen ja kategorian, jos niitä ei ole vielä olemassa.
* Määrittää kouluaineelle kategorian. Korvaa aiemmin määritetyn kategorian.
* Nostaa poikkuksen, jos kouluaine on tyhjä tai jos se ei ole merkkijono.
* @param {String} kouluaine
*/
function lisaaKouluaine(kouluaine, kategoria) {
varmistaEiTyhjaMerkkijono(kouluaine);
varmistaEiTyhjaMerkkijono(kategoria);
if(!onkoOlemassa(kouluaine, kouluaineet)){
kouluaineet.push(kouluaine);
}
if(!onkoOlemassa(kategoria, kategoriat)){
kategoriat.push(kategoria);
}
kouluaine_kategoria_map[kouluaine] = kategoria;
}
try {
/* Ohjelman alustus */
alustaKategoriat();
alustaKouluaineet();
alustaKouluaine_kategoriat_map();
// Ohjelman tiedot voidaan kerätä käyttäjäsyötteellä tai ulkoisesta tietopalvelusta.
lisaaKouluaine("Pupun jäljestys", "Metsästys");
lisaaKouluaine("Hirven jäljestys", "Metsästys");
lisaaKouluaine("Linnun noutaminen", "Metsästys");
lisaaKouluaine("Lumen pöllyytys", "Pihatyöt");
lisaaKouluaine("Kukkapenkkien kaivaminen", "Pihatyöt");
lisaaKouluaine("Parvekkeen vahtiminen", "Muut");
lisaaKouluaine("Piilotetun luun löytäminen", "Muut");
lisaaKouluaine("Oman hännän jahtaaminen", "Muut");
lisaaKouluaine("Kuun ulvonta", "Muut");
/* Ohjelman ajo */
luoUusiTodistus("Selma");
// Todistuksen tiedot voidaan kerätä käyttäjäsyöttellä tai ulkoisesta tietopalvelusta.
arvioi("Pupun jäljestys", 6);
arvioi("Hirven jäljestys", 8);
arvioi("Linnun noutaminen", 10);
arvioi("Lumen pöllyytys", 7);
arvioi("Kukkapenkkien kaivaminen", 6);
arvioi("Parvekkeen vahtiminen", 9);
arvioi("Piilotetun luun löytäminen", 10);
arvioi("Oman hännän jahtaaminen", 8);
arvioi("Kuun ulvonta", 9);
tulostaTodistus();
tulosta();
} catch (ex){
// Tulostetaan ohjelman suorituksessa ilmentynyt virhe
tulosta(ex);
}
@juvuorin
Copy link
Author

Otettu geneerisyys huomioon, mikä kertoo, että asioita on ajateltu koodin ylläpidettävyyden kannalta! Hyvä! Pohdittu myös yhteensopivuutta eri JavaScript versioiden välillä, mikä kertoo, että tekijä tuntee suoritusympäristön. Tässä vastauksessa korostuu ns. rajapinta-ajattelu - tekijä tarjoaa moduulin käyttäjälle palvelua, kuten tekijä kommenteissaan mainitseekin.

Ongelmaa on lähestytty tilaperusteisesti - todistusta voidaan muuttaa ja siihen voidaan yrittää lisätä uusia arvosanoja. Jos aine on jo arvosteltu, arvosanaa ei voida lisätä jne.

Tämäkin on oikein hyvä ratkaisu ja kertoo tekijän taidosta laatia käytettävä rajapinta toisen kehittäjän käyttöön, mikä on tärkeä taito sekin!
Erinomaista on myös, että funktioon tullessa tarkastetaan syötteen oikeellisuus esim. varmistaEiTyhjaMerkkijono(kouluaine) - se on vikasietoisen rajapinnan toteutuksen perusedellytys. Hyvä!

Kehitysajatuksia

Voisiko yhden funktiokutsun sisältävän funktion korvata itse funktiokutsulla (esim. includes), mutta ilmeisesti tekijä on halunnut tehdä suomenkielisen rajapinnan, joten tämä tuskin on ongelma tekijälle.

Tsemppiä ohjelmointihommiin!

@rkuusisto
Copy link

Erittäin hyvä havainto tuo yhden rivin funktio. Tässä tapauksessa se on noin by design.

Valitsin tämän lähestymistavan, jotta jo funktiokutsusta selviää, mitä ollaan tekemässä. Toisaalta ajattelen tässä modulaarisuutta. Jos pitäisikin tehdä lisätarkistuksia includes-kutsun lisäksi, on mahdolliset lisäykset ja muokkaukset helppo tehdä funktion sisään sen sijaan, että tarvitsisi jokaista käyttöpaikka kohden tehdä muutos.

Koodi siis toimisi aivan samoin, vaikka tuon onkoOlemassa-funktion sijaan kutsuisikin suoraan includes-funktiota.

@juvuorin
Copy link
Author

juvuorin commented Apr 24, 2020

Toimisi kyllä vaan! Ja ihan totta, että samaan funktioon olisi kätevää sisällyttää muutakin tarkistusta.

Tuon tyyppiturvattomuuden takia JavaScript on haastava kieli tarjota ohjelmointirajapintoja. Funktioon voi tulla mitä tahansa ja miten paljon tahansa. Parametrien määrällä ja laadulla ei ole rajoitteita, joten niitä joudutaan tarkistelemaan. On myös vähän onnetonta, että usein rajapinnat eivät oikein tahdo "dokumentoitua", koska tyyppejä ei tarvitse määritellä. Se tosin ei johdu rajapinnan laatijasta, vaan on kielen itsensä ominaisuus.

Voisiko filter - forEach yhdistelmän korvata reducella? Tutkisi reducelle annetussa funktiossa, onko kategoria oikea ja jos olisi, niin palauttaisi summa+arvosana ja jos ei, niin palauttaisi vaan summa. Summa olisi siis reducen näkökulmasta ns. akkumulaattori (acc). Samalla vältettäisiin tuo väliaikainen summa-muuttuja ja koko hoidon voisi heittää lambda-lausekkeena siihen lausekkeeseen, missä sitä tarvitaan eli tulosta(kategoria.padEnd(leveys + 2) + summa); tuohon summan tilalle. Menisikö vaikeampilukuiseksi?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment