This repository has been archived on 2022-06-01. You can view files and clone it, but cannot push or open issues or pull requests.
LinkDitch/Documentation/programmEntwurf.org

828 lines
34 KiB
Org Mode
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Programmentwurf
[Bezeichung]
Name: [Name, Vorname]\\
Martrikelnummer: [MNR]
Abgabedatum: [DATUM]
*
:PROPERTIES:
:CUSTOM_ID: section
:END:
Allgemeine Anmerkungen:
- es darf nicht auf andere Kapitel als Leistungsnachweis verwiesen
werden (z.B. in der Form “XY wurde schon in Kapitel 2 behandelt, daher
hier keine Ausführung”)
- alles muss in UTF-8 codiert sein (Text und Code)
- sollten mündliche Aussagen den schriftlichen Aufgaben widersprechen,
gelten die schriftlichen Aufgaben (ggf. an Anpassung der schriftlichen
Aufgaben erinnern!)
- alles muss ins Repository (Code, Ausarbeitung und alles was damit
zusammenhängt)
- die Beispiele sollten wenn möglich vom aktuellen Stand genommen werden
- finden sich dort keine entsprechenden Beispiele, dürfen auch ältere
Commits unter Verweis auf den Commit verwendet werden
- Ausnahme: beim Kapitel “Refactoring” darf von vorne herein aus allen
Ständen frei gewählt werden (mit Verweis auf den entsprechenden
Commit)
- falls verlangte Negativ-Beispiele nicht vorhanden sind, müssen
entsprechend mehr Positiv-Beispiele gebracht werden
- Achtung: werden im Code entsprechende Negativ-Beispiele gefunden,
gibt es keine Punkte für die zusätzlichen Positiv-Beispiele
- Beispiele
- “Nennen Sie jeweils eine Klasse, die das SRP einhält bzw.
verletzt.”
- Antwort: Es gibt keine Klasse, die SRP verletzt, daher hier 2
Klassen, die SRP einhalten: [Klasse 1], [Klasse 2]
- Bewertung: falls im Code tatsächlich keine Klasse das SRP
verletzt: volle Punktzahl ODER falls im Code mind. eine Klasse
SRP verletzt: halbe Punktzahl
- verlangte Positiv-Beispiele müssen gebracht werden
- Code-Beispiel = Code in das Dokument kopieren
* Kapitel 1: *Einführung*
:PROPERTIES:
:CUSTOM_ID: kapitel-1-einführung
:END:
*** Übersicht über die Applikation
:PROPERTIES:
:CUSTOM_ID: übersicht-über-die-applikation
:END:
[Was macht die Applikation? Wie funktioniert sie? Welches Problem löst
sie/welchen Zweck hat sie?]
Die Anwendung ist zur organisierten Abspeicherung von Links bzw. URLs gedacht.
Diese können durch die Anwendung abgespeichert werden.
Zur besseren Organisation ist es außerdem möglich, Kategorien anzulegen und die Links diesen zuzuordnen, wie beispielsweise 'Libary', 'Selfhostable', 'Dienst' usw.
Zusätzlich kann die Anwendung auch Tags zu Links hinzufügen können, wenn die Implementation Webseite beispielsweise bereits kennt (z.B. 'Github').
Eigene Regeln für Tags können auch angelegt werden, sie werden durch einen Regulären Ausdruck beschrieben.
Der User, welcher einen Eintrag angelegt hat wird auch gespeichert.
TODO: Die Anwendung soll Persistenz enthalten sowie verschiedene Methoden zum Durchsuchen (nach Kategorie, User) und Exportieren der Daten.
*** Wie startet man die Applikation?
:PROPERTIES:
:CUSTOM_ID: wie-startet-man-die-applikation
:END:
[Wie startet man die Applikation? Welche Voraussetzungen werden
benötigt? Schritt-für-Schritt-Anleitung]
**** Voraussetzungen:
- Java 17
- Maven
**** Compilieren
#+begin_src shell
mvn clean install
#+end_src
**** Ausführen
#+begin_src
java -jar 0-Plugin/target/0-Plugin-1.0-SNAPSHOT-jar-with-dependencies.jar [PARAMETER]
#+end_src
*** Wie testet man die Applikation?
:PROPERTIES:
:CUSTOM_ID: wie-testet-man-die-applikation
:END:
[Wie testet man die Applikation? Welche Voraussetzungen werden benötigt?
Schritt-für-Schritt-Anleitung]
#+begin_src
mvn clean test
#+end_src
* Kapitel 2: Clean Architecture
:PROPERTIES:
:CUSTOM_ID: kapitel-2-clean-architecture
:END:
*** Was ist Clean Architecture?
:PROPERTIES:
:CUSTOM_ID: was-ist-clean-architecture
:END:
[allgemeine Beschreibung der Clean Architecture in eigenen Worten]
Die Clean Architecture ist eine Software Architektur, die es ermöglichen soll langlebige Systeme zu entwickeln.
Dazu wird der eigentliche Zweck einer Anwendung von möglichst vielen technischen Details getrennt.
Auf diese Weise soll ein Kern der Anwendung entstehen, welcher beispielsweise die Businessregeln enthält und bis auf die Wahl der Programmiersprache (für Langlebigkeit von Sprachen siehe bspw. Java) keine Abhängigkeiten zu technischen Entscheidungen hat.
Konkretere technische Details, wie beispielsweise die Wahl einer Datenbank oder ob für die Benutzerschnittstelle ein CLI oder ein Webserver genutzt wird, werden an den Rand der Anwendung gedrängt und nur durch Zwischenschichten mit dem Kern verbunden.
Die konkreten Schichten sind (von langlebig nach kurzlebig und von wenigen (keinen) nach vielen Abhängigkeiten sortiert):
- Abstraction Code
- Domain Code
- Application Code
- Adapters
- Plugins
Durch die Dependency Rule wird sichergestellt, dass Abhängigkeiten immer von außen nach innen sind, und somit eine äußere Schicht ausgetauscht werden könnte, ohne, dass die inneren Schichten angepasst werden müssten.
*** Analyse der Dependency Rule
:PROPERTIES:
:CUSTOM_ID: analyse-der-dependency-rule
:END:
[(1 Klasse, die die Dependency Rule einhält und eine Klasse, die die
Dependency Rule verletzt); jeweils UML der Klasse und Analyse der
Abhängigkeiten in beide Richtungen (d.h., von wem hängt die Klasse ab
und wer hängt von der Klasse ab) in Bezug auf die Dependency Rule]
Da die Abhängigkeiten zwischen den einzelnen Schichten durch Maven restriktiv kontrolliert werden gibt es kein negativ Beispiel.
**** Positiv-Beispiel: Dependency Rule
:PROPERTIES:
:CUSTOM_ID: positiv-beispiel-dependency-rule
:END:
[[./uml/LinkCliAdapter.png]]
[[./uml/LinkCliAdapterClasses.png]]
Die Klasse LinkCliAdapter ist selbst abhängig von einigen Value Object der Domäne (LinkUrl, UserName, CategoryName) und dem LinkUseCase sowie seinem speziellen Format LinkDto aus der Application Schicht.
Abhängig von der Klasse LinkCliAdapter ist die Klasse LinkCommands aus der Plugin Schicht.
**** 2. Positiv-Beispiel: Dependency Rule
[[./uml/CSVCategoryPersistenceAdapter.png]]
[[./uml/CSVCategoryPersistenceAdapterClasses.png]]
Die Klasse CSVCategoryPersistenceAdapter ist abhängig von dem Domänen Entity Category und Implementiert das Interface der Domäne PersistenceAdapter<Category>. Außerdem ist es abhängig vom Persitenz Entity CategorEntity, das in der Adapter Schicht definiert ist und dem Interface GenericDAO<CategorEntity> aus der Adapter Schicht.
In der Plugin Schicht ist mit dem GenericCSVDAO<T> eine Klasse gegeben, die dieses Interface implementiert.
Das Domänen Repository CategoryRepository ist abhängig von einem PersistenceAdapter (Interface), welche das CSVCategoryPersistenceAdapter implementiert.
Somit ist das Repository der Domäne zur Compile Zeit nicht abhängig von dem CSVCategoryPersistenceAdapter des Adapter sondern nur zur Runtime, da im Adapter eine Implementation des benötigten Interfaces liegt.
*** *Analyse der Schichten*
:PROPERTIES:
:CUSTOM_ID: analyse-der-schichten
:END:
[jeweils 1 Klasse zu 2 unterschiedlichen Schichten der
Clean-Architecture: jeweils UML der Klasse (ggf. auch zusammenspielenden
Klassen), Beschreibung der Aufgabe, Einordnung mit Begründung in die
Clean-Architecture]
**** Schicht: Domain
:PROPERTIES:
:CUSTOM_ID: schicht-name
:END:
[[./uml/LinkUrl.png]]
Die Klasse LinkUrl ist ein Klasse, welche einen zentralen Bestandteil der zu speichernden Daten repräsentiert: Die URL eines Links.
Damit ist sie Teil der Domäne, da es sich direkt um die Businessregeln der zu verarbeiten Daten handelt.
So stellt sie beispielsweise durch die Verwendung der Java Klasse URL sicher, dass die Url ein valides Format hat und somit die Domänenregeln erfüllt.
**** Schicht: Plugin
:PROPERTIES:
:CUSTOM_ID: schicht-name-1
:END:
[[./uml/GitHubTagMatcher.png]]
Die Klasse GitHubTagMatcher ist eine Implementation der des Interface TagMatcher und dafür verantwortlich festzustellen, ob ein Link auf eine GitHub Url verweist und im positiv Fall zu versuchen über die GitHub Repository-Api zusätzliche Informationen über das verlinkte Repository zu erhalten.
Die Klasse ist Teil der Plugin Schicht, da die Interaktion mit der GitHub Repository-Rest-Api eindeutig eine Abhängigkeiten zu einem fremden Bestandteil darstellt und solche Abhängigkeiten an den Rand der Anwendung gedrängt werden sollten.
* Kapitel 3: SOLID
:PROPERTIES:
:CUSTOM_ID: kapitel-3-solid
:END:
*** Analyse Single-Responsibility-Principle (*SRP)*
:PROPERTIES:
:CUSTOM_ID: analyse-single-responsibility-principle-srp
:END:
[jeweils eine Klasse als positives und negatives Beispiel für SRP;
jeweils UML der Klasse und Beschreibung der Aufgabe bzw. der Aufgaben
und möglicher Lösungsweg des Negativ-Beispiels (inkl. UML)]
**** Positiv-Beispiel
:PROPERTIES:
:CUSTOM_ID: positiv-beispiel
:END:
[[./uml/CategoryName.png]]
Die Klasse CategoryName repräsentiert den Namen einer Kategorie und legt dabei fest, welche Werte dieser annehmen kann.
**** Negativ-Beispiel
:PROPERTIES:
:CUSTOM_ID: negativ-beispiel
:END:
[[./uml/CommandHandler.png]]
[[./uml/CommandHandlerClasses.png]]
Die Klasse CommandHandler wird aufgerufen und leitet die CLI Parameter an die einzelnen SubCommand-Klassen weiter.
Da die SubCommand-Klassen jedoch für ihre Konstruktoren, die Adapter benötigen, die Adapter wiederum die Usecases benötigen usw., wird der gesamte Baum an benötigten Klassen im Konstruktor der CommandHandler Klasse aufgebaut.
Dies ist jedoch nicht ihre Responsibility.
**Lösung**: Der CommandHandler bekommt die SubCommand als Konstruktorparameter eingreicht und das Erstellen der restlichen Klassen wird von einer dedizierten Klasse durchgeführt. (UML nicht skizziert, weil es schneller ist den Code zu fixen und dann das UML zu generieren, als das UML per Hand zu machen.)
*** Analyse Open-Closed-Principle (OCP)
:PROPERTIES:
:CUSTOM_ID: analyse-open-closed-principle-ocp
:END:
[jeweils eine Klasse als positives und negatives Beispiel für OCP;
jeweils UML der Klasse und Analyse mit Begründung, warum das OCP
erfüllt/nicht erfüllt wurde -- falls erfüllt: warum hier
sinnvoll/welches Problem gab es? Falls nicht erfüllt: wie könnte man es
lösen (inkl. UML)?]
**** Positiv-Beispiel
:PROPERTIES:
:CUSTOM_ID: positiv-beispiel-1
:END:
[[./uml/TagMatcher.png]]
Die Klasse TagMatcher bietet das Interface (in diesem Fall als abstrakte Klasse) für alle möglichen Test, ob einer URL ein gewisser Tag zugeordnet werden kann.
Statt eines Switchstatements wie hier gezeigt:
#+begin_src java
switch(url):
case githubRegexPatter.matches(url):
tags.add(new GitHubTag())
case someOtherMatcher.matches(url):
tags.add(new GitHubTag())
#+end_src
Wird für einen Link von allen TagMatchern zur Laufzeit geprüft, ob dieser matcht.
So können beispielsweise benutzerdefinierte Matcher verwendet werden (siehe Klasse CustomTagMatcher) und wenn ein neuer TagMatcher mit besonderer Implementation hinzufügt wird muss er nur die abstrakte Klasse erweitern und zur Liste die während der Dependecy-Injection-Phase gebaut wird hinzugefügt werden.
In den Domain, Application und Adapter Schichten muss hierfür kein Code angepasst werden.
#+begin_src java
public Set<Tag> getTagsFor(LinkUrl url) {
Set<Tag> result = new HashSet<>();
tagMatcherRepository.getTagMatchers().forEach(
tagMatcher -> tagMatcher.ifMatches(url).
addTo(result));
return result;
}
#+end_src
**** Negativ-Beispiel
:PROPERTIES:
:CUSTOM_ID: negativ-beispiel-1
:END:
[[./uml/Exeptions.png]]
Die Exeptions der Anwendung erfüllen nicht wirklich das OCP.
So gibt es einen Try-Catch-Block um die gesamte Anwendung.
#+begin_src java
try {
commandHandler.executeCommand(args);
}
catch (PersistenceError persistenceError) {
System.out.println("There was a Error with loading
or saving the persistence data.");
System.out.println(persistenceError.getMessage());
}
catch (RuntimeException runtimeException) {
System.out.println(runtimeException.getMessage());
}
#+end_src
Fügt man eine neue Exeption hinzu muss man zwar keinen Catch-Block hinzufügen um die Lauffähigkeit zu erhalten, es wäre jedoch für die Benutzerfreundlichkeit deutlich besser (vgl. extra Nachricht bei PersistenceError) wenn man es täte.
Damit hierfür dann nicht für jede Exeption ein Catch-Block hinzugefügt werden muss sollten die Exeptions semantisch gruppiert werden und gemeinsame Elternklassen haben.
So könnte man die Elternklasse Domainerror einfügen, für Exeptions, die innerhalb der Domäne liegen und keinen Programmfehler sondern eine falsche Nutzerhandlung bedeuten.
Darunter würden Exeption wie CategoryAlreadyExists fallen.
Sind diese definiert kann man leichter neue Exeptions hinzufügen ohne die Catchblöcke anpassen zu müssen oder dem Nutzer schlechte/inkonsistente Ausgaben zu geben.
UML nicht vorhanden, da es schneller wäre den Fix einzubauen und das UML zu generieren als das UML von Hand zu bauen.
*** Analyse Liskov-Substitution- (LSP), Interface-Segreggation- (ISP), Dependency-Inversion-Principle (DIP)
:PROPERTIES:
:CUSTOM_ID: analyse-liskov-substitution--lsp-interface-segreggation--isp-dependency-inversion-principle-dip
:END:
[jeweils eine Klasse als positives und negatives Beispiel für entweder
LSP oder ISP oder DIP); jeweils UML der Klasse und Begründung, warum man
hier das Prinzip erfüllt/nicht erfüllt wird]
**Dependency-Inversion-Principle**
**** Positiv-Beispiel
[[./uml/CategoryIdGenerator.png]]
Beim der Klasse CategoryIdGenerator wird das DIP erfüllt.
Die Klasse CategoryUseCase ist nicht anhängig von einer konkreten Implementation eines IdGenerators wie dem RandomCategoryIdGenerator sondern von dem Interface CategoryIdGenerator.
Dies ist auch besonders für Test praktisch, da man dann nicht mit Zufallszahlen umgehen muss.
**** Negativ-Beispiel
[[./uml/LinkUseCase.png]]
Beim der Klasse LinkUseCase wird das DIP nicht erfüllt.
Die Klasse LinkUseCase ist anhängig von konkreten Implementation eines IdGenerators, dem RandomLinkIdGenerator.
* Kapitel 4: Weitere Prinzipien
:PROPERTIES:
:CUSTOM_ID: kapitel-4-weitere-prinzipien
:END:
*** Analyse GRASP: Geringe Kopplung
:PROPERTIES:
:CUSTOM_ID: analyse-grasp-geringe-kopplung
:END:
[jeweils eine bis jetzt noch nicht behandelte Klasse als positives und
negatives Beispiel geringer Kopplung; jeweils UML Diagramm mit
zusammenspielenden Klassen, Aufgabenbeschreibung und Begründung für die
Umsetzung der geringen Kopplung bzw. Beschreibung, wie die Kopplung
aufgelöst werden kann]
**** Positiv-Beispiel
:PROPERTIES:
:CUSTOM_ID: positiv-beispiel-3
:END:
**** Negativ-Beispiel
:PROPERTIES:
:CUSTOM_ID: negativ-beispiel-3
:END:
*** Analyse GRASP: Hohe *Kohäsion*
:PROPERTIES:
:CUSTOM_ID: analyse-grasp-hohe-kohäsion
:END:
[eine Klasse als positives Beispiel hoher Kohäsion; UML Diagramm und
Begründung, warum die Kohäsion hoch ist]
*** *Don't Repeat Yourself (DRY)*
:PROPERTIES:
:CUSTOM_ID: dont-repeat-yourself-dry
:END:
[ein Commit angeben, bei dem duplizierter Code/duplizierte Logik
aufgelöst wurde; Code-Beispiele (vorher/nachher); begründen und
Auswirkung beschreiben]
Das Interface SubCommand definiert einen CLI SubCommand, der wiederum einzelne Funktionen hat.
Diese einzelnen Funktionen werden in einer Map gespeichert, welche den Namen auf die Methode mappt.
Die Logik hierfür war zunächst in jeder Implementation von SubCommand gleich implementiert.
#+begin_src java
public class CategoryCommands extends Subcommand {
final private CategoryCliAdapter categoryCliAdapter;
final private HashMap<String, Function<String[], String>> commands =
new HashMap<>();
@Override
public String executeSubcommand(String[] args) {
return commands.get(args[0]).apply(args);
}
}
#+end_src
Indem das Interface SubCommand zu einer abstrakten Klasse umgebaut wurde, wurde die Logik an eine zentrale Stelle verschoben und zusätzlich gleich das benötigte Errorhandling eingebaut.
#+begin_src java
abstract public class Subcommand {
public String executeSubcommand(String[] args);
final public HashMap<String, Function<String[], String>> commands =
new HashMap<>();
abstract public String getSubcommand();
abstract public String getUsage();
public String executeSubcommand(String[] args) {
try {
commandExsits(args[0]);
return commands.get(args[0]).apply(args);
}
catch (IndexOutOfBoundsException e) {
throw new CliError("Missing a value! " +
getUsage());
}
}
private void commandExsits(String command) {
if (commands.get(command) == null) {
throw new CliError("Subcommand does not exist! " +
getUsage());
}
}
}
#+end_src
#+RESULTS:
Die angebenen Änderungen sind im Commit [[https://tea.filefighter.de/qvalentin/LinkDitch/commit/78730bc69f][78730bc69f]] sichtbar. Da die neuen Implementation des Interfaces im selben Commit hinzugefügt wurden ist, wurden diese direkt ohne den duplizierten Code committet.
* Kapitel 5: *Unit Tests*
:PROPERTIES:
:CUSTOM_ID: kapitel-5-unit-tests
:END:
*** 10 Unit Tests
:PROPERTIES:
:CUSTOM_ID: unit-tests
:END:
[Nennung von 10 Unit-Tests und Beschreibung, was getestet wird]
1. CategoryIdTest#ConstructorWorks
Stellt sicher, dass eine CategoryId mit einem int erstellt werden kann.
Da ich vorher noch nie Java-Records verwendet hatte, war es ganz gut mit einem kleinen Test deren Funktionalität zu überprüfen.
Gerne hätte ich auch einen Test zur Unveränderbarkeit von Records gemacht, beim Versuch einen solchen zu schreiben wurde allerdings klar, dass es keine Methoden gibt die Veränderungen bewirken und Records somit die Anforderungen erfüllen (auch wenn es nicht testbar ist).
2. CategoryId#equalsWorks
Stellt sicher, dass zwei CategoryIds die mit dem selben int erstellt wurden durch die equals Methode als gleich angesehen werden.
Erneut eine Überprüfung, dass Records sich wie erwartet verhalten.
3. CategoryNameTest#getNameWorks
Stellt sicher, dass der Getter für Name den erwarteten Wert zurück liefert.
4. CategoryNameTest#constructorThrowsNull,constructorThrowsBlank,constructorThrowsEmpty,constructorThrowsTooShort
Stellen sicher, dass die Regeln die für den Namen einer Category definiert sind auch korrekt überprüft werden und im Fehlerfall eine entsprechende Exeption geschmissen wird.
5. CategoryEntityTest#categoryConversionWorks
Stellt sicher, dass bei der Konvertierung zwischen Category und CategorEntity durch die Funktionen toCategory und den Konstruktor.
Die Komposition der beiden Funktionen sollte die Identitätsfunktion ergeben.
6. LinkEntityTest#toCSVString
Stellt sicher, dass das serialisieren eines Objektes zu einem CSV String das erwartete Ergebnis liefert.
7. LinkEntityTest#fromCSVString
Stellt sicher, dass das de-serialisieren eines CSV Strings das erwartete Ergebnis liefert.
8. CategoryCommandsTest#addCommandWorks
Stellt sicher, dass beim Aufruf der Methode executeSubcommand mit dem String "add" und einem CategoryNamen die korrekte Methode des Adapters aufgerufen wird und die Parameter korrekt weitergereicht werden und eine passende Erfolgsmeldung geliefert wird.
9. GenericCSVDAOTest#addWorks
Stellt sicher, dass nach dem Hinzufügen eines CategorEntitys dieses auch wieder gefunden werden kann.
10. GitHubTagMatcherTest#gettingDescriptionWorks
Überprüft die Interaktion mit der GitHub Repository API indem für ein konkretes Repository der Wert abgefragt wird.
*** ATRIP: Automatic
:PROPERTIES:
:CUSTOM_ID: atrip-automatic
:END:
[Begründung/Erläuterung, wie Automatic' realisiert wurde]
Automatic wurde durch die einfache Ausführbarkeit realisiert.
So muss nur ein Befehl ausgeführt werden um die Test zu starten.
#+begin_src
mvn clean test
#+end_src
Alternativ genügen auch wenige Clicks bzw. Shortcuts in der IDE um die Test auszuführen und detailreiches Feedback über ihren Erfolg zu erhalten.
[[./img/test-ide.png]]
Die Test laufen ohne Eingaben und liefern immer ein Ergebnis, dass eindeutig Erfolg oder Misserfolg bezeugt.
Damit man die Test nicht immer nur lokal ausführen muss werden sie auch bei jedem einchecken des Codes in das entfernte Versionskontrollrepository auf dem Server ausgeführt.
Dies ist mit Drone CI realisiert und liefert in der Weboberfläche der Versionskontrolle schnelles Feedback.
#+begin_src yaml
---
kind: pipeline
type: docker
name: Tests Coverage
steps:
- name: Run Tests With Coverage
image: maven:3.8-openjdk-17-slim
environment:
SONAR_LOGIN:
from_secret: SONAR_TOKEN
commands:
- mvn clean verify sonar:sonar -s ./settings.xml
trigger:
branch:
include:
- master
trigger:
event:
- push
#+end_src
[[./img/gitea-test.png]]
Mit Drone CI bekommt man dann gleich auch einen guten Überblick, wie oft die Test fehlschlagen und wie lange das Testen braucht.
[[./img/drone-test.png]]
*** ATRIP: Thorough
:PROPERTIES:
:CUSTOM_ID: atrip-thorough
:END:
[jeweils 1 positives und negatives Beispiel zu Thorough'; jeweils
Code-Beispiel, Analyse und Begründung, was professionell/nicht
professionell ist]
**** Positives Beispiel
Beim CategoryNameTest werden (bis auf generierten Code) sämtliche Methoden getestet und sämtliche Sonderfälle für die Eingaben in einzelnen Test geprüft (constructorThrowsNull,constructorThrowsBlank,constructorThrowsEmpty,constructorThrowsTooShort)
[[./img/categoryNameTest.png]]
**** Negatives Beispiel
Beim CategorEntityTest werden bis auf die beiden Koversationsmethoden Richtung Category keine Methoden getestet, obwohl beispielsweise die Konvertierung nach CSV leicht einen Fehler enthalten könnte, der Probleme verursachen würde (z.B. vergessenes toString).
[[./img/CategoryEntityTest.png]]
*** ATRIP: Professional
:PROPERTIES:
:CUSTOM_ID: atrip-professional
:END:
[jeweils 1 positives und negatives Beispiel zu Professional'; jeweils
Code-Beispiel, Analyse und Begründung, was professionell/nicht
professionell ist]
**** Positives Beispiel
Der Test GenericCSVDAOTest verwendet eine Datei. Damit dies sauber abläuft wird eine temporäre Datei verwendet.
#+begin_src java
File file = File.createTempFile("test","link-ditch");
#+end_src
Vor und nach jedem Test wird die Datei gesäubert, damit die Test alle im gleichen Zustand starten.
#+begin_src java
@BeforeEach
public void beforeEach() throws IOException {
if (file.exists()) {
file.delete();
}
file.createNewFile();
this.sut = new GenericCSVDAO<>(file, CategoryEntity::new);
}
@AfterEach
void afterEach() {
file.delete();
}
#+end_src
Damit die Tests lesbarer sind wird das recht aufwendige erzeugen eines CategoryEntitys und hinzufügen dessen in eine private Hilfsfunktion ausgelagert.
#+begin_src java
private CategoryEntity addDummyEntity(String categoryName, int id) {
var entityToAdd = new CategoryEntity(categoryName, id);
sut.add(entityToAdd);
return entityToAdd;
}
#+end_src
Somit liest sich der removeAllWorks Test deutlich besser und duplizierter Code wird vermieden, was wiederum Fehler vermeidet.
#+begin_src java
@Test
public void removeAllWorks() throws IOException {
addDummyEntity("categoryName1", 101);
addDummyEntity("categoryName2", 102);
assertEquals(2, sut.getALl().size());
sut.removeAll();
assertEquals(0, sut.getALl().size());
}
#+end_src
**** Negatives Beispiel
Der Test addCommandWorks ist nicht sehr professionell.
Es werden schlechte Variablennamen wie category1 und category2 verwendet.
Die Verwendung des ArgumentCaptors macht den Code schlecht lesbar.
Immerhin werden Variablen verwendet und nicht die Strings an allen Stellen hardgecoded.
#+begin_src java
@Test
void addCommandWorks() {
var url = "http://tea.filefighter.de";
var username = "mario";
var category1 = "funStuff";
var category2 = "workStuff";
ArgumentCaptor<String> captureUrl = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<String> captureUsername = ArgumentCaptor.forClass(String.class);
ArgumentCaptor<Set<String>> captureCategories = ArgumentCaptor.forClass(Set.class);
doNothing()
.when(mockAdapter)
.addLink(captureUrl.capture(), captureCategories.capture(), captureUsername.capture());
var sut = new LinkCommands(mockAdapter);
var returnValue = sut.executeSubcommand(new String[]{"add", url, username, category1, category2});
assertEquals("Added the new Link", returnValue);
assertEquals(url, captureUrl.getValue());
assertEquals(username, captureUsername.getValue());
assertEquals(Set.of(category1, category2), captureCategories.getValue());
}
#+end_src
*** Zusatz: ATRIP: Repeatable
Der Commit [[https://tea.filefighter.de/qvalentin/LinkDitch/commit/d1fdad7cf9][d1fdad7cf9]] zeigt den Fix für einen Test der nicht repeatable war, weil bei Sets die Reihenfolge der Elemente nicht eindeutig ist und somit zufällig.
*** Code Coverage
:PROPERTIES:
:CUSTOM_ID: code-coverage
:END:
[Code Coverage im Projekt analysieren und begründen]
Die Coverage ist mit etwa 25 % deutlich zu niedrig.
Da es sich bei dem Projekt jedoch um Code handelt, der niemals wirklich produktiv eingesetzt werden wird und der nicht langfristig weiterentwickelt wird, ist dies verkraftbar.
Es wurden hauptsächlich die komplizierteren Stellen getestet, wie beispielsweise die CSV-Persistierung und die Interaktion mit der Github-APi.
Bei diesen Stellen wurden grade genug Test geschrieben, um sicherzustellen, dass die Grundfunktion korrekt ist.
Teilweise wurde während des Entwicklungsprozesses gemerkt, dass es besser gewesen wäre, manche Stellen zu testen.
Für Fehler die beim manuellen Testen der Anwendung aufgefallen waren wurden teilweise extra Tests geschrieben.
*** Fakes und Mocks
:PROPERTIES:
:CUSTOM_ID: fakes-und-mocks
:END:
[Analyse und Begründung des Einsatzes von 2 Fake/Mock-Objekten;
zusätzlich jeweils UML Diagramm der Klasse]
1. CategoryCommandsTest:
Beim CategoryCommandsTest wurde die Klasse CategoryCommands erstellt, doch anstatt ein Objekt der Klasse CategoryCliAdapter beim Erstellen zu übergeben wurde ein Mock übergeben.
[[./uml/CategoryCommandsTest.png]]
Dieses Mock wird dann aufgerufen und die Parameter des Aufrufs werden überprüft.
Zusätzlich wird definiert, welche Rückgabewerte das Mock liefern soll.
#+begin_src java
class CategoryCommandsTest {
CategoryCliAdapter mockAdapter = mock(CategoryCliAdapter.class);
@Test
void addCommandWorks() {
var categoryName = "funStuff";
ArgumentCaptor<String> valueCapture = ArgumentCaptor.forClass(String.class);
doNothing().when(mockAdapter).addCategory(valueCapture.capture());
var sut = new CategoryCommands(mockAdapter);
var returnValue = sut.executeSubcommand(new String[]{"add", categoryName});
assertEquals(categoryName, valueCapture.getValue());
assertEquals("Added the new category", returnValue);
}
@Test
void getCommandWorks() {
var sut = new CategoryCommands(mockAdapter);
when(mockAdapter.getCategories()).thenReturn(Set.of("funStuff", "workStuff"));
var returnValue = sut.executeSubcommand(new String[]{"get"});
var expected =
"Available Categories:" + System.lineSeparator() + "funStuff" + System.lineSeparator() + "workStuff";
var expectedDifferentOrder =
"Available Categories:" + System.lineSeparator() + "workStuff" + System.lineSeparator() + "funStuff";
assertTrue(expected.equals(returnValue) || expectedDifferentOrder.equals(returnValue));
}
}
#+end_src
Das Mock ist hier besonders nützlich, da wir uns an der Grenze zwischen zwei Schichten befinden.
Für das Erstellen des CategoryCliAdapters wird eine Usecase benötigt, welcher wiederum Instanzen aus der Domäne benötigt, welche wiederum bestimmte Instanzen benötigen.
Indem wir stattdessen ein Mock erstellen werden quasi alle anderen Schichten weg abstrahiert.
Dies ist auch empfehlenswert, da wir beim aktuellen Unittest ja nur die Funktionalität der aktuellen Klasse testen wollen.
Deshalb definieren wir durch das Mock, wie sich der Rest der Anwendung verhalten sollte und können uns auf unsere aktuell Klasse konzentrieren und sind unabhängig von eventuellen Bugs in anderen Bereichen oder fehlenden Implementationen.
2. LinkCommandsTest:
[[./uml/LinkCommandsTest.png]]
Beim LinkCommandsTest wurde die Klasse LinkCommands erstellt, doch anstatt ein Objekt der Klasse LinkCliAdapter beim Erstellen zu übergeben wird ein Mock übergeben.
Auch hier befinden wir uns an der Grenze von zwei Schichten.
* Kapitel 6: Domain Driven Design
:PROPERTIES:
:CUSTOM_ID: kapitel-6-domain-driven-design
:END:
*** Ubiquitous Language
:PROPERTIES:
:CUSTOM_ID: ubiquitous-language
:END:
[4 Beispiele für die Ubiquitous Language; jeweils Bezeichung, Bedeutung
und kurze Begründung, warum es zur Ubiquitous Language gehört]
| Bezeichung | Bedeutung | Begründung |
| Link | Steht für eine eindeutige URL, die von der Anwendung gespeichert werden soll | Gehört zur Ubiquitous-Laguage, weil Link außerhalb der Domäne der Anwendung auch anders verwendet werden kann |
| Category | Steht für eine Kategorie, die einem Link zugeordnet werden kann | Gehört zur Ubiquitous-Laguage, weil nur im Kontext der Anwendung klar ist, wofür die Kategorien verwendet werden |
| Tag | Steht für einen Tag der automatisch bestimmten Link-Typen zugeordnet wird | Gehört zur Ubiquitous-Laguage, weil nur im Kontext der Anwendung klar ist, was genau getaggt wird und wie dies geschieht |
| *TagMatcher | Steht für eine spezielle Implementation des Interfaces TagMatcher (z.B. GitHubTagMatcher), das dafür sorgt, dass einem Link ein Tag zugewiesen werden kann | Gehört zur Ubiquitous-Laguage, weil nur im Kontext der Anwendung klar ist, auf welche Tag sich die Funktion bezieht und was deren Bedeutung ist. |
*** Entities
:PROPERTIES:
:CUSTOM_ID: entities
:END:
[UML, Beschreibung und Begründung des Einsatzes einer Entity; falls
keine Entity vorhanden: ausführliche Begründung, warum es keines geben
kann/hier nicht sinnvoll ist]
*** Value Objects
:PROPERTIES:
:CUSTOM_ID: value-objects
:END:
/[UML, Beschreibung und Begründung des Einsatzes eines Value Objects;
falls kein Value Object vorhanden: ausführliche Begründung, warum es
keines geben kann/hier nicht sinnvoll ist]/
Klasse: Category Name
*** Repositories
:PROPERTIES:
:CUSTOM_ID: repositories
:END:
[UML, Beschreibung und Begründung des Einsatzes eines Repositories;
falls kein Repository vorhanden: ausführliche Begründung, warum es
keines geben kann/hier nicht sinnvoll ist]
*** Aggregates
:PROPERTIES:
:CUSTOM_ID: aggregates
:END:
[UML, Beschreibung und Begründung des Einsatzes eines Aggregates; falls
kein Aggregate vorhanden: ausführliche Begründung, warum es keines geben
kann/hier nicht sinnvoll ist]
* Kapitel 7: Refactoring
:PROPERTIES:
:CUSTOM_ID: kapitel-7-refactoring
:END:
*** Code Smells
:PROPERTIES:
:CUSTOM_ID: code-smells
:END:
/[jeweils 1 Code-Beispiel zu 2 Code Smells aus der Vorlesung; jeweils
Code-Beispiel und einen möglichen Lösungsweg bzw. den genommen
Lösungsweg beschreiben (inkl./ /(Pseudo-)Code)]/
**** Duplicated Code
Da es bei Java keine Funktion zum durchsuchen eines Sets gibt wurde an mehreren Stellen ein Konstrukt, wie unten sichtbar
verwendet.
Dies macht den Code unleserlich und schwerer zu warten.
#+begin_src java
// LinkRepository.java
public Optional<Link> getById(LinkId id) {
return links.stream().filter(link -> link.getId().equals(id)).findFirst();
}
public Optional<Link> getByUrl(LinkUrl url) {
return links.stream().filter(link -> link.getUrl().equals(url)).findFirst();
}
#+end_src
Durch die Einführung des Dekorator-Entwurfsmuster für Set wurde jedoch auch eine eigene Implementation eines Sets eingeführt.
Dadurch konnte diese Set Implementation auch einfach durch eine find Methode ergänzt werden, wie dargestellt.
#+begin_src java
// CustomStrictSet.java
@Override
public Optional<T> find(Predicate<T> predicate) {
return set.stream().filter(predicate).findFirst();
}
#+end_src
So wurde die Codezeile an vier Stellen ersetzt.
Wenn das Refactoring nicht recht früh durchgeführt worden wäre, wären es eventuell sogar mehr Stellen
geworden.
#+begin_src java
// LinkRepository.java
public Optional<Link> getById(LinkId id) {
return links.find(link -> link.getId().equals(id));
}
public Optional<Link> getByUrl(LinkUrl url) {
return links.find(link -> link.getUrl().equals(url));
}
#+end_src
Das Refactoring wurde mit Commit [[https://tea.filefighter.de/qvalentin/LinkDitch/commit/e4f167074250e791f293eb834a60eb9f63a34664][e4f1670742]] durchgeführt.
*** 2 Refactorings
:PROPERTIES:
:CUSTOM_ID: refactorings
:END:
[2 unterschiedliche Refactorings aus der Vorlesung anwenden, begründen,
sowie UML vorher/nachher liefern; jeweils auf die Commits verweisen]
* Kapitel 8: Entwurfsmuster
:PROPERTIES:
:CUSTOM_ID: kapitel-8-entwurfsmuster
:END:
/[2 unterschiedliche Entwurfsmuster aus der Vorlesung (oder nach
Absprache auch andere) jeweils sinnvoll einsetzen, begründen und
UML-Diagramm]/
*** Entwurfsmuster: Dekorator
:PROPERTIES:
:CUSTOM_ID: entwurfsmuster-name
:END:
*** Entwurfsmuster: [Name]
:PROPERTIES:
:CUSTOM_ID: entwurfsmuster-name-1
:END: