Jump to content

  • Log In with Google      Sign In   
  • Create Account

How to avoid global state here?


Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.

  • You cannot reply to this topic
5 replies to this topic

#1 SeraphLance   Members   -  Reputation: 1419

Like
0Likes
Like

Posted 24 September 2013 - 04:53 PM

Now I realize that having global state (particularly of the mutable variety) is a bad idea, but I've had difficulties in the past getting around it for some applications.

 

Some time ago I had this problem with logging, but I've gotten around to the idea of just using a free function, because logging doesn't really require any kind of persistent state.  Now I have a new problem, and it's logging's ornery younger brother:  debug printing.

 

Here's what I'm using now:

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;

namespace Foo
{
    public static class DebugPrinter
    {
        private static StringBuilder debugString = new StringBuilder();
        private static List<string> idList = new List<string>();

        [Conditional("DEBUG")]
        public static void RegisterString(String str, String id)
        {
            if (!idList.Contains(id))
            {
                debugString.Append(str).AppendLine();
                idList.Add(id);
            }
        }

        [Conditional("DEBUG")]
        public static void Flush()
        {
            debugString.Clear();
            idList.Clear();
        }
        [Conditional("DEBUG")]
        public static string getString()
        {
            return debugString.ToString();
        }
    }
}

Usage is pretty clear:  whatever class wants to write to it calls registerString(passing an id, because I'm using the sharpDX toolkit and update and draw aren't synchronized with each other) and the reader calls getString, followed by flush, then renders the string to the screen.

 

 

Now, if you feel the urge to stab me in the eye for this, I fully accept that.  It's probably the nastiest thing in my whole code base.  I just can't think of a way around it.  I can't use a free function because I need to mantain state between draw calls.  I could make some heavyweight scaffolding around deep dependency-passing down my state hierarchy.

 

I'm aware that it's kind of one of those leading sorts of questions(how do I keep all this shared global state without any shared global state), but it's also a design problem I don't know any other solution for.


Edited by SeraphLance, 24 September 2013 - 04:56 PM.


Sponsor:

#2 frob   Moderators   -  Reputation: 21341

Like
6Likes
Like

Posted 24 September 2013 - 06:14 PM


 I just can't think of a way around it.  I can't use a free function because I need to mantain state between draw calls.  I could make some heavyweight scaffolding around deep dependency-passing down my state hierarchy.

Single responsibility principle.

 

Have one class that accepts listeners for logging info.

Have another class that listens to logging info and displays it to screen.

Have another class that listens to logging info and dumps it to a file.

 

And since this is C#, reuse the functionality already present. Creating a trace listener is easy. Creating trace filters is easy.  Adding a few utility functions that automatically specify the appropriate filters to use is not bad either.  All that is left is adding a trace listener that displays new lines on the screen, and you're done.


Check out my personal indie blog at bryanwagstaff.com.

#3 SeraphLance   Members   -  Reputation: 1419

Like
1Likes
Like

Posted 24 September 2013 - 09:49 PM

So this tracing functionality is awesome and I didn't know about any of it before (except trivial Debug.WriteLine stuff).  I'll have to do a bit of string manipulation to handle duplicates, but it's a small price to pay for all this bling.  Thanks for the heads up!


Edited by SeraphLance, 24 September 2013 - 09:55 PM.


#4 SeraphLance   Members   -  Reputation: 1419

Like
0Likes
Like

Posted 29 September 2013 - 04:02 PM

Now that I've had a chance to mess around with the tracing stuff...

 

I'm still having trouble handling de-duplication.  I've tried a few different designs, but none of them seem to work quite right.  Suppose for simplicity I have two objects -- the renderer and a camera class.  The camera needs to be able to log camera position every update cycle, but neither the camera or the renderer know about the other directly.

 

This is the only thing I've thought of thus far:

class DebugPrintTraceListener : TraceListener
{

