Exclusion of the connection of classes having strong conceptual connections with each other

I have types Rock , Paper and Scissors . These are the components or "hands" of the game Rock, Paper, Scissors. Given the hands of two players, the game must decide who wins. How to solve the problem of saving this chain diagram

Rock, Paper, Scissors chart

without connecting different hands to each other? The goal is to add a new hand to the game (e.g. Jon Skeet) without changing any of the others.

I am open to any proxy idea, but not to large switch statements or code duplication. For example, introducing a new type that governs chain comparisons is fine if I don't need to change it for every new hand I add. Again, if you can rationalize the existence of a proxy server, which should change for each new hand or when the hand changes, this is also welcome.

This is a kind of Design 101 problem, but I'm curious what solutions people can come up with for this. Obviously, this problem can easily scale in much larger systems with many other components with any arbitrarily complex relationships between them. That is why I set a very simple and concrete example for the solution. Any paradigm used, OOP or otherwise, is welcome.

+6
language-agnostic design
source share
6 answers

Have a GameStrategy class that implements the Win method. The victory method takes a list of hands and returns either the Hand - if there is a winner - or zero if the game was a tie. I think that a winning strategy is not really a property of the hand, but a game. Include the determination of the winner of a pair of hands in the GameStrategy class.

EDIT : Potential Strategy

 public enum RPSEnum { Rock, Paper, Scissors } private RPSEnum FirtRPS = RPSEnum.Rock; private RPSEnum LastRPS = RPSEnum.Scissors; public Hand Win( Hand firstPlayer, Hand secondPlayer ) { if (firstPlayer.Value == FirstRPS && secondPlayer.Value == LastRPS) { return firstPlayer; } else if (secondPlayer.Value == FirstRPS && firstPlayer.Value == LastRPS) return secondPlayer; } else { int compare = (int)firstPlayer.Value - (int)secondPlayer.Value; if (compare > 0) { return firstPlayer; } else if (compare < 0) { return secondPlayer; } else { return null; } } } 

To add a new hand value, simply add the value to RPSEnum in the correct sequence. If this is the new “lowest” hand, upgrade FirstRPS. If this is the new "highest" hand, update LastRPS. You do not need to change the actual algorithm at all.

NOTE. This is more complicated than it should be for only three values, but the requirement was to add additional values ​​without updating a lot of code.

+4
source share

If they have sufficient conceptual similarities, you may not want to lose your temper by reducing traction.

"Binding" is actually just a metric of how much code breaking will happen if the internal implementation of one thing is changed. If the internal realization of these things is inherently sensitive to the rest, then it is; communication reduction is good, but software should, in the first place, reflect reality.

+2
source share

I don’t think that different hands are different types: they are separate instances of the same type. This type has attributes such as name, possibly image, etc.

You initialize the game by loading from the data a list of hand names and a matrix giving a hand that beats each hand. Perhaps the data will be loaded into the Game class using the Compare method.

+1
source share

In this case, it is important that if you compare two objects, you return the result.

So, basically, you need to separate this comparison in such a way that you can not only add a new type of object to the mix, but also add comparison rules to the mix that this object can process.

Now I also commented on your question: “Sometimes the questions can be too universal,” and the problem is that no matter how I tell you how to do what you ask, it will not help you one bit about the real problem.

If you are not building a rock paper-scissors-X game.

Personally, I would do the following with my own IoC container.

 ServiceContainer.Global.RegisterFactory<IHandType>() .FromDelegate(() => RandomRockScissorPaper()); ServiceContainer.Global.RegisterFactory<IHandComparison, DefaultHandComparison>(); 

(or rather, I would configure above in the app.config file or similar, so that it can be changed after creating the project).

Then, if the user / client / endpoint needs to be redefined to add another type, I would increase the registration as follows (remember that the above are in app.config, so I would replace the ones listed below):

 ServiceContainer.Global.RegisterFactory<IHandType>() .FromDelegate(() => RandomRockScissorPaper()) .ForPolicy("original"); ServiceContainer.Global.RegisterFactory<IHandType>() .FromDelegate((IHandType original) => RandomRockScissorPaperBlubb(original)) .WithParameters( new Parameter<IHandType>("original").WithPolicy("original")) .ForPolicy("new") .AsDefaultPolicy(); 

Here I add a new way to solve IHandType, not only preserving the original way, but also adding a new one. This new one will be given the result of calling the old one, and then it will be necessary to decide internally whether a random chance should return the fourth type or the original type (one of the three original ones).

Then I would also override the original comparison rule:

 ServiceContainer.Global.RegisterFactory<IHandComparison, DefaultHandComparison>() .ForPolicy("original"); ServiceContainer.Global.RegisterFactory<IHandComparison, NewHandComparison>() .ForPolicy("new") .AsDefaultPolicy() .WithParameters( new Parameter<IHandType>("original")); 

Here's how it will be used:

 IHandType hand1 = ServiceContainer.Global.Resolve<IHandType>(); IHandType hand2 = ServiceContainer.Global.Resolve<IHandType>(); IHandComparison comparison = ServiceContainer.Global.Resolve<IHandComparison>(); if (comparison.Compare(hand1, hand2) < 0) Console.Out.WriteLine("hand 1 wins"); else if (comparison.Compare(hand1, hand2) > 0) Console.Out.WriteLine("hand 1 wins"); else Console.Out.WriteLine("equal"); 

Here's how to implement:

 public interface IHandComparison { Int32 Compare(IHandType hand1, IHandType hand2); } public class DefaultHandComparison : IHandComparison { public Int32 Compare(IHandType hand1, IHandType hand2) { ... normal rules here } } public class NewHandComparison : IHandComparison { private IHandComparison _Original; public NewHandComparison(IHandComparison original) { _Original = original; } public Int32 Compare(IHandType hand1, IHandType hand2) { if hand1 is blubb or hand2 is blubb then ... else return _Original.Compare(hand1, hand2); } } 

After writing all this, I understand that my app.config configuration will not be able to handle delegates, so I need a factory object, but the same thing applies anyway.

You need to decide how to get new hands, and to allow rules for winning hands.

0
source share

Why is it falling away? These elements are all connected to each other, adding a new hand, this will not change.

You just have a base hand class that extends to Rock, Paper and Scissors. Give the base class the .beatenby attribute, which takes one of the other classes of type Hand. If you encounter a situation where a hand can be beaten with more than one hand, just create the .beatenby attribute instead.

0
source share

A few other answers provided very flexible and very untied solutions ... and they are much more complicated than they should be. The short answer to the denouement is double dispatch , which is supported by very few languages. The GoF visitor pattern exists to simulate double sending in languages ​​that support single sending (any OO language supports this, even C with function pointers).

Here is a slightly larger example in the same vein. Suppose you control HTTP traffic and you try to classify patterns based on a combination of a request method and a response code. Instead of looking at this series:

 rock/paper paper/scissors paper/paper ... 

... you look at this series:

 GET/200 GET/304 POST/401 POST/200 ... 

If the system had HttpRequest and HttpResponse objects, the easiest way to send it would be to use a single function that will route to all possible options:

 HttpRequest req; HttpResponse resp; switch ((req.method, resp.code)) { case (GET, 200): return handleGET_OK(req, resp); case (GET, 304); return handleGET_NotModified(req, resp); case (POST, 404): return handlePOST_NotFound(req, resp); ... default: print "Unhandled combination"; break; } 

At the same time, the addition of new types does not affect your base objects. However, your classification method is still very simple and therefore supported by other developers. If you want (or if your language simplifies), you can turn this switch into some kind of function map and register handlers using various combinations of request methods and response codes at startup.

0
source share

All Articles