DEV Community

loading...

Clean Architecture in TypeScript Projekt: Angular als Plugin

devtronic profile image Julian Finkler Updated on ・8 min read

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.


Neues Angular Projekt

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

$ 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)
...
Enter fullscreen mode Exit fullscreen mode

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

...
Enter fullscreen mode Exit fullscreen mode

Angular Core Module erzeugen

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

Weiter zum nächsten Schritt

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

$ ng g m core
Enter fullscreen mode Exit fullscreen mode

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.

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

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

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

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

@NgModule({
  imports: [
    CoreModule, // <--
  ],
})
export class AppModule { }
Enter fullscreen mode Exit fullscreen mode

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

<h1>Meine Aufgaben</h1>
<app-todo-list></app-todo-list>
Enter fullscreen mode Exit fullscreen mode

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.

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

export class TodoListViewModel {
  public toDos: ToDo[] = null;
}
Enter fullscreen mode Exit fullscreen mode

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.

// 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;
  }
}
Enter fullscreen mode Exit fullscreen mode

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

$ ng g m presentation
Enter fullscreen mode Exit fullscreen mode

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

// 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 {
}
Enter fullscreen mode Exit fullscreen mode

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

// 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 {
}

Enter fullscreen mode Exit fullscreen mode

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

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

  useCase.execute();
}
Enter fullscreen mode Exit fullscreen mode

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!

...
Enter fullscreen mode Exit fullscreen mode

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.

// 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;
  }
}
Enter fullscreen mode Exit fullscreen mode

Auch in diesem Fall muss der Service im DataModule bereitgestellt werden

// src/app/data/data.module.ts
providers: [
  {provide: TodoRepository, useClass: TodoRepositoryService}
]
Enter fullscreen mode Exit fullscreen mode

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.

<!-- 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>
Enter fullscreen mode Exit fullscreen mode

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.

// 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();
}
Enter fullscreen mode Exit fullscreen mode

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

// 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);
}
Enter fullscreen mode Exit fullscreen mode

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!
Enter fullscreen mode Exit fullscreen mode

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)

$ ng g m infrastructure 
$ ng g s infrastructure/service/interaction
Enter fullscreen mode Exit fullscreen mode

Im InfrastructureModule verlinkst du wieder den Service aus dem Core:

// src/app/infrastructure/infrastructure.module.ts

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

// ...

providers: [
  {provide: CoreService.InteractionService, useClass: InteractionService},
]
Enter fullscreen mode Exit fullscreen mode

Die Implementierung des InteractionService ist auch überschaubar, hier verwenden wir einfach die Standard JS Funktionen confirm und prompt.

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);
  }
}

Enter fullscreen mode Exit fullscreen mode

Und damit bist du fertig🙂.

Falls es bei dir Probleme gibt, schau mal bei GitHub 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 🙂

Discussion (16)

Collapse
web265p3 profile image
David Elsner • Edited

Das ist ein wirklich schöner Artikel. Habe etwas lernen können!
Doch eine Sache verstehe ich nicht.

Wofür brauchen wir die Provider im Core-Modul?

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

Das ginge doch auch ohne die Use-Cases. Die werden doch ohnehin durch "provideIn="root" zur Verfügung gestellt. Es sollte also auch ohne das funktionieren...

Collapse
devtronic profile image
Julian Finkler Author

Danke für das Lob und den Hinweis 🙂 Ich dachte immer man müsste die immer im Modul registrieren aber tatsächlich reicht das providedIn schon aus, danke! Ich update den Code und den Artikel.

Collapse
web265p3 profile image
David Elsner

Super!
Was mich auch noch interessieren würde:

  1. Aus welchem Grund gibt es eigentlich überhaupt ein Core-Module? In dem anderen Artikel über Clean Architecture mit Angular wurde ebenfalls ein leeres Core-Module angelegt:

medium.com/intive-developers/appro...

Doch den Grund verstehe ich nicht. Es müsste doch gerade gewollt sein das Core-Module nicht zu haben, damit so wenig Framework wie möglich den Core "verschmutzt".

  1. Auch wenn das Providers-Array nicht mehr vom Angular Team empfohlen wird, sehe ich in diesem Fall keinen Weg komplett daran vorbei zu kommen. Schließlich muss diese Definition:

