Wrapping di oggetti di terze parti che dipendono l'uno dall'altro e best practice

2

Sto avendo un po 'di indecisione qui e mi piacerebbe avere una prospettiva su questo.

Attualmente raccolgo oggetti da un'API di terze parti (a casa per il mio progetto e al lavoro) e sto facendo del mio meglio per aderire il più strettamente possibile ai principi SOLID. Tutto questo è scritto usando C #. Giusto per essere chiari qui: questa non è una linea di applicazione aziendale, sto creando un framework per il riutilizzo che avvolge dettagli più complicati di un'API di terze parti.

Quindi il mio problema è questo: Ho questi oggetti racchiusi nelle mie istanze concrete. Chiameremo gli oggetti nativi che sono avvolti nA e nB e i wrapper A e B che contengono rispettivamente nA e nB:

class A { private nA _nA }
class B { private nB _nB }

nB non può essere creato senza passare un riferimento a nA. Quindi, per evitare di esporre al pubblico l'oggetto nA, ho un metodo factory per creare il wrapper B in modo che possa passare in nA al costruttore di nB. Quindi, prima domanda: sto assumendo che questa sia una pratica corretta? Non voglio finire con un sacco di fabbriche o metodi di fabbrica, se posso aiutarlo (c'è un bel po 'da concludere).

Ora, va tutto bene. Posso creare A e B senza problemi a causa di quanto sopra. Ma ora ho un'altra ruga. Devo avvolgere un metodo chiamato M1 (nB) dove il parametro è di tipo nB, che è memorizzato all'interno del wrapper B:

public class A { private nA _nA; public M1(B) { _na.M1( ??? );} }

Ma nB è inaccessibile. Quindi, qual è la procedura migliore per passare nB al metodo nA.M1 () memorizzato nel wrapper A?

Potrei imporre l'uso di tipi concreti e rendere nB un membro interno sul wrapper B in questo modo:

public class B { internal nB { get; private set; } }

Ma questo introdurrà un accoppiamento stretto. Ha importanza dal momento che nA e nB sono strettamente accoppiati comunque?

Questi tipi di problemi mi fanno sempre venire il mal di testa, quindi se qualcuno potesse risparmiare qualche consiglio, sarebbe bello.

Inoltre, mentre cerco di aderire a SOLID nel miglior modo possibile, sono consapevole che sono linee guida. Dopo tutto, non sono regole fisse o una religione. Quindi, se non ho altra scelta che rompere i principi SOLIDI per ottenere ciò di cui ho bisogno, così sia.

Alcuni codici per illustrare ulteriormente il mio problema:

interface IGraphics
{
   CopyResource(Texture src, Texture dest);
}

interface ITexture
{
   // Stuff for textures that have no bearing on this example.
}

class Graphics
   : IGraphics
{
   // Native object (or object that wraps a native object).
   private ID3D11Context1 _d3dContext;

   public CopyResource(Texture src, Texture dest)
   {
      // Do validation, etc...    

      // How can I do this with interfaces?  
      // Internals are not allowed on interfaces by the language
      _d3dContext.CopyResource(src._d3dTexture, dest._d3dTexture);
   }
}

class Texture
   : ITexture
{
   // Native object (or object that wraps a native object).
   private ID3D11Texture2D _d3dTexture;
   // Other stuff
}
    
posta Mike 09.04.2018 - 16:54
fonte

2 risposte

1

But this will introduce tight coupling. Does it matter since nA and nB are tightly coupled anyway?

No, non importa finché sono classi interne, accessibili pubblicamente tramite interfacce. È come ciò che accade all'interno delle tue classi: se Foo chiama un metodo privato, Bar , è strettamente accoppiato a Bar . Ma se successivamente rompi quell'accoppiamento e lo cambi per chiamare FooBar e Baz , nient'altro è interessato dal momento che l'accoppiamento è incapsulato all'interno della tua classe.

Lo stesso vale per ciò che accade dentro A e B . Fintanto che i metodi pubblici fanno riferimento solo a IA e IB , gli interni di A e B sono liberi di intrecciarsi con nA e nB . In realtà, non solo sono liberi di farlo, ma si aspettano: è il loro lavoro come wrapper.

Assicurati che quei wrapper siano esposti al resto del sistema tramite interfacce, IA e IB . In questo modo, i wrapper (e quindi le librerie di terze parti) possono essere derisi quando necessario. Se non lo fai, i tuoi wrapper non stanno facendo nulla di utile dato che il resto del codice è ancora accoppiato a nA e nB , tramite un livello di offuscamento, piuttosto che un livello di astrazione.

Aggiorna

Guardando il tuo esempio di codice aggiornato, tornerò un po 'indietro, poiché qui le interfacce potrebbero essere più problematiche di quanto valgano. Durante il wrapping di classi come questa, personalmente seguo le seguenti "regole":

  1. Prova ad esporre tutte le classi avvolte tramite una sola interfaccia, incapsulando tutti i tipi di dati ecc dietro il wrapper. Questa è la situazione ideale, ma raramente è pratica. Non funzionerà qui.
  2. Assicurati che, ad esempio nel tuo caso ITexture , definisca abbastanza proprietà per consentire di creare un Texture (e quindi un ID3D11Texture2D ) da qualsiasi implementazione di ITexture , in modo che tu possa fare cose come:

    public CopyResource(ITexture src, ITexture dest)
    {
        // Do validation, etc...    
    
       _d3dContext.CopyResource(CreateRealTexture(src).D3dTexture,
                                CreateRealTexture(dest).D3dTexture);
    }
    

    con D3dTexture che quindi è una proprietà interna.

    Questo consente quindi di nascondere ancora tutti i tipi di dati reali, esponendoli solo tramite interfacce. Gli svantaggi di questo sono che può rendere complesse le interfacce e c'è un grande sovraccarico nel creare i tipi "reali" al volo. Anche in questo caso è improbabile che ti sia utile.

  3. Elimina le interfacce e utilizza le fabbriche per creare istanze, come suggerisci tu. A questo punto, direi fermati e chiediti "perché?" Qual è il punto di tali wrapper? Sono un sacco di lavoro da creare e mantenere. Non aiutano molto con test, DI ecc. (A causa della mancanza di interfacce).

    Se la ragione per crearli è così "Posso sostituire la libreria grafica con un'altra, dato che ho aggiunto un'astrazione al loro accesso". Allora direi, non perdere tempo. Le possibilità che una libreria grafica diversa sia in grado di inserirsi dietro le classi del wrapper senza modifiche a questi wrapper è estremamente piccola. Accedi direttamente alle librerie e riscrivi se cambi.

Quindi non c'è una soluzione perfetta qui. Chiediti se i wrapper sono veramente di valore. Se lo sono, la mia reazione istintiva in questo caso particolare sarebbe quella di cambiare tipo:

private ID3D11Context1 _d3dContext;

essere internal elementi. In questo modo, dietro le quinte, le tue classi wrapper possono accedere facilmente a tutti i tipi sottostanti.

    
risposta data 09.04.2018 - 17:06
fonte
0

Tre pensieri per te, OP.

Utilizza una classe anemica

Se B è solo un DTO, non c'è motivo di essere in grado di deriderlo e un'interfaccia non è necessaria (un test unitario può solo usare new e inserire dati fittizi). Ad esempio, se B è una struttura personalizzata progettata per contenere un indicatore data / ora e contiene solo campi e proprietà privi di comportamento, vai avanti e esponila come class B anziché come interface IB . Va bene che il client utilizzi new invece di ottenere ogni singola istanza dalla libreria e i test di unità possono fare lo stesso, a condizione che la classe sia un contenitore di dati stupido.

Utilizza un ID per ricostruire l'oggetto originale

Non preoccuparti di mantenere il tuo corpo con ogni modello in uso dagli oggetti che stai avvolgendo. Il punto di un wrapper è di esporre solo ciò che è necessario al cliente e in un modo che sia facile da usare e difficile o impossibile da utilizzare. Va bene se le cose cambiano.

Ad esempio, potrebbe avere più senso esporre un metodo che accetta un identificatore di oggetto di un oggetto stesso. Quindi invece di

void Method1(ICustomer customer);

potresti esporre

void Method1(int customerID);

Ora internamente, method1 dovrà usare quell'ID cliente per costruire un oggetto Cliente (interno), compatibile con la libreria che si sta tentando di avvolgere. Suggerirei di poterlo costruire al volo.

void Method1(int customerID)
{
    var c = internalLibrary.GetCustomer(customerID);
    c.InternalLibraryCall();
}

Usa interfacce interne

Hai un potente strumento a tua disposizione: interfacce interne. Interfaccia interna membri (come hai scoperto, questi non sono consentiti) ma l'interfaccia stessa.

public interface ICustomer
{
    int CustomerID { get; }
}

internal interface ICustomerEx
{
    InternalCustomer GetOriginalCustomerObject();
}

public class Customer: ICustomer, ICustomerEx  //Client won't see ICustomerEx
{
    InternalCustomer _customer;

    internal Customer(InternalCustomer c)
    {
        _customer = c;
    }

    InternalCustomer ICustomerEx.GetOriginalCustomerObject()
    {
        return _customer;
    }
}

Quindi il metodo potrebbe assomigliare a questo:

public void HandleCustomer(ICustomer customer)
{
    var c = customer as ICustomerEx;
    var internalC = c.GetOriginalCustomerObject();
    c.InternalLibraryCall();
}
    
risposta data 10.04.2018 - 04:15
fonte

Leggi altre domande sui tag