Accoppiamento lento e dipendenze di mischia

5

Ho un sacco di classi che assomigliano a questo:

public class MyGame()
{
    private Graphics graphics;

    private Player player;

    public MyGame()
    {
        graphics = new Graphics();
        //...
    }

    protected void Initialize()
    {
        player = new Player();
        //...
    }

    protected void Update()
    {
        player.DoStuff();
        //...
    }

Ho letto degli schemi di progettazione e ho capito che questo non è un ottimo progetto perché non è strettamente accoppiato. Invece di codificare per interfacce, sto codificando per implementazioni (Grafica, Player, ecc.).

Per correggere questo problema percepito, comincio cambiando quelle dipendenze alle interfacce:

public class MyGame()
{
    private IGraphics graphics;

    private IPlayer player;

    public MyGame()
    {
        graphics = new Graphics();
        //...
    }

    protected void Initialize()
    {
        player = new Player();
        //...
    }

Non sono sicuro di quanto sia veramente utile, ma è bello che il campo grafico (per esempio) non sia legato alla classe Graphics - Posso usare la classe AwesomeGraphics o la classe SparklyGraphics invece che quelle classi implementare IGraphics.

Ma ho ancora delle forti dipendenze da Graphics e Player, quindi qual è stato il punto di tutto ciò che riguarda il refactoring? (Il mio codice attuale è molto più complicato.) Ho anche capito che l'iniezione di dipendenza è una buona idea, quindi è quello che cerco di fare dopo:

public class MyGame()
{
    private IGraphics graphics;

    private IPlayer player;