    //need to reparse hashcodes and check against a dictionary that gets flushed on read.
    public override void Write(string message);
    public override void WriteLine(string message);
    public string readAndFlush();
}

class DebugPrintObject
{
    string message;
    int hashCode;

    public DebugPrintObject(Object parent, string message)
    {
        hashCode = parent.GetHashCode();
        this.message = message;
    }

    public string ToString()
    {
        return hashCode.ToString() + " " + message;
    }
}

//somewhere in the renderer's initialization -- or containg class, whatever
System.Diagnostics.Debug.Listeners.Clear();
System.Diagnostics.Debug.Listeners.Add(myMemberListener);

//Somewhere in the camera
System.Diagnostics.Debug.WriteLine( new DebugPrintObject(this, "stuff"));


This should technically work, but the debugging objects are pretty much the definition of disgustingly hacky.

 

The solution would be easy if I only needed to log, but because of the deduplication problem I need some way of knowing the sender of the tracing data.  The diagnostics stuff doesn't seem to support this, or really supporting anything but sending strings along.  Sure, there's filters on both sides I can use, but it's mostly pointless if the only communication format is a string.

 

EDIT:  It's worth mentionining that, after all this crap, I'm still using global state, as System.Diagnostics.Debug is just as global and stateful as my old ad-hoc solution, albeit slightly less abstracted.

 

I thought of another idea:  Put two listeners on System.Diagnostics.Debug.Listeners, we'll call them sender and receiver.  register a TraceSource owned by the renderer with the receiver, and register other TraceSources in every class that wants to write information with a sender.  Every update cycle, the sender owners cache their string data, but don't send.  When the receiver needs to draw, it sends a trace to the senders.  The senders notify their parents, which then call back to the senders to write to the receiver with their data.  This works, doesn't technically use global state (though it leverages global state to set up), doesn't require weird encodings, but holy hell is it incredibly roundabout.  This can't be the best way to go about it.


Edited by SeraphLance, 29 September 2013 - 04:07 PM.


#5 PhillipHamlyn   Members   -  Reputation: 454

Like
1Likes
Like

Posted 30 September 2013 - 01:26 PM

SeraphLance,

 

I think you might be getting stuck in dogma; sure global state isn't delightful, but its better than the alterative method you've got round to designing (in my opinion). Tracing in C# has a CorrellationManager so you can do correllation of messages from the same class instance. Isn't that what you are after here ?

 

Phillip



#6 frob   Moderators   -  Reputation: 21341

Like
2Likes
Like

Posted 30 September 2013 - 02:38 PM

SeraphLance,

 

I think you might be getting stuck in dogma; sure global state isn't delightful, but its better than the alterative method you've got round to designing (in my opinion). Tracing in C# has a CorrellationManager so you can do correllation of messages from the same class instance. Isn't that what you are after here ?

 

Phillip

 

Indeed, there is a reason for the rules. 

 

It is true that global state should be avoided. It can create non-local dependencies. It can lack access control. It can create memory allocation issues. It can make automated testing much harder.

 

 

The global object has a single responsibility: forward messages to listeners. The only global state it maintains is the list of listeners, but this does not modify behavior to the class users.

 

Note that the Debug.Trace functionality does not suffer from the problems of most global objects. This type of "data distribution service" is one area where global objects are reasonably safe, when implemented correctly.

 

Data distributors and event broadcasters can spread data out, but those are not non-local dependencies; other systems will not be modifying it behind your back in potentially harmful ways. It does provide access control because of the way methods are used. The language's Trace functionality doesn't create memory allocation issues because it comes from the system itself. And it doesn't interfere with automated testing because the developer can control the listeners during the test.

 

 

You need to understand the guidelines and rules so you know when it is acceptable to break them.


Check out my personal indie blog at bryanwagstaff.com.




Old topic!
Guest, the last post of this topic is over 60 days old and at this point you may not reply in this topic. If you wish to continue this conversation start a new topic.



PARTNERS