previous next Title Contents Index

21. Oikeellisuustarkistukset ja avustukset


Mitä tässä luvussa käsitellään?

* yksinkertainen tarkistus

* muita vaihtoehtoja tarkistusten tekemiseen

* funktio- osoittimet

21.1 Miksi tarkistukset

Oikeassa ohjelmassa on todella tärkeää, että syöttötietojen oikeellisuus tarkistetaan. Aina on lähdettävä siitä oletuksesta, että käyttäjät ovat täysiä idiootteja (älkää kertoko tätä mahdollisille käyttäjille). Toisaalta idioottitarkistuksia ei saa olla liikaa. Esimerkiksi "Oletko varma" - tyyliset kysymykset rupeavat ennen pitkää ärsyttämään kokenutta käyttäjää.

Hyvä ohjelma olisi muutettavissa käyttäjän mukaan. Aloittelijalle enemmän ohjeita ja varmistuskysymyksiä ja kokenut käyttäjä saa itse vastata tekosistaan.

Eri kielten toiminta eri tietotyyppejä luettaessa vaihtelee. Pascal - ohjelma kaatuu mikäli numeeriseen tietoon vastataan merkkitietoa. Mikä olisikaan harmillisempaa kuin se, että sihteeri on naputellut tietoja koko päivän (EIKÄ OLE TALLETTANUT NIITÄ VÄLILLÄ!???) ja iltapäivän väsymyksessä vastaa kysymykseen

	Jäsenmaksu>kymppi
ja koko ohjelman suoritus loppuu!

C- kieli on huomattavasti siedettävämpi numeerisen tiedon luvussa. scanf ei vain muuta merkkitietoa numeeriseksi ja näin tavallisen scanf- funktion käyttö on aivan suotavaa numeerisen tiedon lukemiseen. Rivin lukematta jäänyt (mahdollisesti virheellinen) osa on sitten syytä poistaa (fflush).

C- kielen huono puoli on siinä, että vastaavasti merkkijonojen lukeminen standardifunktioilla on vaarallista. Näiden lukemiseen pitääkin lähes poikkeuksetta tehdä oma aliohjelma (tai käyttää sopivaa kirjastoa). Aina on ihmisiä, jotka sanovat: "Laita jonon pituudeksi 200, niin ei kukaan jaksa kirjoittaa niin pitkää vastausta!". Näinhän se on, mutta monestiko itselläsi on manuaali tai jokin muu esine jäänyt nojaamaan näppäimistöön ja näin auto- repeat - toiminto työntää määrättömästi merkkejä?

Todellisissa ohjelmissa voidaan näppäimistön lukua tehdä merkki kerrallaan aliohjelmilla, joille on tarkkaan kerrottu syötön muoto ja sen aikana sallitut näppäimet. Tällöin mahdollisista virhepainalluksista voidaan piipata tai niihin voidaan reagoida muuten. Samoin kentälle varatun koon ylittyessä voidaan heti reagoida. Tämä kuitenkin vaatii puskuroimatonta syötön käsittelyä (ohjelmien siirrettävyys kärsii) ja hieman lisää vaivaa.

21.2 Lukeminen rivi kerrallaan

Eräs suhteellisen vaivaton tapa käsitellä syöttöä on lukea syöttö aina merkkijonona. Tämän jälkeen voidaan syöttörivi käsitellä palanen (tai strtok) tyyppisillä aliohjelmilla tai C++:n merkkijonotietovirralla osa kerrallaan. Kustakin palasesta sitten otetaan tarvittava syöttö esimerkiksi C- kielen sscanf- funktiolla, joka toimii aivan scanf- funktiota vastaavasti:
	jono <= "52 mk"
	if ( sscanf(jono,"%d",&hinta)<1 ) /* virhe.... */;

21.3 Tarkistusaliohjelmat

Kutakin ohjelmassa esiintyvää tietotyyppiä kohden voidaan kirjoittaa tarkistusfunktiot, jotka tarkistavat parametrina tuodun tiedon oikeellisuuden ja sitten silmukassa luetaan tietoa kunnes ko. funktio hyväksyy tiedon:
	string hetu;
	...
	do {
	  printf("Sosiaaliturvatunnus>");
	  getline(cin,hetu,'\n');
	} while ( tarkista_hetu(hetu) )
	...
	Henkilötunnus>1234[RET]
	Hetu väärin! Anna uudelleen!
	Henkilötunnus>020347- 123T[RET]
	... 
