2
\$\begingroup\$

Each class of my project has several inputs and outputs. Each input, the same as output, depends on it's own set of value-datatype.

I need to provide a mechanism to forward data streams from one instance to another, with ability to split that streams and to combine several streams into one.

There are a lot of such classes, and they are hardly grouped by their input or output type. Splitting and combining (may be other operations like filtering or simple math) will provide this classes interaction.

I came out with Table class, which can store and manage two-dimensional array of objects and also can send rows to another Table, using maps on columns (that's how data streams splits). It could be one-dimensional, but data buffer avaibility is useful. Each class become a Context with a collection of that tables. Typical work algorythm for Context is to: 1. initialize tables, 2. handle input table inserts, 3. unbox and process data, 4. insert result to output table

Everything was OK with that Table, but one day, while coding a new Context I felt that data storage and transfering is not the only thing Context must do. When an external infrastructure is hiding behind the Context, it needs to be connected/disconnected/opened/loaded etc. And that looks like I need to add a Procedure or even Function to Context.

This is first huge project of mine and I have no person to help. Questions I care about:

  • this design looks like implementing internal programming language, is that correct? Can you show examples?

  • this design looks like implementing in-memory database, is that correct? Can you show examples?

  • is that normal not to use interfaces on data types if there are a lot of them?

  • can you propose more effective and elegant design for my problem?

  • are there similiar design patterns?


May be some code will help to understand what I need. This is very simplified version. In real project there are a lot of Context realisations, more sugar to access and create Tables, more logic on managing internal collections.

public abstract class Context
{
    public Table this[string name] { get { /* ... */ } }

    public IEnumerable<Table> Tables { get; }

    protected void AddTable(Table t) { /* ... */ }
}

public class Table
{
    object[][] rows; // Two-dimensional for buffering rows

    public Table(string name /* + columns initialisation info */ ) { /* ... */ }

    public void PushRow(params object[] row) { /* ... */ } // insert new row and send it to subscribers

    public void AddSubscriber(Table dst, params int[] columnMap) { AddSubscriber(dst.PushRow, columnMap); }
    public void AddSubscriber(Action<object[]> receiver, params int[] columnMap) { /* ... */ }
}

public class Function
{
    // Do I need this in Context, if, for example, Mathematician must be Opened/Closed?
}

class Mathematician : Context
{
    public Mathematician()
    {
        // input table
        Table t = new Table("Numbers");
        AddTable(t);
        t.AddSubscriber(Count);

        // output tables
        AddTable(new Table("Average"));
        AddTable(new Table("Variance"));
        AddTable(new Table("StDev"));
    }

    void Count(object[] row)
    {
        double newNumber = (double)row[0]; // exception here if data streams connection is not correct

        double avg = 0;
        double variance = 0;
        double stdev = 0;

        /* ... computations begin ... */

        /* Tables can be accessed, their buffer can be used */

        // Finally, publish results
        this["Average"].PushRow(avg);
        this["Variance"].PushRow(variance);
        this["StDev"].PushRow(stdev);
    }
}

Possible usage. Imagine that we have another context Secretary : Context which will receive data from Mathematician to write it somewhere and Device : Context to provide Mathematician with new numbers. Note, they're independant of each other.

static class Program
{
    public static void Main(string[] args)
    {
        Context m = new Mathematician();            
        Context d = new Device();
        Context s = new Secretary();

        d["Sensor1"].AddSubscriber(m.Numbers, /* map must correspond to Device internals */)
        m["StDev"].AddSubscriber(s.Console, /* ... */);


        // Now each sensor1 signal in Device context will be sent to Mathematician, and the results of computations will be sent to Secretary, which can write it to console

        // We can start this system manually like that
        d["Sensor1"].PushRow( /* ... signal data ... */);
    }
}
\$\endgroup\$
  • \$\begingroup\$ my first question (more to come) is why do you have Context as abstract but have no abstract methods in it? abstraction layers help to combine same processes, or have a common method to call. \$\endgroup\$ – Robert Snyder Apr 5 '13 at 11:57
  • \$\begingroup\$ It looks like you are trying to manually implement Tables and DataSet (the .Net versions) that do what you are trying to do, but with a lot of it taken out. Although this is not a bad idea, it can be hard for someone else to come in and review your code (even if that someone is you after a time has passed) Instead you would probably benefit to change the name of Table to say ContextTable. Now there is no ambiguity with Table and System.Web.UI.WebControls.Table or maybe even System.Data.DataTable \$\endgroup\$ – Robert Snyder Apr 5 '13 at 12:05
  • \$\begingroup\$ As for having a function class if I was you I would change it to a interface and add one method called Calculate() or something similar. Then in your Table class implement Function. Then instead of having your different classes implement Context they would instead implement Table. In your math class move the count method over to Calculate Now you can just add Mathmetician to your array of Tables, and loop through them all calling Calculate. \$\endgroup\$ – Robert Snyder Apr 5 '13 at 12:09
  • \$\begingroup\$ @RobertSnyder 1. Because I think about Context like something abstract, it must not be instantiated at any time. But it must have a lot of realisations. It could be an interface, but in this case I must refuse of nice indexer access and other features. \$\endgroup\$ – astef Apr 5 '13 at 13:47
  • \$\begingroup\$ 2. Well, Table is really like System.Data.DataTable and it even can be replaced with it, but I think DataTable is quite complicated and not fit my tasks ideally. What about naming, I am not usually care about ambiguity with other namespaces. Anyway, thanks for idea with ContextTable \$\endgroup\$ – astef Apr 5 '13 at 13:57
1
\$\begingroup\$

I think the single biggest issue with your design is that it's not type-safe and relies on strings a lot.

C# is a statically-typed language, you should take advantage of that and let the compiler check for possible errors. This means:

  1. Making Table generic, so that a row is an object of a specific type, instead of object[]. If you need to “map” data between different types, you can use lambdas.
  2. Tables in a specific context should be properties, instead of accessing them by a string.
  3. You should separate input tables and output tables into separate types (at least on the outside). Mathematician should be able to read from the Numbers table, but others should only write to it. It's a good idea to enforce this by making Numbers something like InputTable<T>.

In general, your requirements are quite similar to TPL Dataflow, you should consider looking at it for inspiration, or even use it in your implementation (though it's .Net 4.5 only).

\$\endgroup\$
  • \$\begingroup\$ I agree that compile-time type-safety is lost and that smells bad. Though, this check can be made at run-time. Moreover, it will be performed at run-time anyway, because connecting different contexts will be performed by user at runtime, using UI. --- I'm coming with next issues... \$\endgroup\$ – astef Apr 5 '13 at 18:48
  • \$\begingroup\$ Now, let's list disadvantages of type-safety in my case. 1. If Table is generic, I must make: Table<T>, Table<T1,T2>, Table<T1,T2,T3>... At first time I would stop at 8. In nearest future I'm sure I'll need up to 14 columns. And that's nothing strange, tables sometime have a lot of columns. 2. Accessing table by a string is neccessary, because I need to make context user (UI, for example) independant from context realisations. For user there is Context, not Mathematician. 3. Actually, I'd like to have full access to read/write to any table. Arguments for this: easy testing, logging, sharing \$\endgroup\$ – astef Apr 5 '13 at 19:02
  • \$\begingroup\$ About TPL Dataflow - thanks much, any external solutions on this topic is very useful for me now. If it will fit my needs, I'll accept this answer \$\endgroup\$ – astef Apr 5 '13 at 19:06
  • 1
    \$\begingroup\$ @astef 1. That's not what I meant. Table<T> should be enough, since T can be any complex type. I don't see why the concept of columns would be useful here. 2. That's not making it actually independent, you still depend on the name of the table. If you meant that you want to be able to easily switch Mathematician for, say, Calculator, then use interfaces. E.g. you could have an interface that contains Table<double> Numbers { get; }. 3. Based on that argument, you should also make all your fields public. I think the advantages of proper encapsulation outweigh the disadvantages. \$\endgroup\$ – svick Apr 5 '13 at 19:44
  • \$\begingroup\$ Main application dependance on different context realisation is not an option. No sense to make Table generic because Table<MathematicianResult> will be external for the application the same as simply MathematicianResult. And I can't see a real problem with strings. Application asks for all table names, then links different tables, using column type checks. Where's the dependence? \$\endgroup\$ – astef Apr 8 '13 at 15:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.