I have used the factory pattern for one of my projects. I want to know if this is a good design or if my code can be improved to make it as fast as possible.
I am not an expert in C#.
This is what I came up with after 2 days of research, with lots of rewrites.
What I want to ask is:
Is this the right approach?
Can I improve it to be faster and safer?
Source Problem: There is incoming data which includes JSON data. Data structure is like:
char.base {"level": 20, "name": "somename"}
What I call first char.base
part is the module.
There are 12 modules like this and each of them have different properties.
// ./Models/Model.cs
namespace Example.Models
{
public abstract class Model
{
}
}
Those 12 models have their own classes which uses this Model
class as their base. Example Model:
// ./Models/Base.cs
namespace Example.Models
{
public class Base : Model
{
public int Level { get; internal set; }
public string? Name { get; internal set; }
}
}
Factory class:
namespace Example
{
internal abstract class Factory
{
public Type Type { get => typeof(Factory); }
internal abstract void Deserialize(string json);
}
internal class Factory<T> : Factory where T : Model
{
public new Type Type { get => typeof(T); }
private JsonSerializerOptions _options = new() { PropertyNameCaseInsensitive = true };
private Root _root { get; set; }
internal Factory(Root root)
{
_root = root;
}
internal override void Deserialize(string json)
{
T? obj = JsonSerializer.Deserialize<T>(json, _options);
if (obj == null)
throw new ArgumentNullException($"{typeof(T)} cannot be deserialized with the given data: {json}");
_root.AddOrUpdate(obj);
}
}
}
Finally Root class that gets the incoming data and sends to the right factory:
namespace Example
{
public class Root
{
private readonly Dictionary<string, Factory> ModelFactory;
private readonly ConcurrentDictionary<Type, Model> Models = new();
public Root()
{
ModelFactory = new()
{
{ "char.base", new Factory<Base>(this) },
// 11 more like this
};
}
public void Parse(string text)
{
int i = text.IndexOf(' ');
if (i < 0)
return;
// text: char.base {...}
// text: [module] [JSON]
string module = text[..i].ToLower();
string json = text[(i + 1)..];
if (!ModelFactory.ContainsKey(module))
return;
ModelFactory[module].Deserialize(json);
}
public T? Get<T>() where T : Model, new()
{
return (T)Models.GetOrAdd(typeof(T), _ => new T());
}
internal void AddOrUpdate<T>(T value) where T : Model
{
Models.AddOrUpdate(typeof(T), value, (key, oldValue) => value);
}
}
}
The codes that needs these models will get it like this:
var base = root.Get<Base>();
Edit
Here is some sample incoming data:
char.base { "name": "Valour", "class": "Warrior", "subclass": "Soldier", "race": "Elf", "clan": "wolf", "pretitle": "Testing ", "perlevel": 1000, "tier": 1, "remorts": 7 }
char.vitals { "hp": 100000, "mana": 90000, "moves": 41599 }
comm.tick { }
There is nothing going on with the Model classes. Just some getter properties according to these JSON data.
1 Answer 1
I think you have over-engineered the problem. You don't need neither the Factory
nor the Factory<T>
classes.
public class Root
{
private readonly Dictionary<string, Type> ModelMapping;
private readonly ConcurrentDictionary<Type, Model> Models = new();
private readonly JsonSerializerOptions Options = new() { PropertyNameCaseInsensitive = true };
public Root()
{
ModelMapping = new()
{
{ "char.base", typeof(Base) },
// 11 more like this
};
}
public void Parse(string text)
{
int separatorIdx = text.IndexOf(' ');
if (separatorIdx < 0) return;
string module = text[..separatorIdx].ToLower();
if (!ModelMapping.ContainsKey(module)) return;
string json = text[(separatorIdx + 1)..];
var parsed = Deserialize(json, ModelMapping[module]);
Models.AddOrUpdate(ModelMapping[module], parsed, (key, oldValue) => parsed);
}
private Model Deserialize(string json, Type parseTo)
=> JsonSerializer.Deserialize(json, parseTo, Options) as Model
?? throw new ArgumentNullException($"{parseTo} cannot be deserialized with the given data: {json}");
public T? Get<T>() where T : Model, new()
=> (T)Models.GetOrAdd(typeof(T), _ => new T());
}
- I've replaced your
ModelFactory
toModelMapping
(mapsstring
toType
) - I've moved the
Deserialize
method from theFactory
to here- I've also changed the return type from
void
toModel
- Now the
Parse
can call theAddOrUpdate
method
- I've also changed the return type from
- I've made the
Deserialize
work onType
rather than expecting aT
type parameter
I've used to following code for testing:
var @base = new Base { Name = "test", Level = 20 };
var source = @"char.base " + JsonSerializer.Serialize(@base);
var root = new Root();
root.Parse(source);
var newBase = root.Get<Base>();
Console.WriteLine(newBase.Level == @base.Level);
-
1\$\begingroup\$ Thanks. It seems I really over-engineered the problem. I always forget the easy method is the best method most of the time! \$\endgroup\$Valour– Valour2021年11月11日 14:40:32 +00:00Commented Nov 11, 2021 at 14:40
-
1\$\begingroup\$ @Valour In addition of @Peter-Csala answer, It would be more feasible if you just make use of
JsonSerializerOptions
and add customJsonConverter
to work with these incomingJson
. find out more about it here docs.microsoft.com/en-us/dotnet/standard/serialization/… \$\endgroup\$iSR5– iSR52021年11月11日 15:27:10 +00:00Commented Nov 11, 2021 at 15:27 -
\$\begingroup\$ Can I use records instead of class in models with this code? \$\endgroup\$Valour– Valour2021年11月11日 19:33:13 +00:00Commented Nov 11, 2021 at 19:33
-
\$\begingroup\$ @Valour yes you can. \$\endgroup\$Peter Csala– Peter Csala2021年11月11日 19:41:42 +00:00Commented Nov 11, 2021 at 19:41
switch
statement? But I will have to rewrite same code for every model with against with DRY coding? \$\endgroup\$Model
derived classes? It would give us more context why did you choose Factory method. \$\endgroup\$There are 12 modules like this and each of them have different properties.
can you give us two different samples of theJson
data \$\endgroup\$