providers: [
{provide: ShowToDoListPresenter, useClass: TodoListPresenter}
]

z.B. wirklich in das PresentationModule (und nicht direkt per provideIn in den ShowToDoListPresenter), weil dort keine konkrete Referenz hingehört. Verstehe ich das richtig?

Thread Thread
devtronic profile image
Julian Finkler Author • Edited

Das sehe ich vollkommen genau so wie du. Deswegen hab ich gestern das Core-Module auch schon rausgeworfen.

Ich hatte es ursprünglich so, wie ich es auch in Teil 1 geschrieben habe, dass ich die Services manuell verdrahtet habe. Das geschah im Core Module. Dadurch, dass die UseCases aber im Nachhinein eh mit @Injectable({providedIn: 'root'}) dekoriert sind, ist das Modul natürlich überflüssig. Ich hab das gestern schon im Code und im Post angepasst :-)

Zu dem verdrahten der Presentern ist mir leider noch kein schöner Weg eingefallen

Thread Thread
web265p3 profile image
David Elsner

okay - dann ist alles klar. Ich war nur so verwirrt, weil ich diese Art von Implementierung zwei Mal gefunden habe. Einmal hier und noch mal in dem englischen Artikel. Zwei Mal ein Core Modul... aber hatte wohl in beiden Fällen den gleichen Ursprung.

Thread Thread
web265p3 profile image
David Elsner • Edited

Noch eine andere Frage: Die Komponenten greifen bei dir direkt auf einen konkreten UseCase zu (es gibt keine abstrakten Klassen dafür (i.e. "interfaces" bei clean architecture))
D.h., dass deine Komponente (das wäre der "Controller") direkt auf die Use cases zugreift ohne Interface dazwischen, wie in dem Buch. War das Absicht oder einfach Pragmatismus? (Siehe Foto aus dem Buch "Clean Architecture") Foto: dev-to-uploads.s3.amazonaws.com/i/...

Thread Thread
devtronic profile image
Julian Finkler Author • Edited

Den UseCase bzw. die boundaries noch mal zu abstrahieren ist überflüssig.
Der UseCase befindet sich ja im Domain Layer, somit kennt der Controller ja den UseCase, jedoch nicht anders herum.

Das InputBoundary ist in meinem Beispiel das TRequest Objekt im UseCase und das OutputBoundary wird durch den TPresenter im UseCase vorgegeben 🙂

github.com/devtronic/clean-archite...

Thread Thread
jim108dev profile image
jim108dev

Hallo, wenn der Controller alles im Core-Layer kennt, warum der Aufwand mit eines UseCase Interfaces und nicht simpler Funktionsaufruf und Aufbereitung des Rückgabewertes?

Thread Thread
devtronic profile image
Julian Finkler Author

Hallo, ich kann dir gerade nicht ganz folgen. Poste mal ein Stück Code, wie du es ohne das Interface machen würdest.

Das Interface selber dient u.A. dafür, dass alle Use Cases nach dem selben Schema aufgebaut sind (TRequest, TPresenter und execute Methode).

Thread Thread
jim108dev profile image
jim108dev

Danke für die schnelle Antwort. Also statt

