struct Animal* p1=myfactory("cat", "Ofelija");Zadatak generičke tvornice je asocirati simbolički identifikator
cat
s
konkretnim podatkovnim tipom struct Cat
.
Na žalost, izvedbe generičkih tvornica
čvrsto su vezane uz izvedbene detalje programskih jezika
pa ćemo stoga pojedine jezike morati razmatrati ponaosob.
Obavezni dio vježbe uključuje izvedbu u C-u (1.1), te barem jednu od izvedbi u Pythonu (1.2), C++-u (1.3) i Javi (1.4). Naravno, uvijek su dobrodošli i studenti koji riješe više od obaveznog dijela.
1.1. Prvo ćemo pogledati kako bismo generičku tvornicu izveli u C-u. S obzirom da C ima vrlo ograničene mogućnosti introspekcije, jedini donekle portabilni način da taj cilj postignemo jest zapakirati konkretne objekte u dinamičke biblioteke (.dll,.so). Međutim, prije nego što uronimo u detalje, definirajmo naš cilj sljedećim ispitnim izvornim kôdom.
#include "myfactory.h" #include <stdio.h> #include <stdlib.h> typedef char const* (*PTRFUN)(...); struct Animal{ PTRFUN* vptr; // vtable entries: // 0: char const* name(void* this); // 1: char const* greet(); // 2: char const* menu(); }; // parrots and tigers defined in respective dynamic libraries // animalPrintGreeting and animalPrintMenu similar as in lab 1 int main(int argc, char *argv[]){ for (int i=0; i<argc/2; ++i){ struct Animal* p=(struct Animal*)myfactory(argv[1+2*i], argv[1+2*i+1]); if (!p){ printf("Creation of plug-in object %s failed.\n", argv[1+2*i]); continue; } animalPrintGreeting(p); animalPrintMenu(p); free(p); } }Zadatci vježbe su sljedeći:
void* myfactory(char const* libname, char const* ctorarg);Funkcija treba i) otvoriti biblioteku zadanu prvim argumentom (
libname
),
ii) učitati iz nje funkciju create
,
iii) pozvati create sa svojim drugim argumentom
(ctorarg
), te
iv) dobiveni pokazivač vratiti pozivatelju.
Zbog jednostavnosti, funkcija treba pretpostaviti
da se tražena biblioteka nalazi u tekućem kazalu
te dekorirati ime biblioteke tekućim kazalom './'
i standardnom ekstenzijom .so
(UNIX)
ili .dll
(Windows).
Prije pisanja kôda preporučamo proučiti funkcije
dlopen
i dlsym
(UNIX), odnosno
LoadLibrary
i GetProcAddress
(Windows).
Također, provjerite da li u potpunosti razumijete
značenje
deklaracije tipa PTRFUN
.
Neka prototip funkcije bude u myfactory.h
,
a izvedba u myfactory.c
.
animalPrintGreeting
i animalPrintMenu
.
Implementacija će biti vrlo slična kao i u vježbi 1.
Međutim, pripazite da sada do imena ljubimca
dolazimo pozivom metode name
(indeks 0 virtualne tablice).
Sada bi se glavni program trebao moći prevesti
(gcc main.c myfactory.c -ldl
).
Pokretanje dobivene izvršne datoteke treba rezultirati porukom
Creation of plug-in objects failed.
.
parrot.so
i tiger.so
(odnosno parrot.dll
i tiger.dll
na Windowsima).
Izvedba će ponovo biti vrlo slična izvedbama
odgovarajućih funkcija u prvoj laboratorijskoj vježbi.
Izvorni kod treba
i) definirati konkretni tip ljubimca strukturom
koja će osim pokazivača na virtualnu tablicu
imati i pokazivač na ime.
ii) implementirati funkcije ("metode"):
name
, greet
i menu
.
iii) definirati virtualnu tablicu,
te iv) definirati funkciju za stvaranje novih objekata na gomili s prototipom
void* create(char const* name);
Izvorni kôd dinamičkih biblioteka smjestite u datoteke
tiger.c
i parrot.c
te ih prevedite
(za parrot.c
:
gcc -shared -fPIC parrot.c -o parrot.so
)
main.c
, myfactory.c
,
myfactory.h
, parrot.c
, tiger.c
)
nalaze u istom kazalu.
Prevođenje i testiranje tada možete provesti
lijepljenjem sljedećih naredbi u terminal
(naravno, možete napraviti i skriptu).
gcc main.c myfactory.c -ldl gcc -shared -fPIC tiger.c -o tiger.so gcc -shared -fPIC parrot.c -o parrot.so ./a.out tiger mirko parrot modrobradi
gcc -c myfactory.c animal.c g++ main2.c myfactory.o animal.oSada bi se linker trebao pobuniti da ne može razriješiti funkcije koje je preveo gcc. Izmijenite datoteke animal.h i myfactory.h tako da riješite problem. Vaše izmjene trebaju biti takve da sve komponente možemo prevesti i gcc-om, kao i ranije. Pomoć: ključni elementi rješenja su makro __cplusplus i deklaracija extern "C".
1.2. Sada ćemo razmotriti izvedbu generičkih tvornica u Pythonu. Kako vježba ne bi bila prelaka, razmotrit ćemo malo teži problem. Potrebno je napisati program koji će napraviti sljedeće: i) iz svake datoteke izvornog koda u kazalu plugins instancirati po jednog ljubimca, te ii) sve instancirane ljubimce predstaviti načinom glasanja i omiljenim obrokom. Ispitni program bi izgledao ovako:
def test(): pets=[] # obiđi svaku datoteku kazala plugins for mymodule in os.listdir('plugins'): moduleName, moduleExt = os.path.splitext(mymodule) # ako se radi o datoteci s Pythonskim kodom ... if moduleExt=='.py': # instanciraj ljubimca ... ljubimac=myfactory(moduleName)('Ljubimac '+str(len(pets))) # ... i dodaj ga u listu ljubimaca pets.append(ljubimac) # ispiši ljubimce for pet in pets: printGreeting(pet) printMenu(pet)Ispis ispitnog programa treba izgledati ovako:
Ljubimac 0 pozdravlja: Sto mu gromova! Ljubimac 0 voli brazilske orahe. Ljubimac 1 pozdravlja: Mijau! Ljubimac 1 voli mlako mlijeko.Upute:
tiger
i parrot
koje ćeš smjestiti u istoimene module kazala plugins.
Kao i do sada, ljubimci trebaju definirati
konstruktor koji u argumentu prima ime ljubimca,
te metode name
,
greet
i menu
.
Pojedine ljubimce možeš opisati s 9 redaka Pythona.
myfactory
treba koristiti
funkciju import_module
modula importlib
,
te ugrađenu funkciju getattr
.
Za definiciju funkcije dovoljna su 3 retka
uključujući i zaglavlje.
1.3.
U C++-u možemo dinamički stvarati komponente iz dinamičkih biblioteka,
ali tim se ovdje nećemo baviti jer smo to već obradili
u odjeljku koji se odnosi na programski jezik C.
Ovdje ćemo međutim pokazati jednu drugu mogućnost,
a ta je da tvornica stvara objekte konkretnih razreda
koji su prevedeni i ugrađeni u izvršnu datoteku,
ali bez da bude ovisna o njima.
Ključ za ostvarivanje te funkcionalnosti je mogućnost
da lokalne varijable u dosegu datoteke
inicijaliziramo povratnom vrijednošću funkcije
za koju će se prevoditelj pobrinuti da bude pozvana
na samom početku izvđenja programa,
prije ulaska u funkciju main()
.
Naš zadatak će biti analogan zadatku u Pythonu:
proiterirati svim ukompajliranim ljubimcima
te svakog od njih predstaviti načinom glasanja i omiljenim obrokom.
U izvedbi valja koristiti sljedeće pretpostavke.
class Animal{ public: virtual char const* name()=0; virtual char const* greet()=0; virtual char const* menu()=0; };
static void* myCreator(const std::string& arg){ return new Parrot(arg); }
static int hreg=myfactory::instance().registerCreator("parrot", myCreator);
creators_
koje povezuje simbolička imena razreda
s pokazivačima na odgovarajuće konstrukcijske funkcije
registerCreator
create
// myfactory.hpp class myfactory{ public: typedef void* (*pFunCreator)(const std::string&); typedef std::map<std::string, pFunCreator> MyMap; public: static myfactory &instance(); public: int registerCreator(const std::string &id, pFunCreator pfn); public: void *create(const std::string &id, const std::string &arg); std::vector<std::string> getIds(); private: myfactory(); ~myfactory(); myfactory(const myfactory&); MyMap creators_; }; // myfactory.cpp myfactory& myfactory::instance(){ static myfactory theFactory; return theFactory; } // some implementations missing...
void*
pa klijenti tvornice moraju koristiti ružne pretvorbe pokazivača (castove).
Bolje rješenje je moguće izvesti upotrebom predložaka,
a to zainteresiranim studentima ostavljamo za neobaveznu vježbu.
Glavni program može izgledati ovako:
int main(void){ myfactory& fact(myfactory::instance()); std::vector<std::string> vecIds=fact.getIds(); for (int i=0; i<vecIds.size(); ++i){ std::ostringstream oss; oss <<"Ljubimac " <<i; Animal* pa=(Animal*) fact.create(vecIds[i], oss.str()); printGreeting(*pa); printMenu(*pa); delete pa; } }Preporuka: sve datoteke (
main.cpp
, animal.hpp
,
myfactory.cpp
, myfactory.hpp
,
parrot.cpp
, tiger.cpp
)
smjestite u isto kazalo.
U tom slučaju prevođenje i pokretanje dobivamo jednostavno s:
g++ *cpp; ./a.out
Napomena za one koji bi ovako nešto htjeli isprogramirati u C-u.
Na žalost, C-ov standard ne predviđa mogućnost pozivanja
korisničkih funkcija prije funkcije main()
.
Međutim, nema nikakvih načelnih prepreka
da se takva funkcionalnost ostvari,
pa većina prevoditelja za to nudi nestandardna proširenja
(gcc: constructor attribute, msvc: pragma data_seg autostart).
1.4.
Na kraju pogledajmo kako bismo generičku tvornicu mogli ostvariti u Javi.
Pretpostavimo da je definiran apstraktni razred hr.fer.zemris.ooup.lab2.model.Animal
s apstraktnim metodama:
public abstract String name(); public abstract String greet(); public abstract String menu();i konkretnim metodama:
public void animalPrintGreeting() { ... } public void animalPrintMenu() { ... }Pretpostavimo da će korisnik konkretne implementacije razreda
Animal
stavljati u paket hr.fer.zemris.ooup.lab2.model.plugins
i da će
svaka životinja imati konstruktor koji prima jedan string (njezino ime). Primjerice:
public class Parrot extends Animal { private String animalName; public Parrot(String name) { ... } }Potrebno je napisati razred
AnimalFactory
koji ima statičku metodu newInstance
kojom stvara proizvoljnu životinju ali na način da ne postoji compile-time ovisnost, odnosno da se sve razrješava tijekom izvođenja programa:
public class AnimalFactory { public static Animal newInstance(String animalKind, String name) { ... return ...; } }Uz pretpostavku da su
.class
datoteke za navedene životinje dostupne u classpath-u Javinom virtualnom stroju, dinamičko učitavanje novih razreda moguće
je obaviti statičkom metodom forName
razreda Class
:
Class<Animal> clazz = null; clazz = (Class<Animal>)Class.forName("hr.fer.zemris.ooup.lab2.model.plugins."+animalKind);Jednom kad imamo referencu na razred, stvaranje novog primjerka razreda preko defaultnog konstruktora moguće je obaviti pozivom metode
newInstance()
:
Animal animal = (Animal)clazz.newInstance();Međutim, u ovom slučaju to neće raditi jer naše životinje nemaju defaultni konstruktor već konstruktor koji prima jedan argument tipa
String
. Stoga se možemo
osloniti na Java Reflection API, potražiti konstruktor koji prima jedan String
i potom ga pozvati, kako je ilustrirano u nastavku:
Constructor<?> ctr = clazz.getConstructor(String.class); Animal animal = (Animal)ctr.newInstance(name);U slučaju da
.class
datoteke nisu dostupne u classpath-u Javinom virtualnom stroju već su negdje drugdje na disku, stvaranje novih primjeraka uporabom
poziva Class.forName(...)
neće raditi. U tom slučaju potrebno je stvoriti primjerak ClassLoader-objekta kojem će se kao argument dati staza do mjesta
na kojem se nalaze .class
datoteke pa koristiti njegovu metodu loadClass(...)
ili pak inačicu metodeClass.forName
koja prima i referencu
na ClassLoader koji treba koristiti. Evo primjera:
ClassLoader parent = AnimalFactory.class.getClassLoader(); URLClassLoader newClassLoader = new URLClassLoader( new URL[] { // Dodaj jedan direktorij (završava s /) new File("D:/java/plugins/").toURI().toURL(), // Dodaj jedan konkretan JAR (ne završava s /) new File("D:/java/plugins-jarovi/zivotinje.jar").toURI().toURL() }, parent);Sada možemo pisati:
Class<Animal> clazz = (Class<Animal>)newClassLoader.loadClass("hr.fer.zemris.ooup.lab2.model.plugins."+animalKind);ili
Class<Animal> clazz = (Class<Animal>)Class.forName("hr.fer.zemris.ooup.lab2.model.plugins."+animalKind, true, newClassLoader);Međutim, ako se koriste ClassLoader-i, važno je napomenuti da bi naš Factory razred trebao pamtiti referencu na već stvoreni ClassLoader i uvijek koristiti isti ClassLoader za učitavanje iste vrste životinja.
Vidimo da osnovni razred omogućava da grafički podsustav samostalno poziva naš kod za crtanje kad god se za to javi potreba, iako je oblikovan i izveden davno prije naše grafičke komponente. Koji oblikovni obrazac to omogućava?
Vidimo također da naša grafička komponenta preko osnovnog razreda može dobiti informacije o pritisnutim tipkama bez potrebe za čekanjem u radnoj petlji. Koji oblikovni obrazac to omogućava?
Oblikovati grafičku komponentu TextEditor koja će korisnicima omogućiti prikazivanje i jednostavno uređivanje teksta. Vaš zadatak je pratiti položaj kursora, pritiske na tipke, iscrtavati liniju koja predstavlja kursor te iscrtati tekst na površini komponente. Vaša komponenta treba se temeljiti na primitivnim prozorima (npr. JFrame pod Swingom odnosno Tk() pod tkinterom) te ne smije koristiti komponente visoke razine (npr. Text pod tkinterom odnosno JTextArea pod Swingom).
Sve podatke o tekstu kojeg uređujemo, položaju kursora te trenutno označenom dijelu teksta (selekciji) potrebno je enkapsulirati zasebnim razredom TextEditorModel. TextEditor treba sadržavati TextEditorModel. TextEditorModel treba sadržavati sljedeće podatkovne članove:
Isprobajte ispravan rad razvijenih komponenata tako da:
Izmijenite TextEditor na način da nakon svakog pritiska tipaka za pomicanje kursora pozove odgovarajuću metodu razreda TextEditorModel. Također izmijenite metodu za iscrtavanje razreda TextEditor tako da prikazuje položaj kursora kratkom okomitom crtom. Osigurajte da TextEditor dobiva informacije o promjenama položaja kursora bilo da TextEditor naslijedi CursorObserver i prijavi se kao promatrač u svom konstruktoru, bilo da se za to koristi pomoćni (ili još bolje anonimni) razred.
Izmjenite TextEditor tako da pomicanje kursora dok je pritisnuta tipka Shift ažurira aktivnu selekciju u modelu. Također izmijenite metodu za iscrtavanje razreda TextEditor tako da dio teksta koji je označen prikazuje drugačijom pozadinskom bojom. Pozivanjem prethodno definiranih metoda osigurajte da pritisak tipke Backspace briše znak ispred kursora a pritisak tipke Del briše znak iza kursora, ako ne postoji selekcija. Ako postoji selekcija, pritisak bilo koje od tih tipaka briše selektirani tekst.
Ako je ascii vrijednost znaka 10 (tj. Enter), metoda treba redak u kojem je kursor prelomiti na dva retka: prvi čine svi znakovi koji su bili ispred trenutne pozicije kursora a drugi redak čine znakovi koji su bili iza pozicije kursora; time se povećava broj redaka teksta za jedan. Položaj kursora mijenja se tako da odgovara početku retka koji je nastao od znakova koji su bili iza trenutnog položaja kursora.
Obratite pažnju na to da bi od ovog trenutka nadalje sve promjene dokumenta trebalo provoditi na uniformni način kako biste omogućili opozivanje i vraćanje izmjena.
Omogućiti razredu ClipboardStack da bude izdavač informacije o promjenama u clipboardu:
Potom definirajte razred UndoManager čija je struktura sljedeća:
Prođite kroz sve metode modela koje mijenjaju tekst (umeću znakove, brišu znakove) te u svakoj od njih stvorite primjerak razreda izvedenog iz EditAction koji pamti informacije potrebne za provođenje i poništavanje akcije (npr. što se briše, što se dodaje, na kojoj lokaciji itd.). Stvoreni objekt pohranite na stog primjerka UndoManagera.
Modificirajte razred TextEditor tako da podrži sljedeće kombinacije tipaka:
Osigurajte da u programu u svakom trenutku može postojati samo jedan primjerak razreda UndoManager primjenom obrasca Jedinstveni objekt.
File +- Open +- Save +- Exit Edit +- Undo +- Redo +- Cut +- Copy +- Paste +- Paste and Take +- Delete selection +- Clear document Move +- Cursor to document start +- Cursor to document end
Proučite kako se u grafičkoj biblioteci radi s alatnim trakama (u Javi to je razred JToolbar). U prozor na vrh dodajte jednu alatnu traku koja sadrži gumbe Undo, Redo, Cut, Copy, Paste.
Osigurajte da izborničke stavke i gumbi alatne trake pokreću za to predviđene akcije. Također osigurajte da su pojedine izborničke stavke te gumbi alatne trake omogućeni samo kada je to smisleno; primjerice, stavka Cut ili Copy ne smije biti omogućena ako ne postoji selekcija u dokumentu; Undo ne smije biti omogućen ako ne postoji barem jedna naredba na undo stogu a Redo ne smije biti omogućen ako ne postoji barem jedna naredba na redo stogu; Paste ne smije biti omogućen ako je clipboard prazan. Kojim oblikovnim obrascem ovo možete postići?
interface Plugin { String getName(); // ime plugina (za izbornicku stavku) String getDescription(); // kratki opis void execute(TextEditorModel model, UndoManager undoManager, ClipboardStack clipboardStack); }Metoda execute(...) prima reference na sve relevantne razrede kako bi mogla ostvariti proizvoljnu funkcionalnost. Potrebno je napraviti sljedeće pluginove: