I've recently been learning about design patterns. Now I decided that I know enough to be able to refactor my inventory code. The question solely exist so I could show my fellow programmers the kind of destructive and lame mindset you'd get out of OOP, SOLID etc. Adder.Add<CoolAdd>(new CoolAddArgs(1, 2, 3, etc)
instead of the simple function CoolAdd(1, 2, 3, etc)
is just absurd.
In an inventory, there are items. Wherever there are items, there's adding and swapping items. In my inventory, there's more than one way to add or swap items - NormalAdd
, ForceAdd
, MouseSwapAdd
, etc. SnapSwap
, HoldSwap
, etc. (Video)
Currently, I have all those methods in my ItemsBag
. I decided to move them outside, to an Adder
/Swapper
.
I came up with an interesting system, which I think is SOLID. It's a mixture of the strategy and a bit of factor method patterns.
Usage:
Adder adder = new Adder();
adder.Add<NormalAdd>(new NormalAddArgs(1, item, slot));
adder.Add<ForceAdd>(new ForceAddArgs(index));
Swapper swapper = new Swapper();
swapper.Swap<SnapSwap>(new SnapSwapArgs(1, 2, 3, etc));
swapper.Swap<HoldSwap>(new HoldSwapArgs("", 1));
The idea, is that you have a Strategy
that does something, given an Args
. Now Adder
and Swapper
are StrategyImplementers
.
Adder
is anAddingStrategy
implementer.NormalAdd
andForceAdd
areAddingStrategy
.Swapper
on the other hand, is aSwappingStrategy
implementer.
Different strategies have different arguments, that's why a custom Args
object has to be created.
Diagram:
Code:
(This is the main part - the bottom left part of the diagram)
public abstract class Args
{
}
public abstract class Strategy
{
public abstract bool Implement(Args args);
}
public static class StrategyCreator
{
static public T Create<T>() where T : Strategy, new()
{
return new T();
}
}
public abstract class StrategyImplementor<T> where T : Strategy
{
private Dictionary<Type, T> dic = new Dictionary<Type, T>();
protected bool Implement<TStrategy>(Args args) where TStrategy : T, new()
{
Type type = typeof(TStrategy);
T strategy = GetStrategy(type);
if (strategy == null) {
strategy = StrategyCreator.Create<TStrategy>(); // lazy
dic[type] = strategy;
}
return strategy.Implement(args);
}
private T GetStrategy(Type type)
{
T strategy;
return dic.GetValue(type, out strategy);
}
}
(Here's the adding stuff - swapping is pretty much the same)
public class Adder : StrategyImplementor<AddingStrategy>
{
public bool Add<T>(AddArgs args) where T : AddingStrategy, new()
{
return Implement<T>(args);
}
}
/* <<< Adding Strategies >>> */
#region
public abstract class AddingStrategy : Strategy
{
}
public class NormalAdd : AddingStrategy
{
public override bool Implement(Args args)
{
if (!(args is NormalAddArgs))
throw new Exception("NormalAdd: Invalid args!");
Console.WriteLine("Normal add");
return true;
}
}
public class ForceAdd : AddingStrategy
{
public override bool Implement(Args args)
{
if (!(args is ForceAddArgs))
throw new Exception("ForceAdd: Invalid args!");
Console.WriteLine("Force add");
return true;
}
}
#endregion
/*<<< Strategy Args >>> */
#region
public abstract class AddArgs : Args
{
}
public class NormalAddArgs : AddArgs
{
public int row;
public int col;
public Etc etc;
public NormalAddArgs() { }
public NormalAddArgs(int row, int col, etc)
{
this.row = row;
this.col = col;
}
}
public class ForceAddArgs : AddArgs
{
public int id;
public Etc etc;
public ForceAddArgs(int id, etc)
{
this.id = id;
}
}
#endregion
So as you can see it's very easily to extend and create new strategies without affecting any existing code. All I have to do to add a new CoolAdd
strategy is write CoolAdd
and CoolAddArgs
and that's it! Now I can:
Adder.Add<CoolAdd>(new CoolAddArgs(1, 2, 3, etc);
The one thing I didn't like about it, is imagine how many arrows will come out of the consumer - i.e. the class that's gonna use the code in the Usage section above. It's gonna have to know about all these stuff... Adder
, NormalAdd
, ForceAddArgs
, etc
Me and my buddy (J), who is helping me with my design patterns study, decided that there has to be something done to reduce dependency, maybe to be able to write Adder.Add<NormalAdd>(1, "string", item, etc);
. But there wasn't a clear way of doing so...
But then he told me, why not just create an Adder
with the adding methods inside, and then when the user wants to add/extend the existing behavior, they he just extends Adder
and add his methods there!
public class Adder
{
public void NormalAdd() { }
public void ForceAdd() { }
etc
}
Later on:
public class MyAdder : Adder
{
public void CoolAdd() { }
}
I care so much about extending the behavior, without modifying existing code because I will be shipping this inventory. If a user does that and I release an update, his changes will be overwritten. I think both our systems guard against that.
Now, my friend's system was very easy to overlook for me because I was so focused on implementing/finding use of design patterns.
I thought a lot about which system is better and why, here are my thoughts in terms of:
Single responsibility (S in SOLID):
I think my system wins, since I have very focused and highly cohesive modules, that each does a very specific thing and nothing else. In his system, it's not very focused. In real code, there might be some adding methods, that does a lot of calculations that they require some helper methods, where would you put those helper methods then? What if
ForceAdd
hadhelper1
,helper2
andhelper3
? Where would you put those? In theAdder
itself? Do so and the S is broken. In my system, anyForceAdd
-related stuff, is in theForceAdd
.Open/closed principle (O in SOLID):
There is no doubt that my system follows this rule to the bone! How about his? Well, even though every time a new adding method needs to be added, the Adder programmer has to go and add the method to the Adder thus he's modifying the Adder in a way. But I don't think the O is broken here, since he's not modifying any of the pre-existing adding methods. If a user wants to add extra adding methods, he'd extend the original adder and add his methods there, not affecting anything.
Liskov substitution principle (L in SOLID):
Who cares? let's light some flares! I didn't really know how to compare the two in terms of L.
Interface segregation principle (I in SOLID):
I think my system wins again. It's true we don't have interfaces, but the idea behind I, is not to carry extra luggage that you ain't gonna need (YAGNI). In his system, if I wanted to add
CoolAdd
I'd have to make a new adder that extend the original adder just so that I could useCoolAdd
. In other words, I don't really care about the other adding methods that my new adder will have, due to inheritance from the original adder! Extra luggage.Dependency inversion principle (D in SOLID):
I'm not sure, but I think we both play nice with this. Since we both allow the user to inject the parameters needed for something to work.
Simplicity/Less complexity:
Obviously, my system if far more complex and sophisticated.
Less/fewer dependencies and loose coupling:
He wins here again, since there's very little that the consumer has to know about things. In fact, the consumer only knows about Adder -
adder.NormaAdd(1, 2, etc);
.Less garbage:
That's an obvious win for him, since I got a lot of
new
ing going on.Less code, fewer classes, less bothersome:
Obviously, in his system you write less code, fewer class thus you're less bothered. But is that an important factor?
Testing:
I'm not a unit testing expert, but from what I know, when you make separate focused modules that have single responsibilities, they are easier to test - So it's easy to test each strategy and see how it behaves, since it's a separate object. In his system, all the adding methods is in one class - the Adder. I'm not sure if that will make it hard to test...
Readability:
Both are readable, I guess.
Maintainability:
I think mine is easy to maintain since it's divided to separate components. In his, the Adder could get messy if a lot of adding methods were added...
Performance:
I can't tell without benchmarking, but I'm gonna give this to him. Since I take some time in my casts and
new
ing.Re-Usability:
I think I currently fail here, since if I wanted to create an adding strategy that takes the same arguments that another adding strategy takes, I can't do that. I have to create a custom args object. So I can't make use of my existing args. In fact, I should look to fix that. I'm not sure about his system...
Summary:
________________________________________ | | V | J | ---------------------------------------- |S | 1 | 0 | |O | 1 | 0.5 | |L | ? | ? | |I | 1 | 0 | |D | 1 | 1 | |Simplicty/Less complexity | 0 | 1 | |Less depe/Loose coupling | 0 | 1 | |Less garbage | 0 | 1 | |Less code, fewer classes | 0 | 1 | |Testing | 1 | 0 | |Readability | 1 | 1 | |Maintainability | 1 | 0 | |Performance | 0 | 1 | |Reusability | 1 | 0.5 | |______________________________________| |Final Score | 8 | 8 |
- What do you think about the two approaches?
- Which system would you use? Which system you think is best and why?
- Are my assessments correct?
- Is there an important factor I missed that will make a big difference?
- Is there a disadvantage to either systems that you see I missed?
swapper.Swap<SnapSwap>(new SnapSwapArgs(1, 2, 3, etc));
That's a lot of repetition. \$\endgroup\$