export class ShowToDoListUseCase
  implements IUseCase<void, ShowToDoListPresenter<any>> {...

gibt's

export class AltShowToDoListUseCase {
  constructor(private readonly repository: TodoRepository) {}

  public async execute(): Promise<ToDo[]> {
    return this.repository.getAllToDos()
  }
}

dann

export class TodoListComponent {
  constructor(...) {
    presenter.reset()

    //useCase.execute()
    const allToDos = altShowToDoUseCase.execute()
    allToDos.then((a) => { ...

Vorteile:

  1. einfacher,
  2. Verzicht auf ShowToDoListPresenter<any>
  3. der UseCase braucht sich nicht nicht mit Präsentation beschäftigen.

Nachteile:

  1. Beliebigkeit des Namens der execute-Methode.
Thread Thread
devtronic profile image
Julian Finkler Author

Der UseCase ist ja dafür gedacht, dass man Geschäftslogik von den Implementierungsdetails trennt.

Um beim Beispiel "Todo Liste" zu bleiben, passen wir mal die Definition des Anwendungsfalls an:

Die Anwendung soll eine Liste der gespeicherten Todos anzeigen. Bevor der Ladevorgang aus der Datenquelle gestartet wird, soll ein Ladeindikator angezeigt werden, welcher nach Abschluss des Ladevorgangs oder beim Auftreten eines Fehlers wieder ausgeblendet werden soll.
Diese Definition ist somit ein Teil der Geschäftslogik und diese soll sich auch nicht in Abhängigkeit, wer diesen UseCase ausführt ändern.

Jetzt hast du zwei Möglichkeiten:

  1. Du setzt in deiner Component, bevor du den UseCase ausführst, eine variable (z.B. isLoading = true), welche du im Template bindest und in einem finally-block wieder auf false setzt oder
  2. du nimmst den abstrakten TodoListPresenter, welcher zusätzlich noch die Methoden showLoadingIndicator() und hideLoadingIndicator oder toggleLoadingIndicator(isLoading: bool); hat, und implementierst diesen in deiner Anwendung.

Beides hat Vor- und Nachteile:

  1. Vorteil: Du sparst dir den Presenter. Nachteil: Das UI kann die Geschäftslogik zerstören. Außerdem musst du, sollte das UI komplett umgebaut werden müssen, die Logik erneut nachbauen.
  2. Vorteil: Du hast alles gekapselt. Die Geschäftslogik macht immer das was sie soll, egal ob sie im CLI oder Web ausgeführt wird. Nachteil: Du hast in der Regel immer zusätzliche Dateien, welche die Implementierung des Presenters enthalten.

Hier muss man aber sagen, dass der Vorteil vom Presenter, nämlich dass das UI nicht die Geschäftslogik zerstören kann, den Nachteil bei weitem wieder ausgleicht.

Daneben hat der Presenter auch noch eine ganz Andere Aufgabe, welche leider aufgrund des Umfangs der Beispiele nicht von mir erklärt wurde:
Aktuell wird in dem Presenter nur das todos-Argument auf das todos-Feld im ViewModel zugewiesen. In der Regel ist es aber so, dass du die Daten, welche vom UseCase kommen, noch einmal aufbereitest, sodass im ViewModel wirklich nur Flache Daten liegen, welche direkt vom Template verwendet werden können. Als Beispiel: willst du eine Liste von Produkten mit Preisen anzeigen, hätte jedes Produkt im ViewModel bereits eine Property displayPrice welche den formatierten Wert inkl. Währung hält.
Warum? Aus Gründen der Testbarkeit. Wir wissen alle wie hart es sein kann, UI zu testen. Wenn wir jedoch dem Template alles fertig geben, müssen wir, ganz naiv gesprochen, nur gegen das ViewModel testen um zu sehen, ob die Ausgabe passt.
Weitere Infos dazu findest du auch hier: Humble Object

Ich hoffe ich konnte es verständlich erklären 🙂

Thread Thread
jim108dev profile image
jim108dev
  1. ok, meine Idee war einfach, dass ein UseCase nur eine Aufgabe haben sollte und alles, was nicht dazugehört um diese zu erfüllen, sollten andere Komponenten übernehmen.
  2. Nehmen wir mal an, alle UseCases bekommen einen Ladeindicator. Bekommen dann alle den selben Presenter? Sie sind verschachtelt, besteht die Gefahr das ein UseCase des anderen Indicator stellt. Bekommen sie unterschiedliche, brauche ich wieder Logik um sie zusammenzuführen.
  3. Wie sieht es denn aus mit Routing? Bekommen UseCases Routing-Interfaces?
  4. Kannst du vielleicht nochmal sagen, warum du Funktionen die eigentlich synchron funktionieren in ein Promise einbettest und dann die Funktion mit await aufrufst. Z. B. bei
  public async enterString(currentValue?: string): Promise<string> {
    return prompt('Eingabe:', currentValue) ?? ''
  }

Aufruf

          todo.description = await this.interaction.enterString(
            todo.description
          )

Im Prinzip das selbe wie

  public enterString(currentValue?: string): string {
    return prompt('Eingabe:', currentValue) ?? ''
Thread Thread
devtronic profile image
Julian Finkler Author

1) Der Use Case hat ja nur eine Aufgabe: er kümmert sich um die Geschäftslogik für diesen einen Anwendungsfall. Da die Geschäftslogik geschäftsübergreifend ist und nicht für diese eine Anwendung gilt, hast du gar keine Möglichkeit die Teilaufgaben sinnvoll auszulagern. (Du kannst und solltest natürlich das Integration Operation Segregation Principle (IOSP) anwenden und die einzelnen Aufgaben in Sub-Methoden unterteilen)

2) In diesem Fall sollte man anfangen zu verallgemeinern. Nehmen wir an, du hast noch 10 weitere Listen.
Dann kannst du ja einfach eine Komponente für alle Liste erzeugen, welche den UseCase "ShowList" hat. Der UseCase erwartet dann im Konstruktor ein kompatibles Repository mit einer allgemeinen Methode, welche die anzuzeigenden zurück liefert. (Kann ja z.B. in Form eines generischen Interface sein).
Der Presenter ist wie gewohnt bei der Komponente implementiert. Wie die Daten dann in eine darstellbare Form gelangen ist wieder ein anderes Thema (wobei bei einer Liste im Grunde ja ein String-Array genügt, welches die Propertynamen der anzuzeigenden Eigenschaften enthält).

Anders sieht es natürlich aus, wenn bei wirklich jedem Use Case ein Ladeindikator angezeigt werden soll.
In dem Fall würde ich im core einen abstrakten "BusyService" bauen, welcher die beiden Methoden show & hide des Presenters enthält. Der Service kann dann im UseCase injected und angesprochen werden werden.
Bei der implementierung würde ich vermutlich auf ein BehaviorSubject zurückgreifen, welches im äußersten Template per async-Pipe verwendet wird. In der show Methode würde ich den Wert inkrementieren und beim hide dekrementieren. Das hat den Vorteil, dass verschaltete show & hide calls sauber funktionieren. (Im Template dann einfach auf > 0 prüfen).

3) Das Thema Routing habe ich bisher so gelöst, dass ich einen abstrakten "RouterService" im core/service abgelegt habe. Darin ist dann pro Seite eine eigene Methode. z.B. showDashboard() oder showTodo(todoId).
Implementiert wird das dann im infrastructure-Modul. (ähnlich wie beim Interaction service)

4) Quasi als Vorsorge 😁 Es könnte ja durchaus sein... Nein es wird definitiv so sein, dass irgendwann ein hübscher asynchroner Dialog den synchronen prompt Dialog ersetzt.
Wir wissen ja: Die einzige Konstante ist die Veränderung

Thread Thread
jim108dev profile image
jim108dev • Edited

ok danke. Wie sieht es denn aus, wenn ich drei Views auf die Todos habe?, Z.B. Menü-Button "Revert Todos" falls mehr als 0 vorhanden, TodoListComponent und View Anzahl Todos im Footer. Ich möchte ja jetzt nicht dreimal die Daten vom Server laden, noch für jede Komponente auf der Seite raten, wurde presenter.reset und usecase.execute schon aufgerufen. Ist ein Usecase jetzt Todo anzeigen oder Seite anzeigen?

Thread Thread
devtronic profile image
Julian Finkler Author

Du darfst natürlich auch mit Subscriptions arbeiten. (Um in der Geschäftslogik nicht von RxJs abzuhängen, würde ich für die verwendeten RxJs-Komponenten minimale, kompatible Interfaces bauen. Z.B. für die (Un-) Subscribe Methoden.)

Im Repository fügst du dann ein Property length$ hinzu, welches die Anzahl der Elemente im Repository emittiert und im UseCase subscribed wird. (Ebenso kannst du auch ein Property elements$ verwenden, welches die aktuellen Elemente emittiert. )
Im Presenter fügst du dann noch eine Methode displayCount(count: number) hinzu, welches vom UseCase bei einer Änderung von length$ gecalled wird.
Somit rufst du die Daten nicht doppelt und dreifach ab.

Ein UseCase stellt ein fachliches Ziel ("business goal") dar. Siehe auch hier: de.wikipedia.org/wiki/Anwendungsfall
Das kann sowohl Todo anzeigen als auch das anzeigen der Liste der Todos sein.

Collapse
m1well profile image
Michael Wellner

Zwei richtig gute Beiträge! Für mich als „Angular Newbie“ sehr verständlich zu lesen.

Forem Open with the Forem app