# Programmierparadigmen
## Was ist ein Paradigma?
- Paradigma – aus dem Altgriechischen Beispiel, Muster; Erzählung mit beispielhaftem Charakter (laut Duden)
- Programmierparadigmen beschreiben grundsätzliche Arten wie Computer-Programme formuliert werden können
- Programmiersprachen können einzelne oder viele Konzepte aufgreifen
- Keine verbreitete Sprache greift alle behandelten Konzepte auf
- Betrachtung unterschiedlicher Sprachen
- Ziel der Veranstaltung: Weiten der in Algorithmen und Programmierung eingeführten Sichten hin zu einem Werkzeugkoffer zur Lösung realer Probleme...
## Warum unterschiedliche Paradigmen?
Komplexität von Software schlecht beherrschbar
## Was bedeutet das?
- Programmierer schreiben, testen und dokumentieren zwischen 325 und 750 Codezeilen pro Monat
- maximal 360.000 Zeilen in 40 Jahren!
- Komplexität muss verborgen werden, z.B. durch
- Kapselung
- Spezifische Spachkonstrukte, Domain Specific Languages
- Ausdrucksstärkere Sprachen
- Entwicklung neuer Programmierparadigmen hilft Grenzen (ein wenig) zu verschieben
- Theoretische Rahmenbedingungen (Turing-Mächtigkeit, Satz von Rice) behalten Gültigkeit!
## Welche Paradigmen existieren?
- Aus Vorlesung AuP:
- Imperative Algorithmen
- Applikative Algorithmen
- Deduktive Algorithmen
- Aber Vielzahl weiterer Formen
- teilweise ergänzend, unterschiedliche Kategorisierung möglich
- Bsp: prozedural, deklarativ, objekt-orientiert, datenstromorientiert, parallele & verteilte Programmierung...
- Teilweise unterschiedliche Bezeichnungen
- Applikativ bzw. Funktional
- Deduktiv bzw. Logisch
- Aktueller Trend: Multiparadigmen-Sprachen
- Umsetzung unterschiedlichster Paradigmen in einer Sprache
- Beispiele: Scala, neuere C++-Standards, ...
# Objektorientierung und weiterführende Konzepte am Beispiel Java
- Bekannt:
- Grundlegendes Verständnis von Java
- Kapselung durch Klassen und Vererbung
- Ziele:
- Verständnis der Probleme bei Vererbung und Typersetzbarkeit in objektorientierten Programmiersprachen
- Kennenlernen der Grundideen generischer und abstrahierender Konzepte in Objekt-orientierter Programmierung (OOP)
- Praktische Erfahrungen anhand von Java & C++
- Ausdrucksstärke erhöhen, Komplexität verbergen
## Unit Testing
### Motivation
- Große Software-Systeme entwickeln sich über lange Zeiträume
- Wie können Änderungen an komplexen Code-Basen beherrscht werden?
- Veränderung über Zeit + Komplexität der Software
- Änderungen führen mglw. zu Auswirkungen, die für Einzelne nicht immer überschaubar sind
- Software muss nach Änderung von Grund auf durchgetestet werden
- Verbreitetes Vorgehen: zusätzlichen Code schreiben, der eigentlichen Code automatisch “überprüft”
- Nicht vollständig möglich (z.B. Halteproblem)
- Eher Heuristik
- Test-Code wird bei Ereignissen oder periodisch ausgeführt
- Vor Releases, nach Commit in Repository, während der Entwicklung ...
### Eigenschaften von Unit-Tests
- Software schlecht als Ganzes testbar -> Zergliederung von Software in sinnvolle Einheiten
- Individuelle Tests dieser Einheiten
- Dabei: reproduzierbar & vollautomatisierbar
- Ziel: Wann immer Änderungen in komplexen Programmen vorgenommen werden, möglichst vollständiger Test, da Programmierer nicht mehr alles überblicken
- Messung der Vollständigkeit der Tests schwierig
- Üblich: Messung von Überdeckung (Coverage) in Bezug auf Anzahl Funktionen, Code-Zeilen oder Verzweigungen
- Gute Praxis: Wenn ein Bug beim Testen oder Live-Betrieb auftritt -> Schreiben eines zusätzlichen Tests, um Wiederauftreten zu erkennen
### Unit-Testing in Java
- De facto Standard: JUnit Framework
- „Best Practice” für einfachen Einsatz:
- Java Code in ein oder mehrere Klassen im Ordner src speichern
- Im Ordner tests jeweils eine Klasse anlegen, die Funktionen einer Implementierungsklasse prüft
- Konvention: Testklasse einer Klasse Name heißt NameTest
- Eigentliche Tests werden in Methoden implementiert, die als Tests annotiert sind
- Typischer Ansatz: für bekannte Werte ausführen und Ergebnis mit Grundwahrheit (erwartetes Verhalten) vergleichen, bspw. mit assertEquals-Funktion
- Viele weitere Features, z.B. Deaktivieren von Tests, Timeouts, GUI Coverage, Mocks
### Unit Testing – Richtiges Abstraktionsniveau
- Um die Tests auszuführen, müssen jeweils entsprechende Hauptprogramme generiert werden („Test Suites“)
- Hauptschwierigkeiten von Unit-Tests:
- Richtiges Abstraktionsniveau
- „Herauslösen“ von zu testendem Code aus Umgebung
- Zwei wesentliche Möglichkeiten:
1. Individuelles Testen von Klassen:
- Vernachlässigt Zusammenspiel zwischen Klassen
- Oft sehr aufwändig, da andere Klassen für Unit-Tests nachgebildet werden müssen (Mocks)
- Was bei zyklischen Abhängigkeiten?
2. Gemeinsames Testen von Klassen:
- Erfordert Eingreifen in gekapselte Funktionalitäten
- Private & Protected Member-Variablen & Methoden!
- Eigentlich nicht möglich?!
## Reflections
- Normaler Ablauf: Programm schreiben, compilieren, ausführen
- Aber was wenn ich ein Programm zur Laufzeit inspizieren oder verändern möchte?
- Unterschiedliche Gründe
- Testen (um Fehler zu injizieren!)
- Fehlersuche („Debugging“)
- Nachladen von Plugins zur Modularisierung von Programmen
- Serialisierung/Deserialisierung von Code
- „Patchen“ zur Laufzeit
- Erkunden der Ablaufumgebung (z.B. OS-/Shared-Library Version)
- Benötigt die Fähigkeit, im Programm Codestruktur zu analysieren und ggf. zu verändern:
- Typisch: Abruf Klassenhierarchie, Auflisten von Methoden und Parametern, Austausch von Klassen und Methoden
- Teil von Java, Python, ...
API verstreut über verschiedene Packages, z.B. java.lang.Class, java.lang.instrument, java.lang.reflect
```java
Class cls = "test".getClass();
System.out.println("Die Klasse heisst " + cls.getName());
// Die Klasse heisst java.lang.String
```
```java
// import java.lang.reflect.Method;
Method[] methods = cls.getMethods();
for (Method m : methods)
System.out.println(m.getName());
```
### Annotationen
- Annotationen erlauben Anmerkungen an Klassen & Methoden
- Beginnen mit @
- Einige wenige vordefinierte z.B. @Override
- Aber auch eigene; u.a. durch Reflections abrufbar
- Häufig genutzt, wenn zusätzlicher Code geladen wird (Java EE)
- Oder um Unit-Tests zu markieren...
```java
class MultiTest {
@org.junit.jupiter.api.Test
void mul() {
...
```
### Reflektionen über Reflections
- Reflections sind ein sehr mächtiges Werkzeug, aber Einsatz sollte wohldosiert erfolgen
- Nachteile:
- Geringe Geschwindigkeit weil Zugriff über Programmcode erfolgt
- Kapselung kann umgangen werden
- private, protected und final können entfernt werden
- Aufruf/Veränderung interner Methoden & Auslesen/Veränderung interner Variablen
- Synchronisation zwischen externen und internen Komponenten bei Weiterentwicklung?
- Debugging veränderter Programme?
- Sicherheit?!
- Verwandte Techniken:
- Monkey Patching (JavaScript-Umfeld)
- Method Swizzling (Swift/Objective-C-Umfeld)
## Assertions
- Kann man interne Zustände testen, ohne invasive Techniken wie Reflections?
- Einfache Möglichkeit: An sinnvollen Stellen im Programmcode testen, ob Annahmen/Zusicherungen (Assertions) stimmen...
- Tests, die nie falsch sein sollten
- Erlauben gezielten Programmabbruch, um Folgefehler zu vermeiden
- Erlauben gezieltes Beheben von Fehlern
- Gemeinsames Entwickeln von Annahmen und Code
```java
class Stack {
public void push(Object o) {
...
if(empty() == true) // es sollte ein Objekt da sein
System.exit(-1);
}
...
}
```
Aber: Ausführungsgeschwindigkeit niedriger
- Zeitverlust stark abhängig von Programm/Programmiersprache
- Verbreitetes Vorgehen:
- Aktivieren der Tests in UnitTests und Debug-Versionen
- Deaktivieren in Releases
- Benötigt spezielle „if“-Bedingung: assert
- Aktivierung der Tests über Start mit java -ea
```java
class Stack {
public void push(Object o) {
...
assert empty() == false
}
```
### Welche braucht man?
- Woran erkennt man beim Programmieren bzw. (erneutem) Lesen von Code, dass man eine Assertion hinzufügen sollte?
- Eine einfache Heuristik – Die „Eigentlich“-Regel:
- Wenn einem beim Lesen von Programmcode ein Gedanke der Art „Eigentlich müsste an dieser Stelle XY gelten“ durch den Kopf geht,
- dann sofort eine entsprechende Assertion formulieren!
### Spezielle Assertions: Pre- & Postconditions
- An welchen Stellen ist es sinnvoll, Annahmen zu prüfen?
- Einfache Antwort: an so vielen Stellen wie möglich
- Komplexere Antwort: Design by contract, ursprünglich Eiffel
- Methoden/Programmabschnitte testen Bedingung vor und nach Ausführung
- Einige Sprachen bieten spezialisierte Befehle: requires und ensures
-> Ziel mancher Sprachen: Formale Aussagen über Korrektheit
```java
class Stack {
public void push(Object o) {
assert o != null // precondition
...
assert empty() == false // postcondition
}
...
}
```
### Klasseninvarianten
- Bei OO-Programmierung sind Vor- und Nachbedingungen nur eingeschränkt sinnvoll
- Bedingungen oft besser auf Objekt-Ebene -> interner Zustand
- Invarianten spezifizieren Prüfbedingungen
- In Java nicht nativ unterstützt:
- Erweiterungen, wie Java Modeling Language
- Simulation:
```java
class Stack {
void isValid() {
for(Object o : _objs) // Achtung: O(n) Aufwand!
assert o != null
}
public void push(Object o) {
isValid() // always call invariant
...
isValid() // always call invariant
}
```
## Exeptions
Signifikantes Element vieler Sprachen: Wie wird mit Fehlern umgegangen?
Fehler können unterschiedliche Gründe haben
Besser für Code-Komplexität: Fehlerprüfungen an zentralerer Stelle
- Abbrechen und Programm-Stack „abbauen“ bis (zentrale) Fehlerbehandlung greift
- Dabei Fehler sinnvoll gruppieren
- Java (und viele mehr): try/catch/throw-Konstrukt
```java
private void readFile(String f) {
try {
Path file = Paths.get("/tmp/file");
if(Files.exists(file) == false)
throw new IOException("No such dir");
array = Files.readAllBytes(file);
} catch(IOException e) {
// do something about it
}
}
```
throw übergibt ein Objekt vom Typ Throwable an Handler, dabei zwei Unterarten:
- Error: Sollte nicht abgefangen werden z.B. Fehler im Byte-Code, Fehlgeschlagene Assertions
- Exceptions:
- Checked Exception: Programm muss Exception fangen oder in Methode vermerken
- Runtime Exceptions: Müssen nicht (aber sollten) explizit behandelt werden, bspw. ArithmeticException oder IndexOutOfBoundsException
### Checked Exceptions
Deklaration einer überprüften Exception:
```java
void dangerousFunction() throws IOException {
...
if(onFire)
throw IOException("Already burns");
...
}
```
Die Deklaration mit "throws IOException" lässt beim build mögliche Fehler durch IOExceptions dieser Funktion zu, diese müssen durch die aufrufende Methode abgefangen werden.
Aufrufe ohne try-catch-Block schlagen fehl!
Sollte man checked oder unchecked Exceptions verwenden?
- Checked sind potenziell sicherer
- Unchecked machen Methoden lesbarer
- Faustregel unchecked, wenn immer auftreten können (zu wenig Speicher, Division durch 0)
Abfangen mehrerer unterschiedlicher Exceptions
```java
try {
dangerousFunction();
} catch(IOException i) {
// handle that nasty error
} catch(Exception e) {
// handle all other exceptions
}
```
Aufräumen nach einem try-catch-Block: Anweisungen im finally-Block werden immer ausgeführt, d.h. auch bei
return in try- oder catch-Block (oder fehlerloser Ausführung)
```java
try {
dangerousFunction();
} catch(Exception e) {
// handle exceptions
return;
} finally {
// release locks etc..
}
```
## Generizät von Datentypen
(Typ-)Generizität:
- Anwendung einer Implementierung auf verschiedene Datentypen
- Parametrisierung eines Software-Elementes (Methode, Datenstruktur, Klasse, ...) durch einen oder mehrere Typen
Beispiel:
```java
int min(int a, int b) {
return a < b ? a : b;
}
float min(float a, float b) {
return a < b ? a : b;
}
String min(String a, String b) { // lexikographisch
return a.compareTo(b) < 0 ? a : b;
}
```
### Grenzen von Typsubstitution
Problem: Für jeden Typ? Wie kann sort implementiert werden?
Möglicher Ausweg: Klassenhierarchie mit zentraler Basisklasse
```java
void sort(Object[] feld) { ... } //z.B. java.lang.Object
void sort(java.util.Vector feld) { ... } //alternativ (nutzt intern Object)
```
Möglicher Ausweg 2: Nutzung primitiver Datentypen nicht direkt möglich
```java
Object[] feld = new Object[10]; //Object[] ≠ int[]
feld[0] = new Integer(42);
int i = ((Integer) feld[0]).intValue(); //erfordert Wrapper-Klassen wie java.lang.Integer
```
Weiteres Problem: Typsicherheit
Typ-Substituierbarkeit: Kann ein Objekt einer Oberklasse (eines Typs) durch ein Objekt seiner Unterklasse (Subtyps) ersetzt werden?
Beispiel (isSubtyp): short $\rightarrow$ int $\rightarrow$ long
Viele Programmiersprachen ersetzen Typen automatisch, d.h. diese wird auch für shorts und ints verwendet
```java
long min(long a, long b) {
return a < b ? a : b;
}
```
Kreis-Ellipse-Problem: Modellierung von Vererbungsbeziehungen
- „Ist ein Kreis eine Ellipse?“ „Oder eine Ellipse ein Kreis?“
- Annahme: Kreis := Ellipse mit Höhe = Breite
```java
Circle c = new Circle();
c.skaliereX(2.0); //skalieren aus Klasse Circle
c.skaliereY(.5); //is das noch ein Kreis?
```
evtl. Reihenfolge in der Klassenhierarchie tauschen (nutzung von Radius)? Was bedeutet das für Ellipse?
Verwandte Probleme: Rechteck-Quadrat, Set-Bag
### Ko- und Kontravarianz
Geg.: Ordnung von Datentypen von spezifisch $\rightarrow$ allgemeiner
- Gleichzeitige Betrachtung einer Klassenhierarchie, die Datentypen verwendet
- Kovarianz: Erhaltung der Ordnung der Typen
- Kontravarianz: Umkehrung der Ordnung
- Invarianz: keines von beiden
- Anwendung für
- Parameter
- Rückgabetypen
- Ausnahmetypen
- Generische Datenstrukturen
Beispiel: Basierend auf Meyer‘s SKIER-Szenario
```java
class Student {
String name;
Student mate;
void setRoomMate(Student s) { ... }
}
```
Wie überschreibt man in einer Unterklasse Girl oder Boy die Methode „setRoomMate“ in elternfreundlicher Weise? Von Eltern sicher gewollt - Kovarianz:
```java
class Boy extends Student {
void setRoomMate(Boy b) { ... }
}
class Girl extends Student {
void setRoomMate(Girl g) { ... }
}
```
Was passiert mit folgendem Code?
```java
Boy kevin = new Boy("Kevin");
Girl vivian = new Girl("Vivian");
kevin.setRoomMate(vivian);
```
- Verwendet setRoomMate der Basisklasse
- setRoomMate Methoden der abgeleiteten Klassen überladen nur Spezialfälle $\rightarrow$ gültig
- In C++ und Java keine Einschränkung der Typen zur Compile-Zeit
- Kovarianz so nur in wenigen Sprachen implementiert (z.B. Eiffel über redefine); Überprüfung auch nicht immer statisch!
- Auch bekannt als catcall-Problem (cat = changed availablility type)
Ausweg: Laufzeitüberprüfung
```java
class Girl extends Student {
...
public void setRoomMate(Student s) { //student wird aufgerufen! nicht boy oder girl, dadurch können die methoden der klasse verwendet werden
if (s instanceof Girl)
super.setRoomMate(s);
else
throw new ParentException("Oh Oh!");
}
}
```
Nachteil: Nur zur Laufzeit überprüfung
#### Ko- und Kontravarianz für Rückgabewerte
Kovarianz (gängig):
```java
public class KlasseA {
KlasseA ich() { return this; }
}
public class KlasseB extends KlasseA {
KlasseB ich() { return this; }
}
```
Kontravarianz macht wenig Sinn und kommt (gängig) nicht vor
In objektorientierten Programmiersprachen im Allgemeinen
- Kontravarianz: für Eingabeparameter
- Kovarianz: für Rückgabewerte und Ausnahmen
- Invarianz: für Ein- und Ausgabeparameter
### Liskovsches Substitutionsprinzip (LSP)
Barbara Liskov, 1988 bzw. 1993, definiert stärkere Form der Subtyp-Relation, berücksichtigt Verhalten:
> Wenn es für jedes Objekt $o_1$ eines Typs S ein Objekt $o_2$ des Typs T gibt, so dass für alle Programme P, die mit Operationen von T definiert sind, das Verhalten von P unverändert bleibt, wenn $o_2$ durch $o_1$ ersetzt wird, dann ist S ein Subtyp von T.'
Subtyp darf Funktionalität eines Basistyps nur erweitern, aber nicht einschränken.
Beispiel: Kreis-Ellipse $\rightarrow$ Kreis als Unterklasse schränkt Funktionalität ein und verletzt damit LSP
### Generics in Java (Typsicherheit)
Motivation: Parametrisierung von Kollektionen mit Typen
```java
LinkedList liste = new LinkedList();
liste.add("Generics");
String s = liste.get(0);
```
auch für Iteratoren nutzbar
```java
Iterator iter = liste.iterator();
while(iter.hasNext()) {
String s = iter.next();
...
}
```
oder mit erweiterter for-Schleife
```java
for(String s : liste) {
System.out.println(s);
}
```
Deklaration: Definition mit Typparameter
```java
class GMethod {
static T thisOrThat(T first, T second) {
return Math.random() > 0.5 ? first : second;
}
}
```
- T = Typparameter (oder auch Typvariable) wird wie Typ verwendet, stellt jedoch nur einen Platzhalter dar
- wird bei Instanziierung (Parametrisierung) durch konkreten Typ „ersetzt“
- nur Referenzdatentypen (Klassennamen), keine primitiven Datentypen
Anwendung:
- explizite Angabe des Typparameters
```java
String s = GMethod.thisOrThat("Java", "C++");
Integer>thisOrThat(new Integer(42), new Integer(23));
```
- automatische Typinferenz durch Compiler
```java
String s = GMethod.thisOrThat("Java", "C++");
Integer i = GMethod.thisOrThat(new Integer(42), new Integer(23));
```
#### Eingrenzung von Typparametern
Festlegung einer Mindestfunktionalität der einzusetzenden Klasse, z.B. durch Angabe einer Basisklasse
- Instanziierung von T muss von Comparable abgeleitet werden (hier ein Interface, dass wiederum generisch ist, daher Comparable)
- Verletzung wird vom Compiler erkannt
```java
static> T min(T first, T second) {
return first.compareTo(second) < 0 ? first : second;
}
```
Angabe des Typparameters bei der Klassendefinition:
```java
class GArray {
T[] data;
int size = 0;
public GArray(int capacity) { ... }
public T get(int idx) { return data[idx]; }
public void add(T obj) { ... }
}
```
Achtung: new T[n] ist unzulässig! Grund liegt in der Implementierung von Generics:
Es gibt zwei Möglichkeiten der internen Umsetzung generischen Codes:
- Code-Spezialisierung: jede neue Instanziierung generiert neuen Code
- Array → ArrayString, Array → ArrayInteger
- Problem: Codegröße
- Code-Sharing: gemeinsamer Code für alle Instanziierungen
- Array → Array