Excuse me that I haven't come up with a good question title. I couldn't just fit this problem in one question title. So I have that application where I convert players to the data structure similar to JSON format:
"players" = {
"EU" = {
"TestServer" = {
"Slacker" = {
},
.
. //more players
},
.
. //more servers
},
.
. //more regions
}
Every player has Region, and Server, and some other data such as Scores.
public class PlayerModel {
public string Name { get; set; }
public RegionModel Region { get; set; } // just has string Name property
public ServerModel Server { get; set; } // just has string Name property
public ScoreModel[] Scores { get; set; }
}
In my application, I have some resource for collecting players data and a method to insert it in:
private Dictionary<string /*region*/,
Dictionary<string /*server*/,
Dictionary<string /*playerName*/, PlayerModel>>> db;
this ridiculous dictionary. I've written a working insert method (included code at the end of question) but It looks really ugly and I convinced myself to write the insert method again and it has come out like this:
private void insertPlayer(PlayerModel player) {
var node = db;
var keys = (new [] {
player.Region.Name,
player.Server.Name
});
var new_node_found = keys.Aggregate(false, (uninit_found, key) => {
if (uninit_found)
node[key] = new Dictionary<string, T>(); // how would I ignore T
else {
if (!node.ContainsKey(key)) {
uninit_found = true;
node[key] = new Dictionary<string, T>();
}
}
node = node[key] // conflicting types
return uninit_found;
});
// should have reached the final node at this point
// leaving the node having equal db[region][key = player.Server.Name]
var key = player.Name;
if (new_node_found || !node.ContainsKey(key)) {
node[key] = player;
} else {
// player already exists at that node, so continue with
// merging the player data with required business logic
}
}
As you can see the problems arise,
- What should T be?
- How would I deal with conflicting types? I'm not working on JavaScript...
So I taught to myself, I was not really interested in:
- The type of dictionary's value, nor the key's type
- Not even dictionary itself
I was just interested in some properties of the dictionary and It could be simplified as:
public interface IPromise<TKey> {
public bool ContainsKey(TKey key);
public object /*WHATEVER*/ this[TKey key];
}
I feel like I can do inherit NET's Dictionary<>, and implement this interface, maybe what would work, but still feeling like this could be done in some other smarter way. Unfortunately I don't have tags and words for making search on search engines about this. Have no clue about how to get over with "WHATEVER" type and things. If I could just write as:
private void insertPlayer(PlayerModel player) {
// If i could just...
var IPromised = interface<TKey> {
public bool ContainsKey(TKey key);
public object this[TKey key];
}
// then...
IPromised<string> node = db;
// I promise you C# compiler, the db has the methods I defined in my interface
var keys = (new [] {
player.Region.Name,
player.Server.Name
});
var new_node_found = keys.Aggregate(false, (uninit_found, key) => {
if (uninit_found || !node.ContainsKey(key) /* meanwhile little opt- has found*/) {
uninit_found = true;
// I still promise you in the future, the dictionary's value still
// be providing the ContainsKey() and array indexor methods.
node[key] = new Dictionary<string, IPromised<string>>();
}
node = node[key]
return uninit_found;
});
NOTE: I created the method as I write this question, I can't guarantee you that code will work as intended. But I think It clearly shows the problem that I'm having. Thanks for your time!
Addendum:
The ugly working version of insertPlayer
private void insertPlayer(PlayerModel player) {
if (!db.ContainsKey(player.Region.Name))
db[player.Region.Name] = new Dictionary<string, Dictionary<string, PlayerModel>>();
if (!db[player.Region.Name].ContainsKey(player.Server.Name))
db[player.Region.Name][player.Server.Name] = new Dictionary<string, PlayerModel>();
if (!db[player.Region.Name][player.Server.Name].ContainsKey(player.Name))
db[player.Region.Name][player.Server.Name][player.Name] = player;
else {
// merge player
var playerRef = db[player.Region.Name][player.Server.Name][player.Name];
playerRef.Scores = playerRef.Scores.Concat(player.Scores).Distinct(new PropertyComparer<ScoreModel>("Level")).ToArray();
}
}
Overview of new insert method:
/* At higest level, human language defination of the function:
I will provide you list of keys, in an order of which follows like:
grandparent, parent, child, grandchild...
if the key is uninitialized at some level, initialize it and continue until
you hit the final key. you don't need to check after this level
because the node is just initialized and rest will have to be initialized too.
if not, and if you find a value is assigned at final key, execute the method
I've provide to you and your job is done.
*/
Update 1
Ended up having this: (following the answer used in this question on SOF:Cast to not explicitly implemented interface?)
private DictKeyEquals<string> db; // modified
... // somewhere in Form initialization:
db = new Dictionary<string, object>().GetPromise();
Extensions:
public interface DictKeyEquals<TKey> {
bool ContainsKey(TKey key);
object this[TKey key] { get; set; }
}
public static class DictionaryExtensions {
private sealed class DictionaryWrapper<TKey, TValue> : DictKeyEquals<TKey> {
private readonly Dictionary<TKey, TValue> dict;
public DictionaryWrapper(Dictionary<TKey, TValue> dict) {
this.dict = dict;
}
public object this[TKey key] {
get => dict[key];
set => dict[key] = (TValue)value;
}
public bool ContainsKey(TKey key) {
return dict.ContainsKey(key);
}
}
public static DictKeyEquals<TKey> GetPromise<TKey, TValue>(this Dictionary<TKey, TValue> dict) {
return new DictionaryWrapper<TKey, TValue>(dict);
}
}
Insert method (gone public in order to execute unit tests):
public void InsertPlayer(PlayerModel player) {
var node = db;
var keys = (new[] {
player.Region.Name,
player.Realm.Name
});
var new_node_found = keys.Aggregate(false, (uninit_found, key) => {
if (uninit_found || !node.ContainsKey(key)) {
uninit_found = true;
node[key] = new Dictionary<string, object>().GetPromise(); // Line A
}
node = (DictKeyEquals<string>)node[key]; // Line B
return uninit_found;
});
// should have reached the final node at this point
// leaving the node having equal db[key = player.Realm.Name]
//var key = player.Name;
var finalKey = player.Name;
if (new_node_found || !node.ContainsKey(finalKey)) {
node[finalKey] = player;
} else {
// player already exists at that node, so continue with
// merging the player data with required business logic
}
}
Update 2
I figured out that I don't need extensions and interfaces, as long as I stick with:
private Dictionary<string, object> db;
with few modifications in Insert Method:
...
node[key] = new Dictionary<string, object>(); // Line A
...
node = (Dictionary<string, object>)node[key]; // Line B
...
I'm still having this question as open, will open bounty for possible better design patterns and features that I might be missing.
Notes for update 2: I feel like I want to leave some opinions for people. Problem has arisen because of I was stubborn on:
private Dictionary<string /*region*/,
Dictionary<string /*server*/,
Dictionary<string /*playerName*/, PlayerModel>>> db;
the dictionary itself from the beginning. What had made me think in wrong pattern was that I taught I knew the maximum depth of JSON tree and limited the tree from the beginning with defining such dictionary. Meanwhile the new Insert_player method was going to be such that It would have no limits to traverse through dictionary. But I had been already limiting this by having the dictionary. As In update 1, the modification with extensions had already been started requiring db to be some dictionary like has its value of typeof object. So as update 2 I got rid of all extensions and interfaces, come up with something more easy to read and understand.