Come evitare le dipendenze di classe e modulo bidirezionali

1

Per dare un po 'di contesto, sto usando python. Abbiamo la seguente struttura del pacchetto:

/package_name
    /request.py  # defines class Request
    /response.py  # defines class Response

Supponiamo anche di avere una dipendenza bidirezionale a livello di classe e modulo:

  • Il metodo Request.run restituisce un'istanza Response .
  • Response ha bisogno di un'istanza Request da __init__ ializzata.

La mia soluzione ingenua consisterebbe nel definire la classe astratta IRequest e IResponse da memorizzare nei rispettivi moduli. Di conseguenza

  • Request implementerà IRequest e dipenderà da IResponse
  • Response implementerà IResponse dipende da IRequest

quindi la dipendenza circolare a livello di classe è sconfitta.

Tuttavia, abbiamo ancora una dipendenza bidirezionale a livello di modulo, che impedisce l'importazione dei moduli quando si utilizza python. La dipendenza del modulo bidirezionale può essere risolta memorizzando le classi astratte in un terzo modulo diciamo interfaces.py , ma mi sembra una soluzione dubbia.

Domanda : Sto violando qualsiasi principio di architettura? Quali sono le possibili soluzioni?

    
posta MLguy 10.04.2018 - 16:26
fonte

4 risposte

5

Pensando a un codice non tipizzato, se la risposta ottiene l'oggetto di richiesta iniettato, ma non ne crea uno da solo, non dipende dalla classe di richiesta. Dipende solo dalla sua interfaccia (che potrebbe non avere alcuna rappresentazione specifica nel codice, ma sicuramente esiste). Quindi in questo caso la dipendenza della classe non è veramente circolare.

Per acquisire quello stato nel codice digitato, scrivi le classi di interfaccia IRequest e IResponse e inseriscile nei propri moduli irequest.py e iresponse.py . La denominazione alternativa, che probabilmente mostra meglio la logica sottostante, consiste nel chiamare le interfacce Request e Response e le classi di implementazione RequestImpl e ResponseImpl (e, ovviamente, modificare i nomi dei moduli, di conseguenza). Apparentemente, non esiste una convenzione di denominazione stabilita per le interfacce in Python , quindi la scelta è tua.

La ragione per dividere i moduli è semplice: se non esiste alcuna dipendenza logica su una classe (classe Request in questa istanza), non dovrebbe esserci alcuna dipendenza dal codice per la sua implementazione. Rende anche chiaro che altre implementazioni possono esistere ed essere valide, e rendono naturale aggiungerle (senza far saltare il singolo modulo a dimensioni enormi).

Questa è una pratica standard e ben nota. Alcuni linguaggi tipizzati staticamente, come Java, potrebbero effettivamente applicarlo. In altri è solo uno standard informale, ma incoraggiato (file di intestazione in C ++). Guarda qualsiasi codice Java per esempi, come l'interfaccia Collection in OpenJDK. In Python, dove la tipizzazione statica è un concetto molto nuovo, non esiste una pratica consolidata, ma puoi aspettarti di emergere e seguire gli esempi esistenti.

Nota pratica: poiché la richiesta utilizza direttamente la classe Response, probabilmente dovresti salvare alcune righe inserendo IResponse nel modulo response.py o semplicemente saltare l'interfaccia completamente - puoi sempre aggiungerla quando necessario.

    
risposta data 10.04.2018 - 17:37
fonte
4

Esistono due modi principali per gestire le dipendenze circolari:

  • Nascondilo con le interfacce
  • Aggiungi un oggetto intermedio

In questo caso particolare, consiglierei la seconda opzione. Il tuo modulo avrebbe qualcosa del genere:

/package_name
    /client.py
    /request.py
    /response.py

Il nuovo oggetto Client avrebbe effettivamente eseguito la richiesta. Questo sposta Request.run in Client.run(request) . Ciò rende il tuo oggetto Request completamente indipendente (nessuna dipendenza).

La prossima domanda è se il tuo Response oggetto abbia effettivamente bisogno del Request passato. Potresti ottenere lo stesso effetto se hai semplicemente passato alcuni dei valori in? Ora hai questa opzione perché il tuo oggetto Client è responsabile dell'inizializzazione di Response .

Quindi ora la tua gerarchia delle dipendenze è:

Client->Request, Response
Response->Request? -- depends on answer to above question
Request

Questo è simile al modo in cui vengono progettate numerose API HTTP. Il Client può memorizzare nella cache i valori predefiniti da inserire nella richiesta (cioè le intestazioni predefinite) ed eseguire altri servizi.

    
risposta data 11.04.2018 - 15:19
fonte
2

Supponendo che non creerai mai un'istanza e restituirai response.Response senza prima creare un'istanza di request.Request ... Dovresti passare l'istanza request.Request al costruttore / iniziatore response.Response .

Come in: response.Response(request.Request("some_request_var"), "other_var", another_var="another_var")

Il modulo di risposta non dovrà più (non dovrebbe avere) una dipendenza dal modulo di richiesta. Le variabili di classe / istanza request.Request possono essere utilizzate per inizializzare response.Response class / istanze di variabili quindi gettate via o salvate / referenziate nella sua interezza ( response.Response.request )

A meno che il modulo di risposta o la classe response.Response stia creando / inizializzando istanze di request.Request , non c'è motivo di avere la classe request.Request nello spazio dei nomi del modulo di risposta (aka import X , nota anche come dipendenza circolare)

Se hai veramente una dipendenza circolare ... Puoi usare le importazioni tardive. Assegna innanzitutto None alla variabile di livello del modulo Request e o Response . Questa variabile normalmente memorizzerà il tuo riferimento alla tua importazione. Quindi all'interno di una funzione / metodo a livello di modulo / classe fai global Request o global Response quindi la tua dichiarazione di importazione. Due approcci comuni sono il controllo di class.__init__() per None e se None esegue l'importazione o il package.__init__.py importa i moduli, quindi chiama una funzione di importazione ritardata.

Esempi:

# In response.py
Request = None
class Response(object):
  def __init__():
    global Request
    if Request is None:
      from request import Request

# In __init__.py
from request import Request, late_import as request_late_import
from response import Response, late_import as response_late_import
request_late_import()
response_late_import()
    
risposta data 11.04.2018 - 07:03
fonte
1

Non strettamente legato a Python, è possibile utilizzare, come regola generale, il principio di inversione delle dipendenze per rimuovere il problema della "esclusione" reciproca ogni volta che si tratta di un problema di avere una dipendenza tipicamente definita tra i tipi. Il trucco consiste nell'aggiungere una classe astratta o un'interfaccia per almeno uno di essi (è possibile creare un'interfaccia per entrambi, ma potrebbe essere eccessivo e controproducente per la loro manutenzione).

Ad esempio: A <-> B diventerebbe A -> IB ; IB <-refines- B ; A <- B

Ora, su Python, non c'è alcun problema in questo senso dato che non è tipizzato staticamente.

Tuttavia , è importante notare che, indipendentemente dalla soluzione adottata, devi assicurarti che ci sia integrità in quella relazione circolare. Cioè, se rimuovi q1 come domanda di risposta r1, q1 deve rimuovere r1 come sua risposta, eccetera. Questa è la parte davvero complicata e perché vuoi evitare riferimenti circolari quando possibile.

    
risposta data 10.04.2018 - 19:26
fonte

Leggi altre domande sui tag