    public MyGame( IGraphics graphics, IPlayer player )
    {
        this.graphics = graphics;
        this.player = player;
        //...
    }

Questo sembra davvero carino perché non ho più alcuna dipendenza da MyGame. Tuttavia, le dipendenze sono ancora nel mio codice: sono state appena spostate nella classe che istanzia MyGame:

static class Program
{
    static void Main( string[] args )
    {
        using ( var game = new Game( new Graphics(), new Player() )
        {
            game.Run();
        }
    }
}

Ora sono tornato dove ho iniziato! Questa classe non è affatto accoppiato! Quindi, di nuovo, qual è stato il punto di tutto quel refactoring? Sono abbastanza sicuro di poter usare un contenitore IoC per occuparmi di queste dipendenze in qualche modo, ma probabilmente aggiungerebbe un sacco di complicazioni a un progetto già complicato. Inoltre, non sposterebbe quelle dipendenze ancora una volta in un'altra classe?

Dovrei rimanere fedele a ciò che avevo in primo luogo, o c'è qualche beneficio tangibile che non vedo ancora?

    
posta David Kennedy 31.12.2013 - 07:59
fonte

5 risposte

5

Hai ottenuto i vantaggi di un accoppiamento lento nel tuo refactoring. Hai spostato la conoscenza dell'implementazione precisa al di fuori della tua classe MyGame e in una classe di livello superiore. Ciò è positivo: mantenendo tutta la conoscenza di ciò che le implementazioni precise sono insieme, stai migliorando la struttura del tuo codice, rendendo più facile l'esecuzione di modifiche di alto livello e rendendo più semplice apportare modifiche che coinvolgono, ad esempio, utilizzando più implementazioni della tua classe Player (magari introducendo un NetworkPlayer o AIPlayer più tardi). MyGame ora ha un motivo in meno per cambiare, quindi è più vicino ad avere un'unica responsabilità, e il Principio di Responsabilità Unica è ora considerato uno dei più importanti nella progettazione orientata agli oggetti da molti programmatori.

    
risposta data 31.12.2013 - 19:12
fonte
2

Alla fine, tutto il codice deve dipendere da un'implementazione concreta, indipendentemente dal fatto che sia specificato nel codice o in qualche tipo di file di configurazione del framework DI. Il vantaggio di dipendere da un'interfaccia è che puoi facilmente dipendere da più implementazioni concrete, come richiede la situazione.

Tuttavia, questi benefici non vengono effettivamente realizzati finché non si hanno effettivamente più implementazioni concrete. Più frequentemente, la seconda implementazione concreta è un oggetto fittizio utilizzato nei test unitari. Potrebbe anche essere cose come più piattaforme di destinazione. Ad esempio, ho un progetto che dipende dalle interfacce per l'interfaccia utente, quindi posso sostituire le implementazioni concrete per Swing o Android. È comune disporre di interfacce per più database o funzionalità specifiche del sistema operativo.

Se non hai questa seconda implementazione, non stai ricevendo alcun beneficio tangibile. Avrai solo la certezza che se hai bisogno di una seconda implementazione, sarà più facile integrarla. È come possedere azioni senza dividendi. Vale la pena qualcosa sulla carta, ma non ottieni alcun beneficio reale finché non lo vendi effettivamente.

    
risposta data 31.12.2013 - 19:24
fonte
2

Sicuramente hai ottenuto notevoli benefici dal cambiare la struttura.

Quando si codificano nuovi sistemi o sistemi abbastanza stabili che implementano modelli di progettazione può sembrare uno spreco di tempo e fatica dal momento che non ti ha reso la vita più facile in questo momento.

Considera che l'oggetto grafico che stavi utilizzando in origine era sviluppato per il rendering di grafica 2D e utilizzava tutti i tipi di logica sotto le copertine per semplificare lo sviluppo. Più avanti il tuo gioco cresce e ora ha bisogno di iniziare a fare grafica 3D, normalmente inizierai ereditando da Graphics, solo grafica 2D mentre potrebbe avere metodi simili come Render (), Refresh () o Invalidate () non ha nulla a che fare con la grafica 3D.

Aprendo diverse interfacce SIMPLE per la grafica, si apre la porta a una maggiore flessibilità del codice.

Come esempio del modello di strategia (inversione di dipendenza) e della codifica alle interfacce:

MyGame richiede diverse cose per funzionare.

Ha bisogno del nostro Render (), Refresh (), Invalidate (). (IGraphics) Ha bisogno di ulteriori metodi 3D come Transform (), Rotate (). (IGraphicsManipulations3D)

E se quelle interfacce multiple hanno senso essere raggruppate insieme per qualcosa, puoi creare un'interfaccia combinata

public interface IGraphics3D : IGraphics, IGraphicsManipulations3D

quindi ora invece di dire che hai bisogno di un'implementazione grafica SPECIFICA hai solo bisogno di qualche oggetto che faccia le cose di base, potresti addirittura scambiare in motori di rendering completamente diversi se lo desideri.

public class UnityEngineGraphics : IGraphics3D

o

public class HavokEngineGraphics : IGraphics3D

Ma al tuo gioco non interessa un po '.

public class MyGame(IGraphics3D, IPlayer)

La cosa importante da notare e molto probabilmente dove trovi difficile vedere il valore è che queste circostanze tendono ad apparire più tardi piuttosto che prima e quando stai costruendo il MyGame sei completamente intenzionato a usare un'implementazione dell'oggetto grafico . Non dimenticare mai che i requisiti cambiano, a volte per il bene, a volte per mal di testa e schemi di progettazione sono IL modo per essere preventivi.

    
risposta data 31.12.2013 - 20:54
fonte
1

In questo esempio:

public MyGame( IGraphics graphics, IPlayer player )

la frase:

However, the dependencies are still in my code

non è corretto. Non ci sono dipendenze perché ora la classe è legata a un contratto e non a un'implementazione.

In alcuni casi non è necessario essere accoppiati liberamente. Ad esempio, la classe giocatore potrebbe non essere un buon candidato per essere accoppiata liberamente se contiene solo proprietà o logica di base.

Diamo un'occhiata a un esempio migliore, diciamo che il tuo gioco utilizza un motore di ranking di terze parti per classificare i giocatori.

Quindi il tuo costruttore potrebbe apparire come questo:

public MyGame( IRankingProvider Provider )

Ora puoi cambiare facilmente le diverse implementazioni di un fornitore di ranking, il tuo codice potrebbe avere 3 o 4 implementazioni a seconda della piattaforma su cui è in esecuzione il gioco.

Inoltre, per i test unitari è possibile creare un fornitore falso per testare la logica su MyGame senza alcuna dipendenza del provider di classifica esterna. Ciò semplifica l'esecuzione di tutti i test unitari su una macchina di sviluppo / costruzione senza installare / configurare alcuno dei provider.

Ora, alla fine, su un server di sviluppo / distribuzione si vorrebbe eseguire test di integrazione usando veri provider per garantire che l'implementazione sia corretta, in quei casi il dispositivo di test inietterebbe un'implementazione reale.

Quindi, i principali vantaggi sono:

  1. Maggiore testabilità
  2. Possibilità di cambiare le implementazioni o utilizzare più implementazioni
  3. Rimozione delle dipendenze

Si dovrebbe scegliere attentamente quali parti del sistema sono accoppiate liberamente. In alcuni casi non è necessario e una volta impiegato provoca uno strato di astrazione inutile.

    
risposta data 31.12.2013 - 19:29
fonte
1

Al livello programma , ci sarà sempre un punto in cui deve essere presa una decisione che le implementazioni concrete di IGraphics e IPlayer saranno date a Game . Questo è quello che stai vedendo qui.

Il punto del tuo lavoro è che hai spostato la decisione fuori da Game stessa, quindi non è strettamente correlata a nessuna particolare implementazione e l'ha lasciata al tuo codice di programma di livello superiore per decidere.

Questo significa che Game è molto più flessibile (come IGraphics e IPlayer ) e il tuo programma ora è composto da unità liberamente componibili invece di un'unità inflessibile gerarchica.

Potrebbe non sembrare un grosso problema con un esempio così semplice, ma una volta diventato anche un po 'più complesso, la componibilità diventa molto più vantaggiosa.

    
risposta data 31.12.2013 - 19:31
fonte

Leggi altre domande sui tag