DEV Community

loading...

Discussion on: Clean Architecture in TypeScript Projekt: Angular als Plugin

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.