Repository che esegue internamente chiamate API - DDD

4

Please note: although my question relates to DDD, I am also interested in this from an architectural and OO design perspective. This question may also be a simple case of CQRS and/or interface segregation. Code examples are in Swift as this is the first language that comes to my head right this second, but this is unimportant.

Nel mio dominio, ho il concetto di Panorama che viene scaricato e visualizzato all'utente. Implementazione-saggio: Panorama s verrà scaricato dall'API di Google Street View, memorizzato nella cache e quindi archiviato localmente sul dispositivo.

Ho letto che va bene nascondere qualsiasi cosa un repository deve fare per raggiungere il suo obiettivo dietro la sua API pubblica - non importa se sta salvando / leggendo da un database, o da un endpoint API.

Con questo in mente, ho creato la seguente interfaccia che apparterrà al livello dominio:

protocol Panoramas
{
    public function findAll(): [Panorama]
    public function findOne(_ lat: Double, lon: Double)
    public function delete(_ panorama: Panorama)
    public function save(_ panorama: Panorama)
}

Il mio primo problema è questo: non riesco a chiamare save() sull'API di Google Street View, né posso chiamare delete() . Quindi il mio primo pensiero è stato "leggere e scrivere modelli" e interfacciare la segregazione.

protocol Panoramas
{
    public function findOne(_ lat: Double, lon: Double)
}

protocol QueryablePanoramas: Panoramas
{
    public function findAll(): [Panorama]
}

protocol ModifiablePanoramas: QueryablePanoramas
{
    public function delete(_ panorama: Panorama)
    public function save(_ panorama: Panorama)
}

E vorrei quindi creare le seguenti implementazioni concrete nel livello infrastruttura:

struct StreetviewApiPanoramaRepository: Panoramas { /** ... **/ }
struct InMemoryPanoramaRepository: ModifiablePanoramas { /** ... **/ }
struct DatabasePanoramaRepository: ModifiablePanoramas { /** ... **/ }

Comunque penso che mi manchi qualcosa. Panoramas è un problema relativo al dominio. Ma Queryable o Modifiable : questi sono tecnici per me. È un modello comune? Vuoi nominarli in modo diverso? Come posso migliorare su questo?

-

Modifica: per peggiorare le cose, il Panorama memorizzato internamente è per un file .

struct Panorama {
    public let id: Int // Unique id from the database
    public let filepath: String // The location of the file on-disk
}

  • Il mio archivio di caricamento del database carica i dati tramite un ORM e con il percorso del file memorizzato nel database, ho bisogno di leggere il file per restituirlo. Quindi la mia entità nel database contiene un valore filepath che richiede un secondo passaggio per leggere il file in memoria.
  • Il repository di caricamento della memoria carica semplicemente l'immagine dalla memoria.
  • Il repository di caricamento api legge il file da una chiamata API.

Il problema qui è che un repository dovrebbe restituire un'entità valida. Ma in alcuni casi, ho bisogno di salvare l'immagine in un file e poi memorizzarla, in altri ho bisogno di caricare da una chiamata API.

Penso di aver incasinato il mio modello di dati qui. Per favore aiuto:)

    
posta James 15.09.2018 - 10:37
fonte

1 risposta

2

Le interfacce, i protocolli e l'ereditarietà in generale non sono adatti per descrivere tassonomie di oggetti. Cioè un'interfaccia che descrive "Un repository QueryablePanoramas è un tipo di repository Panoramas che consente anche di elencare tutti i panorami disponibili" è raramente utile.

Invece, è meglio usare le interfacce per descrivere come alcuni oggetti saranno usati . Se i tuoi repository sono consumati da un codice che esegue solo query sul repository e un codice che scrive anche nel repository, allora è logico dividere tali preoccupazioni separate in interfacce separate (questo è il principio di segregazione dell'interfaccia). Ma qui sembra che i consumatori abbiano bisogno di un accesso completo in lettura / scrittura / query ai repository. Quindi, la creazione di più interfacce non aiuta.

Si noti il problema che non tutte le origini dati sono in grado di supportare tutte le operazioni del repository. Ma quelle fonti di dati non sono necessariamente le stesse del repository. Qui, sembra che l'API di Street View sia in grado di fornire alcune funzionalità, ma deve essere abbinata a un archivio persistente per essere completamente funzionale nella tua applicazione. Quindi, sarebbe compito del repository fare quella connessione. Per esempio. (pseudocodice b / c Non so Swift):

struct PanoramaRepository {
  private let storage = ...
  private let streetView = ...

  // first search locally cached panoramas, otherwise fall back to Street View
  public function findOne(lat, lon): Panorama? {
    if (lat, lon) in storage {
      return storage[lat, lon]
    } else {
      return streetView.search(lat, lon)
    }
  }

  // only return the saved panoramaas
  public function findAll(): [Panorama] {
    return storage
  }

  ...
}

Se questo è possibile dipende dal contratto di quel repository. Per esempio. se c'è qualche invariante che il panorama restituito da findOne() deve essere nella lista restituita da findAll() , questo non funzionerà. Ma quando non esiste alcuna restrizione di questo tipo, il repository può essere astratto sulle specifiche API e meccanismi di archiviazione utilizzati.

Ci sono un paio di varianti per fare ciò, a seconda delle tue esigenze. Ad esempio, è possibile utilizzare il pattern decoratore per aggiungere facoltativamente il supporto Street View a un repository esistente:

struct PanoramasWithStreetView(baseRepository: Panoramas) {
  private let streetView = ...

  // use street view as a fall back if the repository doesn't return a result
  public function findOne(lat, lon): Panorama? {
    if let panorama = baseRepository.findOne(lat, lon) {
      return panorama
    } else {
      return streetView.search(lat, lon)
    }
  }

  // all other methods just delegate to the base repository
  public function findAll(): [Panorama] { return baseRepository.findAll() }
  ...
}

let panoramas: Panoramas = PanoramasWithStreetView(PanoramasInDatabase(...))

Potrebbe essere più elegante da implementare, ma aggiunge ulteriore flessibilità che può rendere la tua applicazione più fragile. Per esempio. potresti utilizzare accidentalmente un repository che non utilizza l'API di Street View.

Infine, considera se l'uso dell'API di Street View appartiene davvero a un repository. Se questa API non è solo un'origine dati tra le tante, ma esiste una logica aziendale che tratta i panorami dall'API in modo diverso rispetto ai panorami archiviati localmente, quindi l'estrazione del codice API in un servizio separato potrebbe essere migliore. L'idea del contesto limitato potrebbe aiutare. A quale contesto appartengono le chiamate API? Si tratta di un dettaglio di implementazione dal punto di vista del dominio del problema principale, in modo che queste chiamate API appartengano al contesto "Archiviazione panorama"?

A seconda delle scelte di progettazione, potresti anche dover riconsiderare se la tua definizione di Panorama è sufficiente.

  • Se i panorami possono provenire da più fonti, è difficile specificare un determinato ID. Gli ID dovrebbero avere uno spazio dei nomi (come gli URI), descrivere semplicemente il contenuto (non modificabile!) (Un hash come in blockchains, Git o IPFS), o garantire l'unicità combinando spazio dei nomi, timestamp, casualità e ID locale (come UUID).
  • Considera se Panorama rappresenta un'immagine che può essere visualizzata direttamente, un'immagine localmente salvata o un localizzatore di immagini più astratto (come un URL). Quando si utilizza un percorso file, considerare se questo percorso file è persistente o semplicemente una cache temporanea.
risposta data 25.09.2018 - 14:19
fonte