Annidare gli oggetti senza passare gli argomenti?

0

Ho un paio di classi che ho annidato insieme (non nel senso che potresti pensare così nudo con me), per creare ciò che chiamo un oggetto prefabbricato (simile al sistema prefabbricato di Unity).

In questo caso particolare, ho un prefabbricato chiamato BarkingHorse che contiene le proprie istanze di oggetti CullingBird e DivingWeasel . Tutte queste classi derivano da una classe base chiamata RenderObject . Esiste un oggetto master chiamato Almighty che gestisce il rendering di singoli oggetti. Ora, quando gestiamo le prefabbricate, la classe Almighty dovrebbe rimanere all'oscuro degli oggetti contenuti all'interno. Ad esempio:

// Almighty Class
public void Render(RenderObject obj) {
    obj.Render();
}

// BarkingHorse Class
public override void Render() {
    if (ShowCullingBird)
        cullingBird.Render();

    if (ShowDivingWeasel)
        divingWeasel.Render();
}

Ora, normalmente questo sarebbe semplice ma in questo caso non lo è. BarkingHorse ha una trama associata ad esso ed è quindi richiesto di informare DeviceContext prima del rendering. Tuttavia, CullingBird e DivingWeasel non hanno textures e sono renderizzati in un drab Color.Gray a meno che DeviceContext non sia stato informato che non hanno immagini. Quindi, ad esempio con più codice di base:

// Almighty Class
public void Render(...) {
    bool hasTexture = !string.IsNullOrWhiteSpace(obj.ImageFile);
    if (hasTexture)
        TellDeviceContextAboutTexture();

    obj.Render();

    if (hasTexture)
        TellDeviceContextTextureIsGone();
}

Il codice illustrato sopra funziona bene quando le tre classi sono separate e rese come tali; tuttavia, con il codice sopra riportato, non lo fa poiché DeviceContext crede ancora che ci sia una trama. So che questo è il problema perché l'ho testato.

La mia classe base RenderObject contiene le seguenti proprietà che potrebbero rivelarsi utili qui:

public RenderObject Parent { get; protected set; }
public List<RenderObject> Children { get; protected set; }
public void AddChild(RenderObject obj) {
    obj.Parent = this;
    Children.Add(obj);
}

Tuttavia, non sono sicuro di adottare l'approccio che sto per discutere, poiché credo che dal punto di vista dell'API pubblica potrebbe non essere una buona idea. Questo ci porta al motivo per cui sono qui.

L'idea

Credo di poter aggiungere un bool al metodo Render su Almighty chiamato renderChildren che potrebbe consentire al metodo di eseguire il rendering dei figli dell'oggetto; ma qui è dove inizia la mia testa a girare sui potenziali inconvenienti. In entrambi i casi, una rappresentazione di base sarebbe:

// Almighty Class
public void Render(RenderObject obj, bool renderChildren) {
    bool hasTexture = !string.IsNullOrWhiteSpace(obj.ImageFile);
    if (hasTexture)
        TellDeviceContextAboutTexture();

    obj.Render();

    if (hasTexture)
        TellDeviceContextTextureIsGone();

    if (renderChildren) {
        foreach (RenderObject child in obj.Children) {
            if (!child.Enabled)
                continue;

            hasTexture = !string.IsNullOrWhiteSpace(child.ImageFile);
            if (hasTexture)
                TellDeviceContextAboutTexture();

             child.Render();

             if (hasTexture)
                 TellDeviceContextTextureIsGone();
        }
    }

    TellDeviceContextTextureIsGone();
}

Le domande

  • Quali sono gli svantaggi di questa nuova idea?
  • C'è un modo più intelligente per affrontare questo problema senza aggirare gli argomenti?
  • Ciò che lavoro funziona bene da un punto di vista dell'API pubblica o dovrei tornare al tavolo da disegno?

Note

Ho dimenticato di menzionare che una delle mie preoccupazioni è che non è possibile accedere alle proprietà del tipo ShowCullingBird da quel livello; in tal caso, dovrei creare una proprietà Enabled o Visible sulla classe RenderObject .

Inoltre, se riesci a pensare a un titolo migliore, non esitare a rinominare questo post.

    
posta PerpetualJ 14.11.2018 - 22:57
fonte

3 risposte

1

Istintivamente, mi sento come se il Polimorfismo del runtime potesse fare molto.

public abstract class RenderObject
{
    public void Render()
    {
        // Baseline render implementation goes here.
    }
}

public class BarkingHorse : RenderObject
{
    public override void Render()
    {
        // Custom logic to render a Barking Horse, and/or call base.Render()
        // or simply omit the override.
    }
}

Per chiamarlo:

RenderObject obj = new BarkingHorse();
obj.Render();  // Renders a Barking Horse.  "Woof whinney."

Render () può andare nella classe base. Non hai bisogno dell'Onnipotente per farlo funzionare.

Se hai problemi a gestire le trame, dai uno sguardo al modello Flyweight.

    
risposta data 14.11.2018 - 23:28
fonte
1

Supponendo che la classe Onnipotente sia necessaria e che TellDeviceContextAboutTexture abbia bisogno di / dovrebbe esserci lì, piuttosto che nelle classi RenderObject, suggerirei di esporre un'interfaccia da quella a RenderObjects.

public interface IRenderingContext // Almighty implements this
{ 
    void WithTexture(Action renderingAction);
}

// Inside Almighty
public void Render(RenderObject obj) 
{
    obj.Render(this);
}

public void WithTexture(Action renderingAction)
{
    TellDeviceContextAboutTexture();
    renderingAction();
    TellDeviceContextTextureIsGone();
}

