0

Suppose you were building a system that receives data on one end and sends filtered data on the other end.

The system is a chain of nodes, each receiving data from the node before and sending filtered data to the next one.

You have two approaches to choose from regarding the way the nodes would be connected:

A- Each node holds a reference to the next one through a particular interface.

B- Each node exposes an event, and the next node in line would be registered to that event, notified when that event is fired (when the previous node in line sends data).

In both cases, an outside entity connects the nodes to one another; they have no direct knowledge of each other.

Currently, I think events may be better because this way, a node doesn't care what the next node does; only what it has to send it. Thus enabling us to insert nodes in the middle easily.

There is a possibility nodes would need to be inserted in the middle, in the future.

Taking that into account, which approach would you prefer, and why? What are the pros and cons of each approach?

  • Can you toss in some specifics (e.g. context of what the nodes are)? – theMayer Jan 15 '15 at 23:38
  • @rmayer06 Please see the paragraph I added near the end, it adds an important detail. – Aviv Cohn Jan 16 '15 at 5:39
  • @rmayer06 I deleted that paragraph, it's no longer needed. I have a different question: can you explain how references are easier than events? – Aviv Cohn Jan 16 '15 at 11:21
  • 3
    @AvivCohn: I "smell" a good question behind a too unspecific wording. Try to give an example, maybe using some code. – Doc Brown Jan 16 '15 at 12:36
  • 1
    I really think this is too broad. There are specific benefits and drawbacks to each. Generally using events is more complicated, but it really depends on what problem you need to solve. – theMayer Jan 16 '15 at 12:52
4

It depends on the layering of the design. If "nodes" are all in the same layer conceptually, then I would prefer references over events, especially read-only references. References are simpler and easier to trace with static analysis tools:

  • References can be read-only fields, whereas event fields are always mutable.
  • A reference field refers to a single object instance, whereas event fields contain a linked list of delegates.
  • A reference field is strongly typed. An event field is weakly typed, as any method can be hooked to the event. Stronger typing can reduce errors, as they occur at compile time, and makes it easier to browse or analyze the compiler output using analysis tools.

If some nodes are in a lower-level layer, I would use events for communication from the lower to the upper layer, in order to avoid references from a lower to a higher layer.

Your question is vague, and there is not enough information to make specific recommendations.

  • Please see the paragraph I added near the end, it adds an important detail. – Aviv Cohn Jan 16 '15 at 5:39
  • Actually I deleted that paragraph, that requirement is no longer needed. However I have a different question: how are references easier than events? Can you give examples? – Aviv Cohn Jan 16 '15 at 11:20
  • How does an event avoid having an inter-layer reference? Surely, the event source must hold a reference to the event receiver(s), otherwise how would it know where to send the event? – Jules Jan 16 '15 at 14:00
  • @Jules: you are correct, it does not avoid an inter-layer reference to the instance. But it does avoid an inter-layer reference to the type in the higher layer, which would enable you to compile the code. Ideally you would avoid lower to higher layer references with instances as well, but that would be impossible in your specific case. Most likely the layering would put all node types into the same layer, if you need to connect them together. – Frank Hileman Jan 16 '15 at 18:17
  • 1
    @FrankHileman The usual solution to that problem is to have the higher-level object implement an interface defined by the lower level (aka dependency inversion). Using an event just because there's a layering issue seems to me to be overkill. – Jules Jan 16 '15 at 19:25
2

Identifying the pattern.

This pattern is actually pretty smart. In fact it's so useful that Microsoft has written a considerable and well liked library called Reactive Extensions to solve this using a technique called functional reactive programming. The pattern you're trying to name is called an Observable.

It generalizes an IEnumerable fundamentally. You have a collection of items you'd like to steam through a series of transformations like mapping, filtering aggregating and so on. However - you also have a Task fundamentally since you have a time dependent action which is not immediately available.

Basically we have a chart that goes something like this:

          |   Singular    | Plural
------------------------------------------------
Spatial   |   Value           | Enumerable<Value>
Temporal  |   Task<Value>     | Observable<Value>

Conceptually an observable is what models your problem. There has been a lot of research into this and you should really avoid inventing the wheel if possible. The flow of using an observable is something like this:

The workflow when using an observable

