64 KiB
		
	
	
	
	
	
	
	
			
		
		
	
	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:
- 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?
 
- 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
Class cls = "test".getClass();
System.out.println("Die Klasse heisst " + cls.getName());
// Die Klasse heisst java.lang.String
// 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...
 
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
 
 
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
 
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
 
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:
 
 
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
 
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:
  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
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)
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:
 
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
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
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
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
 
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
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:
class Boy extends Student {
  void setRoomMate(Boy b) { ... }
}
class Girl extends Student {
  void setRoomMate(Girl g) { ... }
}
Was passiert mit folgendem Code?
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
\rightarrowgü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
 
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):
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_1eines Typs S ein Objekto_2des Typs T gibt, so dass für alle Programme P, die mit Operationen von T definiert sind, das Verhalten von P unverändert bleibt, wenno_2durcho_1ersetzt wird, dann ist S ein Subtyp von T.' Subtyp darf Funktionalität eines Basistyps nur erweitern, aber nicht einschränken.
Beispiel: Kreis-Ellipse\rightarrowKreis als Unterklasse schränkt Funktionalität ein und verletzt damit LSP
Generics in Java (Typsicherheit)
Motivation: Parametrisierung von Kollektionen mit Typen
LinkedList<String> liste = new LinkedList<String>();
liste.add("Generics");
String s = liste.get(0);
auch für Iteratoren nutzbar
Iterator<String> iter = liste.iterator();
  while(iter.hasNext()) {
    String s = iter.next();
    ...
}
oder mit erweiterter for-Schleife
for(String s : liste) {
  System.out.println(s);
}
Deklaration: Definition mit Typparameter
class GMethod {
  static <T> 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
String s = GMethod.<String>thisOrThat("Java", "C++"); Integer>thisOrThat(new Integer(42), new Integer(23)); - automatische Typinferenz durch Compiler
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
 
static<T extends Comparable<T>> T min(T first, T second) {
  return first.compareTo(second) < 0 ? first : second;
}
Angabe des Typparameters bei der Klassendefinition:
class GArray<T> {
  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