Tässä tapauksessa aliohjelma voi myös tulostaa virheilmoituksen, jolloin kutsuvan ohjelman ainoa tehtävä on lukea kunnes tulee oikea vastaus.

21.4 Kerhorekisterin tarkistukset?

Entä kerhon jäsenrekisteri. Ohjelma on jo kirjoitettu hyvin pitkälle, eikä tarkistuksiin ole puututtu juuri lainkaan. Merkkijonoina lukemisen ansiosta ohjelmaa ei voida kaataa väärillä syöttötiedoilla, mutta rekisterin kannalta virheellisiä tietoja, kuten samoja nimiä ja sotuja, voi esiintyä useita, numeerisiksi tarkoitetuissa kentissä voi olla kirjaimia jne. Osa ominaisuuksista voi olla toivottujakin.

Mutta entäpä jos tiettyjä tarkistuksia haluttaisiin kuitenkin tehdä. Pitääkö koko ohjelma kirjoittaa uusiksi?

Ei! Kriittinen on tietenkin vain kysy_tiedot. Mikäli aliohjelma on toteutettu ilman silmukkaa, kysymällä kukin tieto omalla lauseellaan, niin siitä sitten vaan lisäämään tarkistus kunkin lukemisen ympärille. Tämä on aivan hyvä ratkaisu, mikäli kenttiä on vähän tai kentät ovat eri tietotyyppiä.

	int cNaytto::kysy_tiedot(cJasen &jasen)
	{
	. . .
	  do {
	    cout << "Hetu          >"; getline(cin,apujasen.hetu,'\n');
	    poista_tyhjat(apujasen.hetu);
	    if ( apujasen.hetu == "q" ) return 1;
	  } while ( tarkista_hetu(apujasen.hetu) );
	. . .
	 do {
	   cout << "Jäsenmaksu mk >"; cin.getline(N_S(jono),'\n');
	   if ( strcmp(jono,"q") == 0 ) return 1;
	   apujasen.jmaksu = jono_doubleksi(jono,"%lf");
	 } while ( (apujasen.jmaksu < 0) || ( 500 < apujasen.jmaksu ) );
Voiko tarkista_hetu edes selvitä tarkistuksista? Osasta voi, mutta entäpä jos esimerkiksi hetu pitää olla erisuuri kullekin jäsenelle. Aliohjelma ei tiedä mihin hetuja verrattaisiin! Siis aliohjelmalle pitäisi parametrina viedä tietysti myös käsiteltävä kerho tai ainakin jäsenistö.

21.4.1 grep

Näin käy useasti! Myöhemmin huomataan jonkin aliohjelman vaativan lisää parametreja ja niitä joudutaan jälkeenpäin lisäämään. Lisätään parametri! Samalla metodille kysy_tiedot kannattaa välittää tieto siitä, onko kyseessä lisäys vaiko päivitys (korjailu).

Entä kuka kutsui aliohjelmaa. Jokaiseen vastaavaan paikkaan täytyy myös tietysti lisätä kutsuparametri.

Ohjelmointiympäristöjen mukana tulee usein apuohjelma nimeltä grep. Ohjelmalla voidaan etsiä sanoja (tai tiettyä hakuehtoa) valitusta joukosta tiedostoja. Esimerkiksi

	E:\KURSSIT\CPP\KERHO\TARKISTU.4>grep - n+ kysy_tiedot *.cpp *.h
	File NAYTTO.CPP:
	359     int cNaytto::kysy_tiedot(cJasen &jasen)
	426           if ( kysy_tiedot(jasen) != 0 ) return;
	File NAYTTO.H:
	32        int  kysy_tiedot(cJasen &jasen);
	E:\KURSSIT\CPP\KERHO\TARKISTU.4>
Kun saamme listan kaikista esiintymistä, muutetaan tarvittavat kohdat. Päivitetään myös kommentoinnin muutos- osaan, että metodin parametrien määrä on muuttunut, jottei joku muu saman kirjaston käyttäjä sitten ihmettele liian kauan sitä, miksi aliohjelma ei enää toimi. Voidaan myös kirjoittaa aivan uusi aliohjelma eri nimelle (esim. kysy_ja_tark_tiedot).

21.5 Sijoittaminen tarkistaa

Jos tietojen kysyminen hoidetaan silmukassa, voitaisiin tarkistus jättää sijoittamisen huoleksi. Eli jos sijoittaminen ei jäsenen mielestä onnistu, palautetaan tästä tieto. Paluutietoja voisi olla useita, eli osa olisi varoitusluonteisia ja osa tiedon uudelleen kysymistä vaativia. Osa taas vaatii lisätarkistuksia. Esimerkiksi hetu. Jäsenhän ei tiedä mitä muita jäseniä on, joten se ei myöskään voi tarkistaa hetusta muuta kuin muodollisen oikeellisuuden.
	cNaytto::kysy_kentta(int k,cJasen &jasen)
	. . . 
	  virhe = jasen.sijoita(k,jono);
	
	  switch ( virhe ) {
	    case KENTTA_OK:
	      return TOIM_SEURAAVA;
	
	    case KENTTA_VAROITUS:
	      cout << virhe.Virhe() << endl;
	      return TOIM_SEURAAVA;
	
	    case KENTTA_UUDELLEEN:
	    case KENTTA_MUUTETTU_KYSY:
	      cout << "Tarkista ja anna tieto uudelleen" << endl;
	      return TOIM_KYSY_UUDELLEEN;
	
	    case KENTTA_OK_ONKO_AINOA:
	      lkm = kerho- >Jasenet().laske_montako_muuta(jasen,k,kuka);
	      if ( lkm == 0 ) return TOIM_SEURAAVA;
	      kerho- >Jasenet().anna(kuka).tulosta(cout);
	      cout << "On jo ennestään!" << endl;
	      jasen.sijoita(k,edell);
	      return TOIM_KYSY_UUDELLEEN;
	
	    case KENTTA_OK_VAROITA_MUUT:
	      lkm = kerho- >Jasenet().laske_montako_muuta(jasen,k,kuka);
	      if ( lkm == 0 ) return TOIM_SEURAAVA;
	      kerho- >Jasenet().anna(kuka).tulosta(cout);
	      if ( kylla_kysymys("On jo, lisätäänkö silti uusi!") )
	        return TOIM_SEURAAVA;
	      jasen.sijoita(k,edell);
	      return TOIM_KYSY_UUDELLEEN;
	
	    default: ;
	  } // virhe
	. . .  
	int cNaytto::kysy_tiedot(cJasen &jasen)
	{
	. . . kaille kentille
	  do {
	     virhe = kysy_kentta(int k,cJasen &jasen)
	  } while ( virhe != TOIM_SEURAAVA );

21.5.1 sijoita

Yksinkertaisessa versiossa cJasen voi tarkistaa tiedon järkevyyden sijoituksen yhteydessä.
	int cJasen::sijoita(int k,const string &st)
	{
	  switch ( k ) {
	    case  0: ... return KENTTA_OK;
	...
	    case  2: // Hetu
	      if ( tarkista_hetu(st) ) return KENTTA_UUDELLEEN;
	      hetu = st;
	      return KENTTA_OK_ONKO_AINOA;
	...
	    case  9: 
	      if ( sscanf(st.c_str(),"%d"&liittymisvuosi) != 1 ) return KENTTA_UUDELLEEN;
	      if ( liittymisvuosi < 1950 ) return KENTTA_UUDELLEEN;
	      if ( liittymisvuosi > 1998 ) return KENTTA_UUDELLEEN;
	      return KENTTA_OK;
	...
	    default:  return KENTTA_OK; // Vääriä kenttiä ei sijoiteta mutta ne kelp.
	  }
	}
	

21.6 Funktio- osoitin

Usein esimerkiksi matemaattisissa tehtävissä tulee vastaan tilanne, missä jollekin funktiolle tehty aliohjelma kelpaisi tekemään saman homman myös jollekin toiselle funktiolle, mikäli funktio pystyttäisiin vaihtamaan. Tyypillisiä tällaisia esimerkkejä ovat numeerinen derivointi ja integrointi, nollakohdan hakeminen, kuvaajan piirtäminen jne.

Esimerkiksi funktion integrointi suorakaidesäännöllä voitaisiin hoitaa seuraavasti:

integroi.c - esimerkki numeerisesta integroinnista

	#include <stdlib.h>
	#include <stdio.h>
	#include <math.h>
	
	double integroi(double x1, double x2, int tiheys)
	{
	  double x,dx,summa=0;
	
	  dx = (x2- x1)/tiheys;
	
	  for (x=x1+dx/2 ; x<x2; x+=dx) 
	    summa += sin(x)*dx;
	
	  return summa;
	}
	
	int main(void)
	{
	  printf("Integraali sin(x) väliltä [0,pi] on noin %7.5lf\n",
	          integroi(0,M_PI,100));
	  return 0;
	}
Entäpä mikäli haluaisimme integroida vaikka ex. No vaihdetaan sin(x) tilalle exp(x)! Entäpä jos tarvitsemme samassa ohjelmassa sekä sin(x) että exp(x) integraalit? Kirjoitammeko integroi_sin ja integroi_exp. Ei kuulosta järkevältä!

Otamme käyttöön funktio- osoittimet. Määritellään aluksi funktio_tyyppi

	typedef double (*funktio_tyyppi)(double x); 
Sitten voimme esitellä tarpeellisen määrän vastaavaa tyyppiä olevia funktiota. Itse integrointi muutetaan käyttämään yhtä "ylimääräistä" parametria; funktiota jota integroidaan:

integro2.c - esimerkki funktiosta parametrina

	double oma_funktio(double x)
	{
	  return 2*x- 5;
	}
	
	double integroi(funktio_tyyppi f,double x1, double x2, int tiheys)
	{
	  double x,dx,summa=0;
	
	  dx = (x2- x1)/tiheys;
	
	  for (x=x1+dx/2 ; x<x2; x+=dx) 
	    summa += f(x)*dx;
	
	  return summa;
	}
Nyt voimme kutsua esimerkiksi:
	double ifx;
	...
	ifx = integroi(oma_funktio,0,5,100);
	...
	ifx = integroi(sin,0,M_PI,1000);
	...
	ifx = integroi(exp,0,1,500);
	... 
Huomautus! Hienot adaptiiviset integrointimenetelmät muuttavat itse välin tiheyttä funktion käyttäytymisen mukaan.

Tehtävä 21.172 Minimointi

Kirjoita funktioaliohjelma min_fun, jolla voidaan etsiä parametrina annetun funktion minimi väliltä [x1,x2]. Väliä käydään läpi tarkkuudella dx.
Kirjoita aliohjelmat ja pääohjelma, jolla lasketaan funktioiden
f(x) = x2 + 2 ja f(x) = sin2(x- 3)
minimit väliltä [- 2,3] tarkkuudella 0.01.

21.7 Tarkistus- funktio - osoitin

Joissakin tapauksissa tarkistus voitaisiin hoitaa myös siten, että jäseneltä kysytään minkälaisella funktiolla oikeellisuus tarkistetaan:
	/* Funktio- osoitin tarkistusfunktioon:                                      */
	typedef int (*Tarkistus_funktio)(Tarkistus_tyyppi *, string &);
	. . .
int cNaytto::kysy_tiedot(cJasen &jasen)
{ Tarkistus_funktio tark; . . . kaikille kentille do { . . . kysy kenttä => jono tark = jasen.Tarkistus(k); virhe = 0; if ( tark != NULL ) virhe = tark(jono); } while ( virhe ); . . . }

Edellä totesimme, että tarkista_hetu(kerho,jono) ei ehkä riitäkään. Miksi? Jos lisäämme uutta nimeä, niin asia on aivan oikein. Mutta entäpä mikäli korjaamme sotua ja kirjoitamme vastaukseksi täsmälleen saman tekstin kuin alkuperäinenkin. Eikö alkuperäinen hetu tällöin löydy? Löytyypä hyvinkin. Siis tarkista_hetu "hermostuu" ja tulostaa virheilmoituksen:

	Sotu esiintyy ennestään 
ja vaatii uutta syöttöä!

Siis parametrit eivät riitä! Mitä pitää lisätä? Tästä ajattelutavasta saattaa seurata loputon kasa uusia parametreja. Kasataan kaikki tarkistusten tarvitsemat parametrit yhteen ainoaan tietueeseen Tarkistus_tyyppi ja kutsutaan tarkistusfunktioita muodossa:

	t_sotu(tarkistus_tiedot,jono); 

Vastaavasti tietysti tarkistusfunktio-osoittimien kanssa

	typedef int (*Tarkistus_funktio)(Tarkistus_tyyppi &, string &);
	. . .
	int cNaytto::kysy_tiedot(cJasen &jasen)
	{
	  Tarkistus_funktio tark;
	  Tarkistus_tyyppi tiedot(...
	. . .
	    if ( tark != NULL ) virhe = tark(tiedot,jono);
	. . .

21.7.1 kerhotar.cpp

Itse tarkistusfunktiot kirjoitetaan vaikkapa tiedostoon kerhotar.cpp

21.7.2 pvm.c

Esimerkiksi päivämäärän tarkistukseen tarvittavat rutiinit ovat lähes valmiina aikaisemmin kirjoitetuissa aliohjelmissa. Nämä voitaisiin kasata tiedostoiksi pvm.h ja pvm.c.

21.7.3 Uusien tarkistusfunktioiden lisäys

Aluksi loogisen toimivuuden tarkistamiseksi riittää tietysti kirjoittaa vain aliohjelma tark_ok. Muut edellä esitetyt voidaan lisätä myöhemmin vaikkapa yksi kerrallaan.

Kun uusi tarkistusfunktio kirjoitetaan, pitää nyt * kirjoittaa se tiedostoon kerhotar.cpp

* osoite lisätä tarvittavaan kohtaan metodissa cJasen::Tarkistus(int k)

* lisätä funktion prototyyppi tiedostoon kerhotar.h

21.8 Etsiminen

Nimi tai hetu piti tarkistaa siten, että samaa ei saa esiintyä, mutta toisaalta kohdalla olevan jäsenen tiedot eivät saa aiheuttaa virheilmoitusta. Tämä voidaan hoitaa esimerkiksi seuraavalla aliohjelmalla:
	int cJasenet::laske_montako_muuta(const cJasen &jasen, int k,int &kuka) const
	// Lasketaan monnellako muulla kerholaisella on sama tieto kentässä k
	// kuin jasenella.  Palautetaan muiden maara ja sijoitetaan muuttujaan
	// kuka viimeinen sellainen jolla oli sama tieto
	{
	  int i,samoja=0;
	  string kentta = jasen.kentta_jonoksi(k);
	  for (i=0; i<lkm; i++) {
	    if ( alkiot[i]- >kentta_jonoksi(k) == kentta &&
	         alkiot[i]- >sama_rekisteri(jasen) == 0 ) {
	      samoja++;
	      kuka = i;
	    }
	  }
	  return samoja;
	}
Kohdalla olevan jäsenen tiedot voidaan välttää tarkistamalla ettei rekisterinumero ole sama (alkiot[i]- >sama_rekisteri(jasen)).

21.9 cKentta ja perintä

Malliohjelmassa tarkistukset on tehty hieman edellä kuvatulla tavalla, paitsi että jäsenen kentät eivät olekaan merkkijonoja tai reaalilukuja, vaan yleisestä cKentta luokasta perittyjä kenttä- luokkia, joista jokainen tietää itse miten ko. kenttä tulee käsitellä (yksikäsitteinen nimi, puhelinnumero, jossa vain numeroita jne...). Nämä luokat hoitavat sitten itse merkkijonosijoitukset, tiedon ottamisen tietovirrasta, oikeellisuustarkistukset jne.

Tekniikan etuna on se, että ajan oloon kertyy kattava määrä erilaisia kenttä- luokkia ja seuraava ohjelma voidaan kasata vain valitsemalla mitä luokkia tarvitaan:

	class cJasen {
	  cIntKentta         tunnus_nro;
	  cNimi1Kentta       nimi;
	  cHetu1Kentta       hetu;
	  cJono1isoksiKentta katuosoite;
	  cPostinumeroKentta postinumero;
	  cJonoIsoksiKentta  postiosoite;
	  cPuhKentta         kotipuhelin;
	  cPuhKentta         tyopuhelin;
	  cPuhKentta         autopuhelin;
	  cIntKentta         liittymisvuosi;
	  cDoubleKentta      jmaksu;
	  cDoubleKentta      maksu;
	  cJonoKentta        lisatietoja;
	...

21.10 Avustus

Avustusta tarvitaan kahdenlaista:

1. Yleistä avustusta, jossa mielellään voidaan selata ohjelman eri kohtien toimintaa. Tämä on suhteellisen helppo toteuttaa yleisesti.

2. Erityistä avustusta kutakin toimenpidettä kohti. Tällaisen sisältöriippuvan avustuksen (context sensitive help) tekeminen vaatii ohjelmaan lisäyksiä mm. kunkin eri kentän lukurutiinin toimintoihin.

Kirjoitamme yleiskäyttöisen aliohjelmakirjaston help.c, jolla molemmat edellä mainitut ominaisuudet voidaan toteuttaa. Koska käytännössä avustustietoutta on aina liian vähän ja siinä on kirjoitusvirheitä, pyrimme sijoittamaan avustuksen omaksi tiedostokseen, josta help.c- kirjaston on sitä helppo lukea. Olkoon avustustiedoston muoto vaikkapa seuraava:

tarkistu.4\kerho.hlp - avustustiedosto

	[?] 
	  ? = Avustuksen avustus
	  ======================
	  ?- merkillä saa yleensä joka paikassa avustusta!
	  Jos avustuksessa kysytään aihetta, josta avustusta halutaan,
	  voidaan vastata esimerkiksi:
	....
	[Lisäys]
	  Lisäys
	  ======
	
	  Lisäystoiminolla lisätään uusia henkilöitä.  Lisättävä henkilö
	...
	  Katso myös: Tietojen syöttö, Asetukset
	
	[Tietojen syöttö]
	  Tietojen syöttö
	  ===============
	
	  Tietoja syötettäessä näytössä näkyy suluissa arvo, joka tulee kentän
	...
	[t_sotu]#
	  Sotuksi kelpaa...
	...
	[SISÄLLYS]
	  Sisällysluettelo:
	  =================
	  ? 
	  Lisäys
	  Etsiminen
	... 
Nyt esimerkiksi kutsulla
	help(NULL); 
päästäisiin avustuksen sisällysluetteloon, josta sitten käyttäjä voi tarvittaessa siirtyä haluamaansa kohtaan vaikka kirjoittamalla Tie*.

Sisältöriippuvassa avustuksessa kutsuttaisiin sitten suoraan haluttua kohtaa, esimerkiksi:

	help("[Lisäys]"); 
Helpoimmin tämä kävisi esimerkiksi lisäämällä kysy_kentta - aliohjelmaan kutsu funktioon: char *avustus(int nro), jonka mukaan avustusta pyydettäisiin halutusta kentästä jos käyttäjä painaisi?.

Toisaalta avustusta tarvitaan ehkä mieluumminkin kentän tyypin mukaan, ei niinkään itse kentän mukaan (koska "Jäsenen nimi>" tai "Sotu>" sinänsä ovat jo itse selittäviä). Tällöin teemme tarkistusfunktion perusteella löytyvän avustuksen tarkistus_nimi(t_funk), joka palauttaa vastaavan nimen (esim. t_sotu =>"t_sotu"). Näin kysy_kentta voi funktion perusteella saada selville tarkistusfunktiota vastaavan nimen ja nimen perusteella voi kutsua avustusta. Avustukseen on kirjoitettu valmis funktio help_aihe(nimi), joka lisää sulut []nimen ympärille ("t_sotu" =>"[t_sotu]").

Toteutamme tämän vasta ohjelman viimeisessä versiossa.


previous next Title Contents Index