Osztályokat a DEFCLASS függvénnyel lehet definiálni:
A szülő-osztály neve, slot-leírás és osztály opciók közül bármelyik elhagyható.
A slot az attribútumok és metódusok összefoglaló neve. A slot-leírások (slot-név slot-opció*) formájúak, ahol minden opció egy kulcsszó, melyet név, kifejezés vagy egyéb követ. A legfontosabb slot-opciók:
:ACCESSOR | definiál egy függvényt ami írja és olvassa is a slot-ot |
:INITFORM | alapértelmezett érték megadása |
:INITARG | az osztály létrehozásakor a kezdeti érték felüldefiniálható |
:READER | definiál egy függvényt ami kiolvassa a slot-ot |
:WRITER | definiál egy függvényt ami írja a slot-ot |
:TYPE | Slot típusa adható meg vele |
:DOCUMENTATION | slot dokumentációja |
A legfontosabb osztály-opciók:
:DEFAULT-INITARGS | az ősosztályból örökölt initform érték felüldefiniálása |
:DOCUMENTATION | osztály dokumentációja |
A DEFSTRUCT hasonló a DEFCLASS-hoz. A szintaxis kissé más, és a programozónak nagyobb kontrollja van a funkciók elnevezése felett. Példaként nézzük a DEFSTRUCT következő alkalmazását:
A DEFSTRUCT automatikusan definiál slotokat: kezdőértékeket számítókat, PERSON-NAME jellegűeket a slot-értékek beállításához és kiolvasásához, és egy MAKE-PERSON nevűt, mely az inicializást végzi a megadott argumentumokkal (initargs), például:
Ehhez hasonló működés létrehozása DEFCLASS kulcsszóval:
A DEFCLASS alkalmazásával a programozó határozhatja meg, mit hogy hívjanak. Például, nem kell a kiegészítőt PERSON-NAME -nek hívni, lehet egyszerűen csak NAME-nek.
A DESCRIBE OSZTÁLY függvény alkalmazásával információhoz juthatunk az osztály felépítéséről és további paramétereiről.
Nem kötelező az összes opció beállítása minden slot számára. Esetleg nem akarunk egy slotot inicializálni a MAKE-INSTANCE (l. később) hívásakor. Ebben az esetben, nem kell :INITARG-ot írni. Avagy lehet, hogy nincs értelme az alapértelmezett értéknek, mert például az értékeket mindig a leszármazott osztályoknál akarjuk beírni. Ekkor hagyjuk el az :INITFORM-ot.
A CLOS-ban az osztályok is objektumok, melyekre a létrehozás, megszüntetés, manipuláció stb. műveletei vannak definiálva.
A struktúráknak és az osztályoknak egyaránt vannak mezői és a mezőinek értékei. Osztályok esetében a mezőket azonban a Lisp terminológia szerint slotoknak nevezzük.
Ha egy struktúrát definiálunk a Lisp-ben, ahhoz több funkció automatikusan generálódik (pl. példánylétrehozó, típuslekérdező függvény stb.), osztályt alkalmazva azonban több dolgot kell a programozónak magának megadnia.
Az osztályok definiálása a következő minta szerint történik:
Példa:
Legyen a computer-article az article osztály leszármazottja:
Fontos különbség az osztályok és a struktúrák között, hogy az osztályoknál a MAKE-INSTANCE kulcsszóval és az osztály nevének megadásával ehet új példányt létrehozni, és nem egy speciális nevű konstruktor alkalmazásával. A minta a következő:
A LISP és a CLOS funkcionális nyelv, tehát a matematikai függvényfogalomra épít. Ez nyomot hagy az objektumelvű részeken is. Az objektumelvű imperatív nyelvekkel szemben (pl. Smalltalk, C++, Java), a CLOS a multimetódus koncepciót követi. A metódusok nem egy konkrét osztályhoz tartoznak, hanem azokhoz, melyek hívását metódus argumentumai lehetővé teszik. Melyik hajtódik végre az azonos metódusok közül? Az, melynek argumentumai illeszthetők az "aktuális paraméterekre", és az illeszthetők közül a legspecifikusabb argumentumlistával rendelkezik. (Aciklikus osztályhierarchia esetén balról jobbra haladva egyértelmű metódus precedencia sorrendet kapunk, ciklikus esetben is van kiszámítási szabály, az már kissé bonyolultabb.)
Osztály példánya a MAKE-INSTANCE kulcsszóval hozható létre. Ez hasonló a DEFSTRUCT-hoz tarozó MAKE-x függvényekhez, de a példányosítandó osztályt itt argumentumként lehet megadni.
Az osztály objektum alkalmazása helyett, a nevét is meg lehet adni. Például:
Ennek a person objektumnak 100 lesz az age "mezőjében", a neve BILL lesz, mert ez az alapértelmezett (az osztály definícióját l. fent)
Sokszor a MAKE-INSTANCE közvetlen meghívása helyett célszerű saját konstruktort definiálni, mert így elrejthetők az implementációs részletek és nem kell mindenre kulcsszó paramétert használni.
Így a név és a kor kulcsszóparaméterből pozícionális paraméterré változott.
A kiegészítő (accessor) függvények a slot-értékek olvasására és írására alkalmazhatók.
DEFCLASS használata esetén, a példányokat #<.> formában írja ki az értelmező, nem #s(person :name jill :age 100) jelölésmódot használva. Ezt azonban meg lehet változtatni a PRINT-OBJECT általános függvény használatával.
A slotok a nevükön keresztül is elérhetők a (SLOT-VALUE példány slot-név) kifejezésen keresztül.
Sok hasznos dolgot megtudhatunk egy példányról a DESCRIBE meghívásával:
Az eddigi osztályoknak nem volt szülőosztálya. Ezért volt "()" a "defclass person" után. Valójában ez azt jelenti, hogy a STANDARD-OBJECT nevű osztály a szülője.
Ahol van szülőosztály, a leszármazott definiálhat olyan slotot, amit már a szülő osztály definiált. Ekkor a két információt össze kell egyeztetni. A következő slot opcióknál vagy uniózás vagy felüldefiniálás történik:
:ACCESSOR | definiál egy függvényt ami írja és olvassa is a slot-ot |
:INITFORM | alapértelemzett érték megadása |
:INITARG | az osztály létrehozásakor a kezdeti érték felüldefiniálható |
:READER | definiál egy függvényt ami kiolvassa a slot-ot |
:WRITER | definiál egy függvényt ami írja a slot-ot |
:TYPE | Slot típusa adható meg vele |
:DOCUMENTATION | slot dokumentációja |
Végülis ezt várja az ember. A leszármazott meg tudja változtatni az alapértelmezett kezdőértéket az :INITFORM felüldefiniálásával, és inicializáló argumentumokat (initarg) és kiegészítőket (accessor) adhat hozzá. Mindazonáltal, a kiegészítők (accessor) uniója az általános függvények működésmódjának következménye. Ha egy általános függvény alkalmazható a C osztály példányára, akkor alkalmazható a C leszármazottjainak példányára is.
(A kiegészítő (accessor) függvények általános függvények.)
Példa az öröklésre:
A #<...> jelölésnek általában következő a formája:
Tehát a maths-teacher osztály példányai #<MATHS-TEACHER ...> formátumban íródnak ki. A fenti osztályok jelölése megmutatja, hogy a STANDARD-CLASS példányai. A DEFCLASS a standard osztályokat definiálja, a DEFSTRUCT a struktúra osztályokat.
A LISP és a CLOS funkcionális nyelv, tehát a matematikai függvényfogalomra épít. Ez nyomot hagy az objektumorientált részeken is. Az imperatív objektumorientált nyelvekkel szemben (pl. Smalltalk, C++, Java), a CLOS a generikus függvények módszerét alkalmazza. Ennek lényege, hogy a metódusok nem egy konkrét osztályhoz tartoznak, hanem azokhoz, melyek hívását a metódus argumentumai lehetővé teszik.
A generikus függvények elvéhez hasonló példákat a mindennapi életben is megfigyelhetünk, hiszen bármilyen tevékenység végre hajtása erősen függ attól, hogy milyen tárgyon hajtjuk végre:
Ugyanezt az elvet alkalmazhatjuk a programozás esetében is. Tegyük fel, hogy síkbeli alakzatok rajzolásához a következő struktúratípusokat definiáljuk:
Ha ezekhez területszámító eljárást akarunk írni, azt első közelítésben megtehetjük a következő módon:
Bár ez a megoldás tökéletesen működik, gondjaink lesznek vele, amint egyre több síkbeli alakzat területét akarjuk kiszámolni. Ha sok alakzatunk van, a feltételes elágazás több oldalon keresztül fog folytatódni, egyre áttekinthetetlenebb lesz, és így például az is előfordulhat, hogy valamit újra felveszünk az elágazásba, pedig azt már egy korábbi feltételnél lekezeltük. A többoldalas elágazást megpróbálhatjuk új eljárások definiálásával kivédeni:
De mivel ebben az esetben mindig emlékeznünk kéne az eljárások nevére, ezért célszerű újból definiálni az általános AREA függvényt:
Így az AREA függvény kezelhető méretű darabokra van osztva, és maga az area függvény meghívja az adott alakzathoz éppen aktuális területszámító függvényt.
Amire igazán szükségünk lenne, az egy olyan automatikus mechanizmus, mely meg tudná találni az alakzat típusának megfelelő AREA függvényt. Habár ezt meg lehet oldani manuálisan is — mint tettük ezt a fenti példában —, de egy automatikus megoldás jóval kényelmesebb lenne.
Szerencsére lehetőségünk van a definíciók kisebb darabokra bontására, melyek a nekik megfelelő paramétertípus esetén hajtódnak végre. A darabokat a CLOS-ban metódusoknak, az azonos nevű metódusok gyűjteményét pedig generikus függvénynek hívjuk.
A metódusokat az eljárásokhoz hasonló módon definiáljuk, két dolgot kivéve: először is a DEFMETHOD kulcsszót használjuk a DEFUN helyett, másodszor a paraméterlistában megadjuk, hogy a metódus milyen adattípusra vonatkozik:
Megjegyezzük, hogy mindegyik AREA metódusnak ugyanaz a neve. A három metódus együttesen alkotja az AREA generikus függvényt.
Felhívjuk a figyelmet arra, hogy a hagyományos függvénydefinícióhoz (l. defun-os példa) képest megváltozott a paraméter: olyan kifejezést kell megadni, mely a paraméter nevén kívül azt is meghatározza, hogy az adott metódust milyen típus esetén lehet alkalmazni. A (figure triangle) a TRIANGLE adattípust használja, ezt paraméterspecializációnak nevezzük.
A programozásban két megközelítést különböztethetünk meg. Az egyik, az adatvezérelt perspektíva azon az elven alapul, hogy az argumentumok típusainak kombinációja határozza meg, hogy melyik eljárást hajtsuk végre. Az adatvezérelt perspektívából nézve az AREA metódusok egy családja, és az AREA argumentuma határozza meg, hogy ezek közül pontosan melyik metódust használjuk.
A másik megközelítés, az objektumorientált perspektíva arra épül, hogy az adattípusokat attribútumok és eljárások csomagjaként képzelhetjük el. Az objektumorientált perspektívából, a síkbeli alakzatok típusához (pl. TRIANGLE, RECTANGLE, és CIRCLE) tartoznak azok a metódusok, melyek összességükben az AREA és egyéb generikus függvényeket alkotják.
A területszámító AREA számára az objektumorientált perspektíva tűnik természetesebbnek, mert az AREA metódusoknak csak egy paramétere van. Több paraméter esetében gyakran az adatvezérelt perspektíva a gyümölcsözőbb megközelítési forma.
Megjegyezzük, hogy a generikus függvények és metódusok által lehetővé tett programozási formát is objektumorientált programozásnak szokták nevezni.
A generikus függvények hívása esetében fontos tudnunk, hogy az azonos nevű metódusdefiníciók közül melyik fog végrehajtódni. Ez a generikus függvény meghívásakor alkalmazott aktuális paraméterek típusától függ. Az aktuális paraméterekre illeszkedő metódusok halmazát, az adott esetben használható metódusoknak nevezzük. Ezek közül az fog végrehajtódni, melyet a legspecifikusabbnak tekinthetünk. Ennek meghatározásához a paraméterek osztályainak öröklődési viszonyait kell figyelembe vennünk. Az a metódus fog kiválasztódni, melynek paraméterspecializációja a legkorábban fordul elő az ún. osztály-precedencialistában. Bár ezen lista előállítása egy bonyolultabb algoritmus szerint történik, két alapvető szabályt mindenképpen fontos ismernünk.
Egyszeres öröklődés esetében egyszerűen meghatározható az osztályok precedenciájának sorrendje: minden osztályt specifikusabbnak tekintünk a szülőjénél (1. szabály). Tehát a precedencialistát úgy állítjuk elő, hogy az öröklődési hierarchiában alulról fölfelé haladunk.
A többszörös öröklődésnél a precedencia lista előállítása már bonyolultabb kérdéssé válik. Tekintsük a következő osztálydefiníciót:
Ekkor egyértelmű, hogy az A osztály specifikusabb a B-nél és a C-nél is, de mi van abban az esetben, ha valamilyen metódust vagy kezdőérték-számító formot a B és C osztály már egyaránt definiált? Melyik az érvényes? A CLOS-ban ekkor az a szabály, hogy az előbb felsorolt szülőosztály specifikusabb a később felsoroltnál (2. szabály). Tehát ekkor a precedencia listát úgy kapjuk meg, hogy az öröklődési gráfban mindig alulról fölfelé haladunk, és ha elágazáshoz jutunk, akkor a különböző ágakat balról-jobbra egymás után járjuk be.
A két előbb említett szabály azonban nem mindig elegendő az egyértelmű sorrend meghatározásához (kört tartalmazó öröklődési gráf esetén). A CLOS nyelv tervezői ezért definiáltak egy algoritmust, mely ciklikus öröklődés esetében is feloldja a kétértelműségeket. Ennek alapján minden CLOS-implementációnak ugyanazt a precedencia-sorrendet kell előállítani az ősosztályok között. Mivel az algoritmus nem triviális és bonyolultabb öröklődés esetén nehezen követhető, ezért a programozás során nem célszerű erre építeni. Ha fontos a szülőosztályok sorrendje, lehetőségünk van arra, hogy azt explicit módon fogalmazzuk meg az osztály definiálásakor.