I link to actual examples later.

  • You start with a source - for example a timer, or a stream of data packets, a function that makes a request every few seconds or back-to-back and so on.
  • Then, transformations are applied at each turn - you can take an existing observable and .Select it into a new mapped observable or .Where it into a filter. You can also debounce it, flatMap it and perform interesting transformations. This is like LINQ only LINQ represents a single deferred one time query where an Observable represents an ongoing process.
  • You can unwrap the observable by subscribing to it, executing a function on its end.
  • You can fork an observable creating not only "lists of nodes" but whole trees.

This concept of a functional observable that chains is not only more powerful than events and aggregates well (You can merge observables, test them easily and so on) - it's also very simple. Your code can look something like:

GetDataItems() // returns a new observable
.Where(IWantItemsForTheNextRound) // filtering logic
.Select(Mapper) // mapping logic
... // any more transformations

Just like working with IQueryable in LINQ the old transformations don't care and are not aware of what's passed from then on. Like LINQ you get a strong abstraction over processing a collection - only this time there's time involved.

You can insert a node in the middle by putting a hook for it and injecting a function that does something with that hook - if you flat map on that you can put arbitrarily complicated logic there extending it as much as you want.

Actual theory and reading.

There is plenty of theoretical material about why this approach is very robust in KrisKowal's gtor or Erik Meijer's Duality and the end of reactive.

  • It's worth mentioning that the events approach is similar. Just like the imperative equivalent of a Task is a callback - an event emitter that shoots a single event multiple times is the imperative equivalent of an Observable. Just like Tasks are stronger than callbacks and people prefer them so are Observables stronger than events in this case. – Benjamin Gruenbaum Jan 17 '15 at 20:08
0

In a single application references are easier. Above all, they will allow you to process data asynchronously.

If your nodes can be located in different applications (even in different computers) you'll need an event-based pattern like Message Queue. This pattern can be implemented with help of .NET events in each single application.

So, if you don't plan to scale your application, it's easier to use references instead of events.

  • Can you explain why references are easier? (Loosely coupled) references require you to create explicit interfaces. Events allow the nodes to register to one another without the the need for interfaces. So why are references easier? – Aviv Cohn Jan 16 '15 at 11:18
  • References are easier because of a few things. We can set them in the constructor so we know they are always valid. We know they always go to exactly one place, whereas events can be sent to multiple receivers. For most purposes, an explicit event handler type needs to be created in order to use events, too. – Jules Jan 16 '15 at 14:05
  • Hi, there's a possibility that new nodes would need to be added in the future in the middle. Events allow us to not care about to whom we're sending, only what we have to send it. Does this possible future requirement make events more suitable? – Aviv Cohn Jan 17 '15 at 16:45
  • The problem is that in additional to the development, you'll need the debugging and the maintaining. Someday you'll have a question "why this data fall into this branch, it shouldn't". Then references will be much more useful than events. Think about asynchronous processing also. – Mark Shevchenko Jan 17 '15 at 22:07
0

This is a tricky question :) If I really have to choose from only this two options, I would choose A for the simpler form of your task and B for the extended. That is because events does not really make sense until you are calling exactly one function.

The primary responsibility of your nodes is to filter the data that they receive. Still when you change the structure of your graph, you also have to change your node class, although the change has nothing to do with its primary function.

The way around this is to conform to the Single Responsibility Principle (SRP). Make the nodes sole responsibility the filtering, and create a class what transfers the data between nodes.

So here is option C:

interface InputNode
{
  DataBuffer getData();
}

interface OutputNode
{
  void setData(DataBuffer data);
}

class FilterNode : InputNode, OutputNode
{
  ...
}

Implement this node in a manner that its sole responsibility is to do the filtering. Make it absolutly unaware of that other nodes exist at all! And this is the interface of the class that transfers the data between nodes:

interface DataTransfer
{
  InputNode {get; set;}
  OututNode {get; set;}
  void transfer();
}

And thats how your graph should look like:

class Graph
{
  private List<DataTransfer> transfers;

  public Graph()
  {
    //initialize nodes and transfers
    ...
  }

  public void setData(DataBuffer data)
  {
    transfers[0].setData(data);
  }

  public void process()
  {
    foreach(var transfer in transfers)
      transfer.transfer();
  }

  public DataBuffer getData()
  {
    return transfers.Last().getData();
  }
}

If you want to extend this solution as you described, you will need to change the DataTransfer interface to have a list of output nodes, and you will need to change the graph representation so that it can do the branching.

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.