Luku 3.5: Kaksiosainen tehtävä: Undo-Redo (100 + 200p)
Tästä sivusta:
Mitä käsitellään? Tutustutaan Command-suunnittelumalliin.
Mitä tehdään? Toteutetaan ominaisuus jota voi soveltaa omaan projektiaiheeseen.
Pistearvo: 100+200p
Bonus: +5 % omista pisteistä aikaisesta palautuksesta.
Tehtävä palautetaan kahdessa osassa
- Tehtävän ensimmäisessä osassa (100p) koodataan ja testataan luokka
RemoveAction
.- Tehtävän toisessa osassa (200p) koodataan ja testataan luokka
DeleteTimeAction
** Huom!**: Toinen osa on huomattavasti työläämpi ja sen tekemisessä kannattaa miettiä pidempään ennen kuin aloittaa koodaamaan.
Yleisimmät ohiluetut kohdat tehtävänannossa
Näiden ymmärtäminen voi vaatia tehtävän lukemista ensin loppuun...
- Ei huomata että poistettavan alueen molemmat päät kuuluvat poistettavaan alueeseen.
- Metodille
Split
annettu ajanhetki kuuluu "poisleikattuun alueeseen", eli myös tuo aika poistetaan VideoSectionista jota leikataan.- Luokalla
TimeCode
on käteviä metodeja.- Metodia
split
ei koskaan pidä käyttää jollei sitä tarvita.
Intro
Suuressa osassa ohjelmia, joissa käsitellään jotakin dataa (kuvia, tekstiä, taulukoita) on mahdollista sekä tehdä dataan muutoksia, että perua muutoksia jälkeenpäin. Kutsumme tässä tätä toiminnallisuutta undo-redo -mekanismiksi.
Tapoja toteuttaa undo-redo on useita. Joissakin toteutuksissa tallennetaan kaikki muutettu data, joissakin vain muuttuneet osat. Meidän kannaltamme mielenkiintoista on kuinka tämän voi tehdä ohjelmassamme tyylikkäästi ja riippumatta siitä kuinka suuri muutos on tai minnepäin ohjelmamme tilaa se vaikuttaa.
Tyypillinen tapa toteuttaa Undo-Redo-mekanismi olio-pohjaisissa järjestelmissä perustuu Komento -suunnittelumalliin (Command design pattern). Tässä harjoituksessa rakennamme undo-redo mekanismin komento-suunnittelumallia käyttäen kuvitteelliseen videoeditointi-ohjelmaan.
Command Pattern - Komento-suunnittelumalli
Komento-mallissa ideana on ottaa jokin suoritettava asia ja "kääriä sen ympärille" olio. Komentojen ulkoinen rajapinta määritellään rajapinnan tai abstraktin luokan kautta. Tällöin komentoja suorittava osa ohjelmasta voi suorittaa ne tietämättä komentojen sisältöä. (Polymorfinen metodikutsu) Komento-mallilla voidaan toteuttaa mm.
- Undo-redo mekanismi
- Valikoiden toiminnallisuus käyttöliittymissä
- Makrojen nauhoittaminen
- jne...
Esimerkki
Yksinkertaisimmassa versiossa yliluokassa on vain yksittäinen metodi, jota kutsumalla jo aiemmin aseteltu toiminnallisuus saadaan aikaan. Esimerkissämme tämä metodi on nimeltään execute.
class Command(object):
def execute()
Luokka DrawLineCommand
on malliesimerkki luokasta joka täyttää em. yliluokan. Luokan ilmentymä saa parametrit ennalta määrättyä tehtäväänsä
varten konstruktorin parametreina. Tämä ei kuitenkaan ole komento-suunnittelumallin vaatimus - olennaista on vain että olion tila asetetaan valmiiksi
komennon suoritusta varten joka tapahtuu vasta kun execute-metodi suoritetaan.
class DrawLineCommand(Command):
def __init__(self, draw_here, x1, y1, x2, y2):
self.drawing_area = draw_here
self.startX = x1
self.startY = y1
self.endX = x2
self.endY = y2
def execute(self):
self.drawing_area.draw_line(x1, y1, x2, y2)
Lopuksi katsotaan kuinka komentoja luodaan ja suoritetaan. Tässä suoritetaan komentosarja joka piirtää täytetyn neliön. Huomaa viivanpiirtokomentojen
joukossa oleva FillCommand
, ja se ettei se muuta for-silmukkaa, jossa komentoja suoritetaan. Esimerkissämme arvot olivat kiinteitä, mutta
todellisuudessa arvot komennolle voisivat tulla vaikka käyttäjän hiiren klikkailuista ja tällä tavoin saisi rakennettua vaikka makron jolla voi
piirtää neliöitä.
# koodia
do_these_later.add(DrawLineCommand(this_pic, 0, 0, 15, 0) )
do_these_later.add(DrawLineCommand(this_pic, 15, 0, 15, 15) )
do_these_later.add(DrawLineCommand(this_pic, 15, 15, 0, 15) )
do_these_later.add(DrawLineCommand(this_pic, 0, 15, 0, 0) )
do_these_later.add(FillCommand(this_pic, 10, 10, color.RED) )
# koodia
# koodia
for this_command in do_these_later:
this_command.execute()
# koodia
Komento-suunnittelumalli ja Undo-mekanismi
Komento-suunnittelumallin käyttö Undo-Redo mekanismin toteutuksessa lähtee siitä että yhden execute-metodin sijaan laitetaankin yliluokkaan kaksi "execute-metodia": Undo ja Redo.
Redo on käytännössä pitkälti toiminnaltaan sama kuin perusmallin execute-metodi. Undo-toimintoa varten joudutaan yleensä tallentamaan hieman enemmän informaatiota. Pystynet arvaamaan miten allaoleva koodi toimii.
class RenameCommand(Command):
def __init__(self, person, new_name, old_name):
self.person = person
self.newName = new_name
self.oldName = old_name
def undo(self):
self.person.set_name(old_name)
def redo(self):
self.person.set_name(new_name)
Viimeinen osa mekanismia on komentojen käsittely. Komentoja varten on kaksi pinoa. Kun komento suoritetaan se laitetaan undo-pinoon. Kun tehdään undo, komento otetaan undo-pinosta, suoritetaan undo-metodi ja laitetaan se redo-pinoon. Redo-operaatio toimii päinvastoin.
Jos joku tekee jotakin uutta kun redo-pinossa on komentoja, ne täytyy yleensä heittää pois, koska ei voida taata edes että oliot joita komennot käsittelevät olisivat enää olemassa.
Tehtäväpohjan kuvaus
Tässä tehtävässä muokataan kuvitteellista videoeditoriohjelmaa. Editoriohjelmassamme on yksi videoraita (VideoTrack
), joka sisältää videopätkiä (VideoSection
). VideoTrack sisältää seuraavat metodit, joita kutsumalla raidan sisältöä voi muuttaa:
def add_section(self, section)
# Lisää annetun videopätkän raidalle heti viimeisen pätkän perään.
def remove_section(self, section)
# Poistaa annetun videopätkän raidalta. Tämä voi olla mikä tahansa raidalla oleva pätkä.
# removeSection jättää raidalle ajallisesti tyhjän kohdan.
def delete_time(self, start, end)
# Poistaa raidalta kaiken annettujen ajanhetkien väliltä.
# Annetut alku ja loppuhetket sisältyvät poistettavaan alueeseen.
Vaatimukset toteutukselle
- Ei None:eja: VideoTrack:in sisältämä lista ei koskaan sisällä None-alkioita ja sen sisältämät videopätkät ovat aina kronologisessa järjestyksessä. Videopätkien väliin saa jäädä aikaa jolloin materiaalia ei ole. Toisinsanoen, videomateriaali ei liiku raidalla ajallisesti minnekään.
- Ei luoda explisiittisesti uusia olioita: Toimintojen on tarkoitus käsitellä vain jo olemassaolevia (esim testissä luotuja) ja
VideoSection.split
-metodin luomia olioita. Toisin sanoen: LuokistaRemoveAction
jaDeleteTimeAction
ei ole tarkoitus kutsuaVideoSection
-luokan konstruktoria suoraan.
FAQ: Selvennystä ja vinkkejä tehtävää 2.3 koskien (30.1)
- Tehtävässä 2.3 videopätkiä täytyy tarvittaessa jakaa komennolla split ja liittää yhteen komennolla join. Split-komennon antamat videopätkien nimet ovat sellaisenaan oikeat - itse asiassa sectioneiden nimiä ei voi edes muuttaa kuin tilanteessa jossa takaisin kokoon liitettävälle alueelle annetaan vanha nimi takaisin join-komennon yhteydessä.
- Pohdi tarkkaan mistä kohdasta pätkät jaetaan tarvittaessa osiin - poistettavan alueen molempien päiden täytyy kuulua poistettuun alueeseen. Tutustu myös luokkaan TimeCode.
Esimerkki
Alkutila
Esimerkkimme alkutilassa videoraidalla (track) on kaksi pätkää.
- Näytös1 (00:00:00 - 00:02:39)
- Näytös2 (00:02:40 - 00:06:39)
list : [naytos1, naytos2]
Lisätään videopätkä
Luodaanpa nyt uusi videopätkä nimeltään "Näytös3", jonka pituus on 00:02:20. Kun suoritetaan VideoTrack-luokan metodi add_section(example) ilman aikaparametria tulee uusi pätkä lisätyksi raidan loppuun. Tällöin lisätään myös lisäysoperaatiota kuvaava olio UndoManager-luokan peruttavien operaatioiden jonoon metodilla add_action(). (lisää tämä koodi kaikkiin operaatioihin)
# VideoSection example:
example = VideoSection("Näytös3", TimeCode(0,0,0), TimeCode(0,2,20))
track.add_section(example)
- Näytös1 (00:00:00 - 00:02:39)
- Näytös2 (00:02:40 - 00:06:39)
- Näytös3 (00:06:40 - 00:09:10)
list : [naytos1, naytos2, naytos3]
Undo
Jos nyt kutsutaan trackin undo-toimintoa, palataan alkutilaan:
track.undo()
list : [naytos1, naytos2]
Redo
Jos nyt taas redo-metodia niin päästään tilaan jossa pätkiä on kolme.
track.redo()
list : [naytos1, naytos2, naytos3]
Poistetaan pätkä
Käytetään seuraavaksi trackin metodia delete_time
. Lopputulos on tässä himpun verran monimutkaisempi kuin edellisissä tapauksissa, sillä jotta
vain haluttu osuus poistettaisiin täytyy jotkin pätkät pilkkoa osiin. Tähän löytyy metodi split
VideoSection luokasta, sekä vastakkainen metodi
join
, jolla pätkiä voi yhdistää. Split irroittaa halutusta pätkästä loppuosan ja lyhentää alkuperäistä pätkää. Loppuosa ei automaattisesti palaa
VideoTrack:ille.
HUOM! Split-metodia tulee käyttää vain jos se todella jakaa pätkän kahteen osaan. Tarkista siis tilanne ennenkuin toimit.
Jos pilkotut loppuosat laittaisi takaisin VideoTrack:ille (kuten tehtävän vinkeissäkin neuvottiin) lopputulos näyttäisi tältä:
track.delete_time(TimeCode(0,1,20), TimeCode(0,7,0))
Kun harmaalla merkitty alue poistuu näyttää lopputulos tältä:
list : [naytos1-1, naytos3-2]
Undo
Undo palauttaa meidät jälleen tilaan jossa pätkiä on kolme. Huomaa että alueet ovat taas yhtenäisiä ja niillä on alkuperäiset nimet.
track.undo()
list : [naytos1, naytos2, naytos3]
Undo
Seuraava undo palauttaa meidät vielä pidemmälle.
track.undo()
list : [naytos1, naytos2]
Tehtävänanto
Valmistautuminen
- Katso ensin luokkaa UndoManager. Huomaa kuinka UndoManager käsittelee UndoableAction luokan olioita vaikka se ei tiedä lopullisesta toteutuksesta mitään. Huomaa myös että Undomanager ja UndoableAction ovat täysin sovellusalueesta (videoeditori) riippumattomia luokkia, joten niitä voitaisiin soveltaa jossakin toisessa projektissa.
- Käy nyt katsomassa luokkia video.VideoTrack, video.VideoSection ja undo.AddAction. Tutki mitä tapahtuu kun VideoTrack-olion metodia add-section kutsutaan. Katso myös (vaikka debuggerilla) mitä tapahtuu kun kutsutaan metodeja undo ja redo.
Toteutus ja testaus
Toteutetaan ja testataan undo-redo kahdelle uudelle toiminnolle.
VideoTrack luokan metodit remove_section
ja delete_time
luovat ja käyttävät luokkien RemoveAction
ja DeleteTimeAction
toimintoja. Käy lukemassa niiden kuvaukset. Toteuta osatehtävässä 1 tämän perusteella luokka RemoveAction
ja osatehtävässa 2 luokka
DeleteTimeAction
loppuun.
Huom!
- Näiden toimintojen on tarkoitus käsitellä mahdollisuuksien rajoissa vain jo olemassaolevia ja VideoSection.split-metodin luomia
olioita. Toisin sanoen: Luokista
RemoveAction
jaDeleteTimeAction
ei ole tarkoitus kutsua VideoSection-luokan alustusmetodia suoraan. - Split-komennolla ei tule yrittää jakaa videopätkiä jos tarvetta jakamiseen ei ole. t.s. kohdassa on jo muutenkin kahden pätkän raja.
Testiluokan runkoon on jo toteutettu kaksi testiä malliksi. Ensimmäinen testaa onnistuiko videopätkän (VideoSection) lisääminen raidalle. Toinen testi testaa redo-toimintoa tilanteessa jossa yhtään uudelleensuoritettavaa komentoa ei pitäisi olla. Tarkoitus on lisätä joukko testejä, jotka testaavat kaikki koodin rivit ja suorittavat if/for/while jne lauseiden molemmat haarat. ts. jokaisen if-lauseen tulisi olla testatessa ainakin kerran tosi ja ainakin kerran epätosi.
Riittää, että testaat kattavasti vain luokan RemoveAction
(osatehtävä 1) ja DeleteTimeAction
(osatehtävä 2). Muuta ei tarvitse testata.
Heti kun olet luonut itsellesi Eclipseen projektin, voit ajaa pohjassa annetut testit. Molempien pitäisi mennä läpi.
Valmiiksi annettu koodi
Tässä tehtävässä on siinä määrin paljon koodia, että saat koodin zip-paketissa undoredo.zip.
Paketti joka sisältää tehtävässä tarvittavat luokat. Tämän paketin saat helposti Eclipseen luomalla ensin tyhjän PyDev-projektin, minkä jäkeen voit valita
File->Import->General->Archive file
Etsi sitten tämä paketti, aseta kansioksi luomasi projekti ja paina Finish.
Voit halutessasi purkaa ylläolevan paketin itse tai ladata tiedostot yksitellen. (alla)
Toimintaohjeet
Lue molemmat PyUnit-ohjeet, mikäli et vielä ole lukenut niitä. Halutessasi tee Eclipseen projekti jossa PyUnit:ia on helppo suorittaa.
Osatehtävän 1 palautus
Palauta A+:ssa luokat:
- undo/remove_action.py
- tests/test.py (Älä käytä DeleteTimeAction:a tässä testissä)
Osatehtävän 2 palautus
Palauta A+:ssa luokat:
- undo/delete_time_action.py
- tests/test.py (Älä käytä RemoveAction:ia tässä testissä)
Huom!
Muutoksia voi vielä tulla.