Clean Architecture in TypeScript Projekt: Angular als Plugin
Tags: typescript, architecture, angular, german
Published: 2019-11-30 22:25:00

Im letzten Teil des Beitrags haben wir die Business Logik unseres Aufgaben Tools programmiert. In diesem Teil geht es darum, unsere Anwendung zum Leben zu erwecken.
Ich verwende hierzu Angular und ich gehe davon aus, dass du dich damit schon auskennst, weil ich nicht näher auf die Installation der CLI etc. eingehen werde.

<hr>

## Neues Angular Projekt

Als erstes starten wir, außerhalb des Verzeichnisses des letzten Beitrags, ein neues Angular Projekt.

```bash
$ ng new todo-app

Would you like to add Angular routing? No
Which stylesheet format would you like to use? CSS
CREATE todo-app/README.md (1024 bytes)
CREATE todo-app/.editorconfig (246 bytes)
CREATE todo-app/.gitignore (629 bytes)
CREATE todo-app/angular.json (3825 bytes)
CREATE todo-app/package.json (1307 bytes)
CREATE todo-app/tsconfig.json (435 bytes)
CREATE todo-app/tslint.json (1621 bytes)
CREATE todo-app/src/favicon.ico (5430 bytes)
CREATE todo-app/src/index.html (294 bytes)
CREATE todo-app/src/main.ts (372 bytes)
CREATE todo-app/src/polyfills.ts (2841 bytes)
CREATE todo-app/src/styles.css (80 bytes)
CREATE todo-app/src/test.ts (642 bytes)
CREATE todo-app/src/browserslist (388 bytes)
CREATE todo-app/src/karma.conf.js (1021 bytes)
CREATE todo-app/src/tsconfig.app.json (166 bytes)
CREATE todo-app/src/tsconfig.spec.json (256 bytes)
CREATE todo-app/src/tslint.json (244 bytes)
CREATE todo-app/src/assets/.gitkeep (0 bytes)
CREATE todo-app/src/environments/environment.prod.ts (51 bytes)
CREATE todo-app/src/environments/environment.ts (662 bytes)
CREATE todo-app/src/app/app.module.ts (314 bytes)
CREATE todo-app/src/app/app.component.css (0 bytes)
CREATE todo-app/src/app/app.component.html (1120 bytes)
CREATE todo-app/src/app/app.component.spec.ts (984 bytes)
CREATE todo-app/src/app/app.component.ts (212 bytes)
CREATE todo-app/e2e/protractor.conf.js (752 bytes)
CREATE todo-app/e2e/tsconfig.e2e.json (213 bytes)
CREATE todo-app/e2e/src/app.e2e-spec.ts (637 bytes)
CREATE todo-app/e2e/src/app.po.ts (251 bytes)
...
```

## Business Logik hinzufügen
Als nächstes kopierst du den Inhalt deiner Business Logik, aus dem letzten Beitrag, nach `todo-app/src/app`, damit folgende Struktur entsteht:

```
todo-app/
|- src/
|  |- app/
|  |  |- core/
|  |  |  |- entity/
|  |  |  |- repository/
|  |  |  |- use-case/
|  |  |- data/
|  |  |- infrastructure/
|  |  |- presentation/
|  |  |- app.module.ts

...
```

## Angular Core Module erzeugen

**Dieser Schritt entfällt da die UseCases mit** `@Injectable({providedIn: 'root'})` **dekoriert werden.**

