Interfaces and DI #148
DavidRieman
started this conversation in
General
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
We used to have a ton of interfaces. We'd have things like
IWeapon
and then a concreteWeapon
class that matched exactly. We realized this didn't really do anything for us as the concrete classes were not heavy, so cases where you'd want DI for angles like testing didn't have any real wins. What it did do was make it harder to try to mold the framework to newer, better techniques like the Behaviors system (more on Behaviors here), not only due to having to double a lot of changes, but due to extra indirection in seeing what all was being used and where. So those myriad of interfaces that felt redundant are long gone. (For example, if we want to model aThing
to be a weapon, we actually want to compose it with something like aWeaponBehavior
instead.)Ideally the places we most want to use interfaces are to describe generic systems where we don't have to know the full details about those systems (like
ISubSystem
orITelnetOption
) to work with them.In the past, we'd also had various discussions about interfaces on our way through this journey so far. I'll post the more interesting/useful bits here.
This thread contains restored content from prior forums; see #134 for details.
[May 2009]
So we had this idea that Behaviors would be the things that could be used to extend items' abilities. Let's say you have a stick, but then you add the
WeaponBehavior
(and appropriate weapon properties within that behavior), then code like the 'wield' command would allow you to wield any item that has theWeaponBehavior
, etc. Is there any reason we don't ditch the whole interface thing for Items altogether, and give them, say for a barrel you can put items and liquids in, aLiquidContainerBehavior
andItemsContainerBehavior
?Alternatively there could be a property on a
ContainerBehavior
which states what types of things can be put in it: liquids and/or other items. TheContainerBehavior
itself would house the list of contained items as one of its properties, and be responsible for handling persisting the list of contained items when it persists, and tell all its items to persist themselves, etc.[ed. update 2021] Actually contents would likely be
.Children
of the containerThing
.I'm not sure if the "liquid" of a potion is considered an item right now. And we don't have a waterskin with the 'fill' command and room fountains or streams to fill it wilth, etc.
But anyway, the way I understand it as currently designed, a Sword, Shield, Helmet, Gauntlets, etc are just normal Items which have a
WeaponBehavior
,ShieldBehavior
, orArmorBehavior
applied to them.Being able to iterate and detect all
IDrinkable
is great. That would be great for weapons and armor too. The trickiness comes in crossover items; do we need a new class for every conceivable combination?What do you mean I'm not on the side of compile-time enforcement? (BTW I don't have all the answers here, I've actually not done interface-heavy programming before; the polymorphism I've done has generally been sufficient to use only the 'class inherits from class' variety and ignore interfacing altogether.)
I think along the strict Behaviors approach, that generally an Item wouldn't have more than a couple behaviors on average, so it would still be sufficiently performant. Iterating them would still be pretty fast, and since it only occurs when a player does a command or action that is relevant, like attempting to wield a target item, then there tend not to be a blanket 'iterate all behaviors of all items' operation very often. Exceptions might be things like 'find every burnable object on the player and check if we burn it up' or whatnot, but a player doesn't just type 'drink' and have it automatically scan for drinkable items to perform the operation on.
I disagree with the assumptions you imposed about what a base shield is, and the sheer quantity of classes one would have to define with essentially copy-paste coding just for shields alone. Not all shields are metal. Some are wooden. In some settings, they may be riot shields (plexiglass?). In not all cases are they viable weapons either; some tower shields, at least in some game systems, may not be used as such. So your
BasicShield
andFlyingShield
iterations, just from those expansions (and there's probably more) means you combinatorially have to add the following code clases (and C# files):This is clearly out of hand, and will continue to grow exponentially in order for every combination to be available to use by non-coding builders. Either we add more properties to shields as we decide we need them, like material, or add a
MaterialBehavior
, or something else altogether. 100% interfacing is ridiculous already.Yeah we had a good IM debate, and the short of it is that we feel there should be a good compromise: Interfaces for core stuffs, Behaviors for specifics.
[Redacted description of interfaces we actually nixed later.] If you care about making shields that are good for the 'shield bash', add a
ShieldBashableBehavior
to represent shield with spikes or the like. I think we agreeIMetal
is a bad idea, and that material could be a property ofThing
s, even if that usually defaults to 'unknown'.Too many interfaces is bad, especially in compounding combinations, but they are performant and more direct to query.
BehaviorsManager is less performant (having to iterate a list), but allows less impact to DB and building scenarios and such upon expansion.
[ed. update 2021] Actually at this point even defining "metal" things would probably come down to a
MetallicBehavior
orCoreMaterialBehavior
with an enum to describe the core material type or whatnot... which could be used for, say, corrosion attacks to decide if they're going to add aCorrodedEffect
orDamagedEffect
or further destroy the item outright, etc. Such composition approaches mean a given MUD implementation is free to either care about core materials, or not care about core materials if they don't want to.The intent of
IContainerFurniture
was to enable containers to have the same sorts of furnishings that doors can, such as locks and traps. We can probably actually call it something else altogether, and use the same thing for doors and containers. Maybe we just have aLockingBehavior
(with properties for key ID, etc) andTrapBehavior
(with properties for whether the trap sets off upon unlocking or opening, difficulty to spot or disarm it, theEffect
to clone to the target, and so on).Yeah, that's basically what I was saying, that while the intent of the interface made sense, it does really make much more sense as Behaviors of the door. Much more consistent.
The builder's experience portrayed there isn't very great; at the very least we could have them type 'FireTrap' instead of an ID there. Anyway, that's obviously not the point you're making here. Just to remind everyone, I do still have a pretty in-depth in-game builder design in the works, and hope to put more time into it and post it maybe this weekend. I don't have a User Story started for it, but it will very much help for this, and I plan to do that now.
I have no thoughts yet on the out-of-game builder design, and haven't looked at the existing code base there either. (I do have to wonder how one would isolate areas they built at home, send them to a MUD admin, and how the admin would import the data, or if some other process would be involved like exporting an in-game builder sequence of commands used to replicate the area. Darn, now I'm getting totally sidetracked!)
[Mar 2010]
There is some similarity to a .h file, but an interface is completely optional. An interface is essentially a contract which helps enable things like polymorphic code in certain circumstances. Unlike C/C++, in C#, any given class is only allowed to inherit from a single concrete base class, but it IS allowed to inherit from as many interfaces as it likes. The class must then fulfull the interface contract in order to compile. Often, inheritence is used to make guarantees about certain behaviors that a class supports.
For example, if we have the
IStackable
interface which lists theSplit
andCombine
methods and aCount
property... When you then make a classCoin : IStackable
, thenCoin
has to fulfill methods forSplit
andCombine
and provide aCount
property. Elsewhere in the code, say for theDrop
action processing of the "drop X object" pattern, all you need to know about the target object is that it "is"IStackable
, before you canif targetStackable.Count >= X then targetStackable.Split
... and so on. Polymorphism can be quite prevalent in C# code, and makes tons of things much simpler when used right. :)We have too many interfaces in WheelMUD code right now. The way a lot of the interfaces are currently used in WheelMUD is something that seems to be intended for test-driven-development: this is when the interface essentially maps exactly to a class, rather than describing some comon, reuseable sub-behavior of multiple classes. However, since we essentially aren't doing TDD, we've been removing some unneeded interfaces where we can, and using concrete base class inheritence instead. For example, IIRC (I can't look at the code ATM)
Stackable
is a class rather than just an interface, andCurrency
derives fromStackable
instead of justIStackable
. If we then wantedCoin
andPaperBill
classes, we could derive them fromCurrency
; we would probably want such aPaperBill
class automatically gain theIsFlammableBehavior
during its constructors.Ideally, we want to strike an intelligent balance between interfaces, concrete classes, and extensions (such as the Behaviors system). For example, we could choose to have an
IFlammable
interface instead of using a Behavior, but that would require every object type which could potentially be flammable, to fulfill the interface, even for object types which are only sometimes flammable. In this case, it's better to have the Behavior.Also, I believe interfaces sometimes help with interfacing across application boundaries, and may be a basis for utilizing things like MEF effectively. At work, I've never actually had to expose interfaces instead of sharing concrete classes in a shared Project (DLL) to get things to work together well. So mileage may vary based on the complexity of projects, boundaries, and so on.
[Moving DI topics to #149.]
Beta Was this translation helpful? Give feedback.
All reactions