Gyakorlati alapok III.
Objektumorientált számológépváz interfésszel
Az alábbi kódegyüttes bár konkrét,
futtatható implementáció és jónéhány dolgot képes kiszámolni, ám egy igazi
számológép alkalmazásnak csupán működőképes, “zörgő” csontváza. Lényegében a
kód erőteljesen lecsupaszított, hogy meglássuk benne az objektumorientált
komponensek egymás közti kapcsolatát, működését, főleg az interfész konkrét
alkalmazását. Ilyen minőségében azonban bármikor továbbgondolható és
bővíthető. Csak 1 példa: adatbekérés és validálás (bemeneti adatok
ellenőrzése) helyett bemeneti számként statikusan 2 változó van deklarálva…
double a = 10;
double b = 2;
...amelyet a tapasztaltak elegáns könnyedséggel bővíthetnek ki egy adatbekérő
osztályban.
Először gondolkodjunk el azon, hogy elvi szinten mit is csinál egy számológép?
Tevékenységét kissé leegyszerűsítve: számokon matematikai műveleteket végez.
Ez sokféle lehet, ám ha csak 1 szintet is feljebb lépünk (ez a már említett
elvi, “filozófiai” szint), akkor már csupán 1 tevékenységet láthatunk meg és
ez maga a matematikai művelet. Első pillantásra ezzel az elvonatkoztatással
(absztrakcióval) nem érünk semmit, azonban ez tévedés. Pontosan ezen az
absztrakciós szinten lebegnek az interfészek és várnak arra, hogy végre egy,
vagy több osztályba “inkarnálódjanak”. Azok számára, akik a fentieket nem
értették meg, nézzük meg mindezt egy másfajta megközelítésben!
Mi a közös a következőkben:
-
összeadás,
-
kivonás,
-
szorzás,
-
osztás,
-
hatványozás,
-
stb.
Nyilvánvalóan mindegyikük
matematikai művelet. Ha magát a műveletet, mint legáltalánosabb tulajdonságot
sikerül egy működőképes kódszerkezetben megfogalmaznunk (pontosan ez a
kódszerkezet az interfész), akkor utána viszonylag kevés átalakítással, de
tonnaszámra gyárthatjuk belőle a (kissé továbbmódosított) műveleteket. Olyan
ez, mint a tervrajz: csak egyszer kell okosan és jól megterveznünk, hogy
belőle számos további építmény születhessen. Azonban az interfésznek van egy
másik, szintén legalább ennyire hasznos tulajdonsága is: mivel csak elvi
kidolgozás, a konkrét, futtatható implementációt ráhagyhatjuk másvalakire.
Sőt, néha erre egyenesen szükségünk is van. Gondoljunk csak az interfész
eredeti jelentésére: csatlakozási vagy határfelület. Mi történik akkor, ha
megírt kódunk már készen van és csatlakozna egy másik, de még meg sem írt
kódhoz? Az objektumorientált elveket követve ez nem is történhet másképpen,
mint interfészek deklarálásával, majd későbbi kidolgozásával.
Az alábbi Java-kódban a gondolati-tervezési hierarchia csúcsán az
Operation (művelet) nevű interfész áll:
public
interface Operation {
double execute(double a, double b);
String getName();
}
2 db kidolgozandó metódusa van, ez
az execute() és a getName().
Az interfész implementálásával (implements Operation)
a 2 metódus specializálható, átalakítható, azaz műveletenként testre szabható.
Nézzük meg például szorzás esetében:
class Addition implements Operation{
@Override
public double execute(double a, double b) {
return a + b;
}
@Override
public String getName() {
return "Osszeadas";
}
}
Észrevehetjük, hogy ez az elv van implementálva az összes műveleti osztályban,
persze az adott műveletre jellemző specifikummal.
Az összes osztályt pedig a Main osztály, mint
irányító, kontroller fogja egybe, az osztályok ott vannak példányosítva,
illetve az alapszintű számítások és eredménykiírás is ott találhatók. Ezek
ugyan lehetnének külön osztályban is, amellyel tovább követtük volna az
objektumorientált elveket, ám a modellezéshez ennyi elég.
További érdekesség, hogy a példányosított osztályokat belegyömöszöltük egy
Operation típusú tömbbe…
Operation [] tomb = {addition, subtraction,
multiplication, division, exponentation, logarithm};
...amellyel nagyon elegánsan tudtuk kilistázni az eredményeket:
for (int i = 0; i < tomb.length; i++) {
double result = tomb[i].execute(a, b);
String operationName = tomb[i].getName();
System.out.println(operationName + " eredmenye: " + result);
}
Nézzük meg a futtatható Java-kódot:
Main.java
public class Main {
public static void main(String[] args) {
Addition addition = new Addition();
Subtraction subtraction = new Subtraction();
Multiplication multiplication = new Multiplication();
Division division = new Division();
Exponentation exponentation = new Exponentation();
Logarithm logarithm = new Logarithm();
double a = 10;
double b = 2;
Operation [] tomb = {addition, subtraction, multiplication,
division, exponentation, logarithm};
for (int i = 0; i < tomb.length; i++) {
double result = tomb[i].execute(a,
b);
String operationName =
tomb[i].getName();
System.out.println(operationName + "
eredmenye: " + result);
}
}
}
Operation.java
public interface Operation {
double execute(double a, double b);
String getName();
}
Addition.java
class Addition implements Operation{
@Override
public double execute(double a, double b) {
return a + b;
}
@Override
public String getName() {
return "Osszeadas";
}
}
Division.java
public class Division implements Operation{
@Override
public double execute(double a, double b) {
return a / b;
}
@Override
public String getName() {
return "Osztas";
}
}
Exponentation.java
public class Exponentation implements Operation{
@Override
public double execute(double a, double b) {
return Math.pow(a, b);
}
@Override
public String getName() {
return "Hatvanyozas";
}
}
Logarithm.java
public class Logarithm implements Operation{
@Override
public double execute(double a, double b) {
return Math.log(a);
}
@Override
public String getName() {
return "Logaritmus";
}
}
Multiplication.java
public class Multiplication implements Operation{
@Override
public double execute(double a, double b) {
return a * b;
}
@Override
public String getName() {
return "Szorzas";
}
}
Subtraction.java
public class Subtraction implements Operation{
@Override
public double execute(double a, double b) {
return a - b;
}
@Override
public String getName() {
return "Kivonas";
}
}
Végeredmény:
Összeadás eredménye: 12.0
Kivonás eredmenye: 8.0
Szorzás eredmenye: 20.0
Osztás eredmenye: 5.0
Hatványozás eredmenye: 100.0
Logaritmus eredmenye: 2.302585092994046