OOP Practice: come gestire al meglio le dipendenze dipendenti

7

Ho un problema con il design mostrato nell'immagine. La mia Section class ha qualche TextBlocks (o semplicemente Blocks). La sezione dovrebbe essere disegnata su una pagina (una bitmap come contesto del dispositivo). Imposta i blocchi della pagina e chiama page.DrawBlocks() . La pagina imposta l'oggetto Graphics di ciascun blocco e chiama block.Draw()

Graphics non può essere impostato nel costruttore di ogni TextBlock quando li introduco in Section perché la pagina potrebbe non essere inizializzata. Tuttavia, so che è meglio introdurre le dipendenze nel costruttore.

Come posso ridisegnare le mie classi per risolvere questo problema? Devo passare il Graphics alla funzione Draw() ? Preferisco no, perché alcune altre funzioni della classe richiedono anche Graphics .

    
posta Ahmad 03.03.2015 - 07:18
fonte

3 risposte

9

Should I pass the Graphics to the Draw() function?

Bene, quando ho visto questo disegno, la prima cosa che mi è venuta in mente è stata "perché diamine il TextBlock ha un attributo Graphics?" Non dovrebbe essere possibile disegnare lo stesso blocco su due diversi contesti grafici ? L'oggetto TextBlock non ha una durata che può essere più lunga della durata dell'oggetto Graphics ? "

Quindi la mia risposta è si - non ha senso per me not avere un parametro Graphics nella funzione Draw . Ciò implicherà di passare il parametro Graphics nel metodo DrawBlocks di Page , e qualche altro metodo nel tuo Section , ovviamente, ma probabilmente è la progettazione corretta.

Infatti, quando il tuo TextBlock ha più di una mezza dozzina di metodi privati, tutti chiamati direttamente o indirettamente da Draw , e tutti usano lo stesso oggetto Graphics , potrebbe essere più conveniente Graphics oggetto passato dal metodo Draw in una variabile membro, quindi non è necessario avere lo stesso parametro Graphics in tutti questi metodi. Quindi mantenere questo attributo come membro potrebbe avere senso (ma questo non può essere decifrato dalla piccola parte della classe di blocchi di testo che vediamo nel modello corrente). E se decidi di mantenere questo attributo, assicurati di aggiungere un commento quando questo attributo è impostato e per quanto tempo è valido. Il codice sarà quindi simile a

   void Draw(Graphics g)
   {
      graphics=g;
      // ...
      // call some members depending on "graphics"
      // ...
      graphics=null; // just in case, to avoid accessing an invalid graphics object
   }

In alternativa, potresti considerare di introdurre una classe helper TextBlockDrawer , che ottiene l'oggetto Graphics e il TextBlock passato come parametro costruttore e incapsula l'intero processo di disegno. In questo modo, non avrai più bisogno di un membro Graphics nella tua classe TextBlock , solo in TextBlockDrawer e questo attributo può essere inizializzato nel costruttore di quella classe. Questo design crea anche una migliore separazione tra i dati ( TextBlock ) e il processo di disegno. Il codice risultante sarà simile al seguente:

class TextBlock
{
   public void Draw(Graphics g)
   {
      var tbDrawer = new TextBlockDrawer(this,g);
      tbDrawer.Draw(); 
   }
}

oppure, omettendo completamente il metodo Draw in TextBlock e riutilizzando TextBlockDrawer for different text blocks :

class Page
{
   // ...
   public void DrawBlocks(Graphics g)
   {
      var tbDrawer = new TextBlockDrawer(g);
      foreach(var tb in MyTextBlocks())
          tbDrawer.Draw(tb); 
   }
}
    
risposta data 03.03.2015 - 07:36
fonte
6

Se l'applicazione utilizza C #, c'è anche un elemento non menzionato da Doc Brown che rende Graphics come proprietà una cattiva scelta (potrebbe probabilmente essere rilevante per Visual Basic e parzialmente rilevante anche per Java).

Graphics implementa IDisposable . Ciò significa che se lo passi a Draw() , il codice sembra semplice:

using (var g = new Graphics(...))
{
    foreach (var block in this.Blocks)
    {
        block.Draw(g);
    }
}

D'altra parte, se si utilizza una proprietà, diventa molto difficile:

  • Determina all'interno della classe del chiamante quando dovresti effettivamente disporre dell'oggetto Graphics ,

  • Trova all'interno della classe TextBlock se l'oggetto Graphics è già disposto.

Finirai con qualcosa che assomiglia a:

using (var g = new Graphics(...))
{
    foreach (var block in this.Blocks)
    {
        block.Graphics = g;
        block.Draw();
        block.Graphics = null;
    }
}

Utilizzi Analisi del codice ? Sarei sorpreso se hai zero errori con l'approccio effettivo.

    
risposta data 03.03.2015 - 09:17
fonte
4

Il tuo problema è che TextBlock s non dovrebbe sapere come disegnare se stessi in primo luogo. Questa è una violazione del principio di responsabilità singola; ora le tue classi dovranno cambiare ogni volta che l'interfaccia Graphics cambia o ogni volta che vuoi cambiare il modo in cui vengono disegnate sullo schermo, anche se la loro preoccupazione principale dovrebbe essere rappresentata da parti di pagine.

Invece, rendi Graphics quello per disegnare pagine e blocchi di testo. Questo mette tutta la logica grafica in un unico posto, ti consente di cambiarlo senza influenzare il resto del codice e non devi passare attorno a oggetti Graphics durante il test unitario.

    
risposta data 03.03.2015 - 13:45
fonte

Leggi altre domande sui tag