[Weiter zum nächsten Schritt](#dependency-injection-fehler-beheben)


~~Als nächstes erzeugen wir ein Core Modul für Angular. Darin werden all unsere Use Cases und Services registriert.~~

```bash
$ ng g m core
```

~~Nun sollte eine Datei `src/app/core/core.module.ts` bei dir angelegt worden sein, welche du nun öffnest.
In dem `@NgModule` Decorator fügst du nun das Feld `providers: []` hinzu, in welchem du deine UseCases hinterlegst.~~

```typescript
// src/app/core/core.module.ts
import {AddToDoUseCase, DeleteToDoUseCase, EditToDoUseCase, ShowToDoListUseCase} from './use-case';

@NgModule({
  // ...
  providers: [
    AddToDoUseCase,
    DeleteToDoUseCase,
    EditToDoUseCase,
    ShowToDoListUseCase,
  ]
})
export class CoreModule {
}
```

~~Nun musst du noch das `CoreModule` in dem `AppModule` importieren, damit in der Angular Anwendung auf die UseCases zugegriffen werden kann.~~

```typescript
// src/app/app.module.ts
import {CoreModule} from './core/core.module';

@NgModule({
  imports: [
    CoreModule, // <--
  ],
})
export class AppModule { }
```

## Dependency Injection Fehler beheben
Wenn du jetzt mit `ng serve` die Anwendung 'servierst', wirst du beim Aufruf der Anwendung nichts sehen und in der Konsole einen Fehler wie `Can't resolve all parameters for AddToDoUseCase: (?, ?, ?).` sehen.

Das liegt  daran, dass Angular die Abhängigkeiten nicht automatisch injiziert. Hierfür gibt es nun zwei Lösungen.

1. Du verdrahtest alles manuell im `CoreModule` (`{useClass: AddToDoUseCase, dependencies: [...]}`)
2. Du fügst den UseCases die `@Injectable()` Decorator von Angular hinzu.

Beide Lösungen haben Vor- und Nachteile. Bei der ersten Lösung hältst du deinen Code getrennt vom Angular Kram, du hast jedoch jede Menge zu tun um Services zu verdrahten.
Bei der zweiten Lösung koppelst du Angular Code an deine UseCases, sparst dir aber auf der anderen Seite viel Schreibarbeit.

Und damit kommen wir zu dem 'Beweis' dass sich Architekturen in der Praxis selten 1:1 umsetzen lassen. Ich persönlich habe Variante 1 ausprobiert, diese hat mich jedoch extrem ausgebremst, weswegen ich mittlerweile Variante 2 nutze - auch wenn es nicht schön ist. Aber hey, es ist lediglich ein Decorator der mir die Arbeit sehr vereinfacht und die Fehlerrate reduziert (falsch verdrahtete Services etc.) 😅.
(Wer für Lösung 1 eine Alternative kennt, bitte kommentieren 🙂)


Deine Aufgabe besteht nun darin, alle vier UseCases mit `@Injectable({providedIn: 'root'})` zu dekorieren.

Nach dem du das erledigt hast, sollte die Anwendung ohne Probleme laden.

## Presentation Layer
Als nächstes machen wir uns an die Liste der Aufgaben. Generiere hierzu einfach mit `ng g c presentation/todo-list` die Angular Komponente.

Als nächstes ersetzt du noch den Inhalt der Datei `src/app/app.component.html` mit

```html
<h1>Meine Aufgaben</h1>
<app-todo-list></app-todo-list>
```

### ViewModel und Presenter anlegen
Wie du dich sicherlich erinnerst, verwenden unsere UseCases einen Presenter um Daten darzustellen. Presenter bereiten die Daten entsprechend für die View auf und schreiben die Daten in das ViewModel.

Ich habe mir als Namenskonvention `[component].view-model.ts` und `[component].presenter.ts` angeeignet. Somit haben die Dateinamen im Verzeichnis der Komponente den gleichen Aufbau.


#### ViewModel

Da wir in unserer Anwendung die Aufgaben nicht wirklich für die View aufbereiten müssen, reicht es wenn wir einfach das `ToDo` Entity missbrauchen und dem ViewModel eine Property vom Typen `ToDo[]` geben.

```typescript
// src/app/presentation/todo-list/todo-list.view-model.ts
import {ToDo} from '../../core/entity';

export class TodoListViewModel {
  public toDos: ToDo[] = null;
}
```

#### Presenter

Weiter geht es mit dem Presenter. Wenn du dir jetzt denkst, dass du da den `ShowToDoListPresenter` verwenden musst, liegst du goldrichtig!

Unser `TodoListPresenter` erweitert einfach den `ShowToDoListPresenter<T>`. Als Typ-Argument geben wir das gerade angelegte `TodoListViewModel` an. Außerdem rufen wir im Konstruktor `super(TodoListViewModel);` auf.

Jetzt musst du nur noch die `displayToDos` Methode implementieren, welche lediglich `toDos` in das ViewModel schreibt, und schon ist unser Presenter für die Aufgabenliste fertig.

```typescript
// src/app/presentation/todo-list/todo-list.presenter.ts
import {ShowToDoListPresenter} from '../../core/use-case';
import {TodoListViewModel} from './todo-list.view-model';
import {ToDo} from '../../core/entity';

export class TodoListPresenter extends ShowToDoListPresenter<TodoListViewModel> {

  constructor() {
    super(TodoListViewModel);
  }

  public displayToDos(toDos: ToDo[]): void {
    this.viewModel.toDos = toDos;
  }
}
```

#### TodoListPresenter bekannt machen
Aktuell weiß Angular noch nicht, was für `ShowToDoListPresenter<T>` injected werden soll. Um das zu ändern, erzeugst du erst einmal das `PresentationModule`
```bash
$ ng g m presentation
```

Anschließend referenzierst du dieses im `AppModule`. In diesem entfernst du zusätzlich die Verwendung der `TodoListComponent`.

```typescript
// src/app/app.module.ts
import {BrowserModule} from '@angular/platform-browser';
import {NgModule} from '@angular/core';

import {AppComponent} from './app.component';
import {CoreModule} from './core/core.module';
import {PresentationModule} from './presentation/presentation.module';

@NgModule({
  declarations: [
    AppComponent,
  ],
  imports: [
    BrowserModule,
    CoreModule,
    PresentationModule,
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {
}
```

Im `PresentationModule` fügst du unter `declarations` und `exports` die `TodoListComponent` hinzu und verdrahtest unter `providers` die beiden Presenter.

```typescript
// src/app/presentation/presentation.module.ts
import {NgModule} from '@angular/core';
import {CommonModule} from '@angular/common';
import {TodoListComponent} from './todo-list/todo-list.component';
import {ShowToDoListPresenter} from '../core/use-case';
import {TodoListPresenter} from './todo-list/todo-list.presenter';

@NgModule({
  declarations: [
    TodoListComponent, // <--
  ],
  imports: [
    CommonModule,
  ],
  exports: [
    TodoListComponent, // <--
  ],
  providers: [
    {provide: ShowToDoListPresenter, useClass: TodoListPresenter}, // <--
  ]
})
export class PresentationModule {
}

```


### Component zum Leben erwecken
Nach wie vor macht unsere Anwendung nichts als
> # Meine Aufgaben
> todo-list works!

ausgeben. Das wollen wir jetzt ändern.

Injecte zunächst den `ShowToDoListUseCase` als `private readonly` und den `ShowToDoListPresenter<T>` als `public readonly`.
Im body des Konstruktors rufst du dann `presenter.reset()` auf, wodurch das ViewModel neu initialisiert wird. Danach rufst du `useCase.execute()` auf, um die Business Logic unserer Aufgaben-Liste 

```typescript
// src/app/presentation/todo-list/todo-list.component.ts
constructor(private readonly useCase: ShowToDoListUseCase,
            public readonly presenter: ShowToDoListPresenter<TodoListViewModel>,
) {
  presenter.reset();

  useCase.execute();
}
```

Wenn du die Seite jetzt wieder Aufrufst, erscheint wieder ein Fehler. 
Die Ursache ist in Zeile 3 zu finden.

```
AppComponent.ngfactory.js? [sm]:1 ERROR Error: StaticInjectorError(AppModule)[EditToDoUseCase -> TodoRepository]: 
  StaticInjectorError(Platform: core)[EditToDoUseCase -> TodoRepository]: 
    NullInjectorError: No provider for TodoRepository!

...
```

Klar, wir haben ja noch gar keine Datenquelle für unsere Aufgaben.

## Daten Layer

Als nächstes müssen wir das `TodoRepository` implementieren. Dieses wird sich im `DataModule` befinden, welches mit `ng g m data` angelegt wird.
Dieses musst du, wie auch schon die anderen Module im `AppModule` importieren.

### TodoRepository implementieren

Das Repository ist nichts anderes als ein Angular Service. Diesen erzeugst du mit `ng g s data/repository/todo-repository`. Die Klasse selber muss natürlich vom `TodoRepository` aus dem Core extenden.

Um das Beispiel einfach zu halten, werden die Aufgaben nur temporär gespeichert, also nach einem Neuladen der Seite ist die Aufgabenliste wieder leer.

```typescript
// src/app/data/repository/todo-repository.service.ts
import {Injectable} from '@angular/core';
import {TodoRepository} from "../../core/repository";
import {ToDo} from "../../core/entity";

@Injectable({
  providedIn: 'root'
})
export class TodoRepositoryService extends TodoRepository {

  private toDos: ToDo[] = [];

  public async createToDo(todo: ToDo): Promise<ToDo> {
    this.toDos.push(todo);
    return this.toDos[this.toDos.length - 1];
  }

  public async deleteToDo(id: number): Promise<void> {
    if (this.toDos[id] === null) {
      throw new Error('Diese Aufgabe existiert nicht.');
    }

    this.toDos.splice(id, 1);

    return;
  }

  public async editToDo(id: number, todo: ToDo): Promise<ToDo> {
    if (this.toDos[id] === null) {
      throw new Error('Diese Aufgabe existiert nicht.');
    }

    this.toDos[id] = todo;
    return this.toDos[id];
  }

  public async getAllToDos(): Promise<ToDo[]> {
    return this.toDos;
  }
}
```

Auch in diesem Fall muss der Service im `DataModule` bereitgestellt werden
```typescript
// src/app/data/data.module.ts
providers: [
  {provide: TodoRepository, useClass: TodoRepositoryService}
]
```

Wenn du die Seite jetzt neu lädst sollte der Fehler verschwinden.

## Liste + Aktionsbuttons rendern
Na, schon aus der Puste?🙂 Ist nicht mehr viel, versprochen.
Als nächstes soll die Liste mit den Aufgaben sowie Buttons zum anlegen, bearbeiten und löschen angezeigt werden.

Wechsle dafür erst einmal in das Template der `TodoListComponent`.

```html
<!-- src/app/presentation/todo-list/todo-list.component.ts -->
<button (click)="addToDo()">Aufgabe hinzufügen</button>
<ul>
  <li *ngFor="let todo of presenter.viewModel.toDos; index as id">
    <label>
      <input type="checkbox" [checked]="todo.isDone" (change)="setToDoState(id, todo)">
      {{ todo.description }}
    </label>
    <button (click)="editToDo(id, todo)">Bearbeiten</button>
    <button (click)="deleteToDo(id)">Löschen</button>
  </li>
</ul>
```

Sollte soweit verständlich sein. Der erste Button ist zum hinzufügen von Aufgaben, die ungeordnete Liste zeigt die Aufgaben an, wobei die Checkbox den Status der Aufgabe anzeigt. Mit den beiden Buttons lässt sich die Aufgabe bearbeiten bzw. löschen.

Als nächstes müssen wir den Code für die Buttons schreiben. Wechsle also in den TypeScript Teil der Komponente.
Dort fügst du als erstes einmal die anderen UseCases als `private readonly` im Konstruktor hinzu.
```typescript
// src/app/presentation/todo-list/todo-list.component.ts
constructor(private readonly useCase: ShowToDoListUseCase,
            public readonly presenter: ShowToDoListPresenter<TodoListViewModel>,
            private readonly addToDoUseCase: AddToDoUseCase, // <--
            private readonly editToDoUseCase: EditToDoUseCase, // <--
            private readonly deleteToDoUseCase: DeleteToDoUseCase, // <--
) {
  presenter.reset();

  useCase.execute();
}
```

Anschließend fügst du noch die Funktionen hinzu, welche die Buttons im Template aufrufen.

```typescript
// src/app/presentation/todo-list/todo-list.component.ts
public addToDo() {
  this.addToDoUseCase.execute();
}

public setToDoState(id: number, todo: ToDo) {
  this.editToDoUseCase.execute({id, todo, onlyToggleDone: true})
}

public editToDo(id: number, todo: ToDo) {
  this.editToDoUseCase.execute({id, todo, onlyToggleDone: false})
}

public deleteToDo(id: number) {
  this.deleteToDoUseCase.execute(id);
}
```

Wenn du jetzt die Seite lädst, dann sollte... es wieder ein Fehler geben 😉
```
AppComponent.ngfactory.js? [sm]:1 ERROR Error: StaticInjectorError(AppModule)[EditToDoUseCase -> InteractionService]: 
  StaticInjectorError(Platform: core)[EditToDoUseCase -> InteractionService]: 
    NullInjectorError: No provider for InteractionService!
```

Stimmt, unser Interaction Service fehlt noch.


## Infrastruktur Modul und InteractionService
Als letzten Schritt, muss noch das Infrastruktur Modul sowie der InteractionService erzeugt werden. (Modul nicht vergessen zu importieren)

```bash
$ ng g m infrastructure 
$ ng g s infrastructure/service/interaction
```

Im `InfrastructureModule` verlinkst du wieder den Service aus dem Core:
```typescript
// src/app/infrastructure/infrastructure.module.ts

import * as CoreService from "../core/service";

// ...

providers: [
  {provide: CoreService.InteractionService, useClass: InteractionService},
]
```

Die Implementierung des `InteractionService` ist auch überschaubar, hier verwenden wir einfach die Standard JS Funktionen `confirm` und `prompt`.
```typescript
import {Injectable} from '@angular/core';
import * as CoreService from "../../core/service";

@Injectable({
  providedIn: 'root'
})
export class InteractionService implements CoreService.InteractionService {

  public async confirm(message: string): Promise<boolean> {
    return confirm(message);
  }

  public async enterString(currentValue?: string): Promise<string> {
    return prompt("Eingabe:", currentValue);
  }
}

```

Und damit bist du fertig🙂.

Falls es bei dir Probleme gibt, schau mal bei [GitHub](https://github.com/devtronic/clean-architecture-typescript) vorbei. Dort findest du den kompletten Code. 

## Zusammenfassung
Du hast heute die Application Logic für die Business Logic aus dem vorherigen Beitrag implementiert. Aus abstrakten Services wurden konkrete Implementationen. Service Abhängigkeiten werden per Dependency Injection injiziert.
Schau dir einmal den tatsächlich geschriebenen Code im Data / Infrastructure / Presentation Layer an. 
Ich persönlich war anfangs ziemlich erstaunt, wie wenig das ist.

Außerdem ist die Kopplung so lose, dass du den TodoRepositoryService problemlos durch einen Service ersetzen könntest, der mit einer API kommuniziert und Daten permanent speichert.


Fragen / Anregungen gerne in die Kommentare 🙂