Reputation: 1492
Let me preface this by stating that I have seen similar posts to this, but none of the solutions have satisfied me and/or applied to C#.
I have a Graph
class that consists of Node
and Connection
objects. The graph contains collections consisting of all of the child Node and Connection objects associated with it. In addition to this, each Node has a collection of Connection objects.
Please note: This is a simplified toy problem. You can view the actual (work-in-progress) production code here. In production, a Neuron
is a Node
and an Axon
is a Connection
.
public class Graph : IDeepCloneable<Graph>
{
// These are basically just Dictionary<int,object>s wrapped in an ICollection
public NodeCollection Nodes;
public ConnectionCollection Connections;
public Graph Clone()
{
return new Graph
{
Nodes = this.Nodes.Clone(),
Connections = this.Connections.Clone()
};
}
}
public class Node : IDeepCloneable<Node>
{
public int Id;
public NodeConnectionCollection Connections;
// NodeConnectionCollection is more or less the same as NodeCollection
// except that it stores Connection objects into '.Incoming' and '.Outgoing' properties
public Node Clone()
{
return new Node
{
Id = this.Id,
Connections = this.Connections.Clone()
};
}
}
public class Connection : IDeepCloneable<Connection>
{
public int Id;
public Node From;
public Node To;
public Connection Clone()
{
return new Connection
{
Id = this.Id,
From = this.From.Clone(),
To = this.To.Clone()
};
}
}
public class ConnectionCollection : ICollection<Connection>, IDeepCloneable<ConnectionCollection>
{
private Dictionary<int, Connection> idLookup;
private Dictionary<ProjectionKey, Connection> projectionLookup;
public int Count => idLookup.Count;
public bool IsReadOnly => false;
public Add( Connection conn )
{
idLookup.Add( conn.Id, conn );
projectionLookup.Add( new ProjectionKey( conn.From, conn.To ), conn );
}
...
internal struct ProjectionKey
{
readonly intFrom;
readonly int To;
readonly int HashCode;
public ProjectionKey( int from, int to )
{
From = from;
To = to;
HashCode = ( 23 * 397 + from ) * 397 + to;
}
public override int GetHashCode() { return HashCode; }
}
}
public class NodeCollection : ICollection<Node>, IDeepCloneable<NodeCollection>
{
private Dictionary<int, Node> nodes;
private Dictionary<int, InputNode> inputNodes;
private Dictionary<int, InnerNode> innerNodes;
private Dictionary<int, OutputNode> outputNodes;
...
public Node this[ int id ]
{
get => nodes[ id ];
}
}
Each of these objects support deep cloning, with the main idea being that consuming classes can call Clone()
on child classes and work down the stack that way.
However, this is not viable in production. A call to Graph.Clone()
will clone the NodeCollection
and ConnectionCollection
fields, which will clone each Node
and Connection
instance stored within them, which will each clone other referencing child elements.
A common solution seems to be storing the Ids of each child object and then rebuilding the references when all cloning is complete. However, as far as I am aware, this would require a reference to parent objects and tightly couple the data structure.
I am very puzzled at how to properly approach this. I require a reasonable amount of performance, as my application (a genetic algorithm) performs cloning constantly, but in this case I am more interested in finding a robust design pattern or implementation that will allow me to perform deep cloning of this graph structure while stashing a lot of the gruntwork behind the scenes.
Is there any design pattern that will allow me to clone this data structure as-is while updating circular references and maintaining its integrity?
Upvotes: 2
Views: 322
Reputation: 7352
My suggestion would be to change your approach to the problem from cloning to recreating. I've dealt with a resembling problem, where I was saving a graph user created manually from the user interface, and then upon an import of saved graph I was recreating it. It sounds almost the same if you think about it.
So the solution I came up with was serializing the graph from a central control (considering you are modifying graphs with an heuristic I assume you have central control over the graph). Even if you don't have a central control over the graph I believe it can be traversed in a way to get all the information.
In the simplest form a graph is a collection of neighborhood information.
Can be directed or undirected as well
1 -> 2
1 -> 3
3 -> 2
So if you can come up with a way to generate a list like this, after just tweaking this simple list, you can create your new graph.
Or another approach would be to list your nodes with their neighbors like below,
1, [2,3]
3, [2]
This would even be simpler to recreate the graph in my opinion.
Here is the file from the project I applied this approach if you are curious about - I don't think it would be a reference for the answer or question though.
Upvotes: 1