// Inside BarkingHorse
public void Render(IRenderingContext context)
{
    context.WithTexture(() => 
    {
        // Do rendering
    });

    if(ShowCullingBird)
        cullingBird.Render(context); // Does not call WithTexture
}

Se questo è sempre basato su ImageFile, potrebbe essere parte del metodo RenderObject.Render di base.

    
risposta data 15.11.2018 - 00:38
fonte
1

Metterei tutta la logica per far camminare l'albero degli oggetti (che può essere fatto in modo ricorsivo) nella classe base, e offrire un altro metodo interno per rendere solo l'oggetto stesso. Quindi, in altre parole, c'è

  1. Un metodo pubblico ( Render ) per il rendering dell'oggetto (chiamando RenderInternal ) e chiamando Render su tutti i bambini, che a loro volta chiameranno il proprio chilren.

  2. Un metodo protetto ( RenderInternal ) che uno sviluppatore deve eseguire l'override per implementare la logica di rendering per quel particolare tipo.

Ad esempio:

abstract class RenderObject
{
    public string Name { get; set; }

    public List<RenderObject> Children { get; private set; } 

    protected readonly IDeviceContext _context;

    public RenderObject(IDeviceContext context)
    {
        _context = context;
        Children = new List<RenderObject>();
    }

    public void Render()
    {
        this.RenderInternal();
        foreach (var o in Children) o.Render();
    }

    protected abstract void RenderInternal();
}

Quindi avrei una classe derivata separata per comportamento . Non per animale-- non sarebbe mai in scala. Hai solo bisogno di una classe se c'è qualcosa sul comportamento che dipende da esso o se sono necessari campi diversi (ad esempio ImageFile non è necessario in un oggetto strutturato). In questo caso, puoi avere ImageRenderObject e TexturedRenderObject .

Le implementazioni sono brevi perché la maggior parte della logica è ancora nella classe base. Tutto ciò che devi fare è implementare RenderInternal .

class ImageRenderObject : RenderObject
{
    public string ImageFile { get; set; }

    public ImageRenderObject(IDeviceContext context) : base(context) {}

    protected override void RenderInternal()
    {
        Console.WriteLine("ImageRenderObject '{0}' is rendering an image of {1}", this.Name, this.ImageFile);
    }
}

class TexturedRenderObject : RenderObject
{
    public TexturedRenderObject(IDeviceContext context) : base(context) {}

    protected override void RenderInternal ()
    {
        _context.StartTexture();
        Console.WriteLine("TexturedRenderObject is rendering {0}", this.Name);
        _context.EndTexture();
    }
}

Ora noterai che ho aggiunto un campo _context . Questo può essere compilato dall'iniezione del costruttore e dalla classe factory:

class RenderObjectFactory
{
    protected readonly IDeviceContext _context;

    public RenderObjectFactory(IDeviceContext context)
    {
        _context = context;
    }

    public T Create<T>(string name) where T : RenderObject
    {
        return Create<T>(name, Enumerable.Empty<RenderObject>());
    }

    public T Create<T>(string name, string imageFile) where T : ImageRenderObject
    {
        var instance = Create<T>(name, Enumerable.Empty<RenderObject>());
        instance.ImageFile = imageFile;
        return instance;
    }

    public T Create<T>(string name, IEnumerable<RenderObject> children) where T : RenderObject
    {
        var type = typeof(T);
        var instance = (T)Activator.CreateInstance(type, new[] {_context} );
        instance.Children.AddRange(children);
        instance.Name = name;
        return instance;
    }
}

Per i test, ho creato un contesto fittizio:

interface IDeviceContext
{
    void StartTexture();
    void EndTexture();
    void Paint(string o);
}

class DeviceContext : IDeviceContext
{
    public void StartTexture()
    {
        Console.WriteLine("Starting texture on device context.");
    }

    public void EndTexture()
    {
        Console.WriteLine("Ending texture on device context.");
    }

    public void Paint(string s) 
    {
        Console.WriteLine("Drawing {0}", s);
    }
}

Ora le tue chiamate sono molto più semplici. Ecco come lo chiameresti dal livello più alto. Nota che qui stiamo costruendo DeviceContext e factory con la parola chiave new , ma nella tua applicazione potresti volerli costruire nel Composizione Root .

public static void Main()
{
    var factory = new RenderObjectFactory(new DeviceContext());
    var divingWeasel = factory.Create<ImageRenderObject>("DivingWeasel", "DivingWeasel.png");
    var cullingBird = factory.Create<ImageRenderObject>("CullingBird", "CullingBird.png");
    var barkingHorse = factory.Create<TexturedRenderObject>("BarkingHorse", new RenderObject[] {divingWeasel, cullingBird} );
    var almighty = new Almighty();
    almighty.Render(barkingHorse);
}

In questo modo soddisfiamo i tuoi requisiti:

  1. Non devi passare argomenti
  2. La classe onnipotente non ha bisogno di sapere nulla dell'oggetto che sta rendendo
  3. Ogni classe ha una propria implementazione specifica del comportamento, ad es. impostando le trame su o meno
  4. La separazione dei dubbi è migliorata
  5. La soluzione viene ridimensionata senza ulteriore codifica se vengono aggiunti nuovi animali con gli stessi comportamenti dei vecchi animali.

Output:

Starting texture on device context.
TexturedRenderObject is rendering BarkingHorse
Ending texture on device context.
ImageRenderObject 'DivingWeasel' is rendering an image of DivingWeasel.png
ImageRenderObject 'CullingBird' is rendering an image of CullingBird.png

Ecco l'esempio su DotNetFiddle: Link

    
risposta data 15.11.2018 - 02:03
fonte

Leggi altre domande sui tag