Lets first respond to your initial question:
is it fine to typecast from one implementation to another?
Absolutely! When using interfaces to declare behaviours like you have shown, not only is it fine but it becomes important that you DO typecast, otherwise what was the point of declaring the behavior through interfaces in the first place.
The use of interfaces implies that not all classes will implement the behaviour, so you can ONLY access the behaviour if your code knows the exact type of the concrete class or if you know the interface of the behaviour you want to execute, perhaps more importantly you may need to handle the case where the concrete class does NOT implement the expected interface.
Should you have to Type Cast?
If you are using interfaces, then yes you should have to typecast.
A better code implementation for WeaponReload would be to check first that the class does in fact implement IReloadable
before reloading, since c# 7.0 we can use the is
operator to test an expression against a pattern and we can capture the output in a single line of code:
if (weapon is IReloadable reloader)
reloader.Reload();
Notice we didn't even need to use the Wrapper Client class WeaponReload
to access the IReloadable.Reload()
behaviour, I think this is what you are referring to with:
Is there a better way to handle this object creation part
Yes, the better way is to skip the Clients altogether.
If you are set on your current implementation, then that is just as easy to incorporate, however you should still handle the cases where the passed in object does not implement your interface.
WeaponReload reloader = null;
if (weapon is IReloadable r)
reloader = new WeaponReload(r);
...
if (reloader != null)
reloader.Reload();
Is there an entirely better design that I can come up with which does not necessarily ends up in casting.
There is nothing wrong with casting, it can lead to clear and concise code as you have created clear structural boundaries that can help you separate implementation from definitions.
If you are concerned about performance, then we have a solution for that too, but don't optimise your code for the sake of it, the performance losses due to boxing values into objects, though comparitively expensive to alternatives, wil not generally be noticied by the user unless you are performing millions of operations in quick succession.
To avoid performance issues caused by Boxing and Unboxing in C# we can use Generics. Interfaces play a huge part in C# generics, a generic type argument on a class or method defines all the interfaces or conditions that an object
MUST implement for it to be passed into the methods as a parameter or stored in a variable.
Because these conditions are validated and enforced by the compiler, the storage and referencing to the underlying object stored in a generic typed variable is a lot more efficient at runtime compared to simply boxing objects into an object
typed variable.
a Better design
Your interfaces are on the right track, interfaces allow your concrete classes to define explicit behaviors without having to incorporate those behaviours into their base class.
What you have missed here is a base class that all your weapons can inherit from, it's the first logical assumption that one makes when rationalising a domain that includes different types of weapons like shotguns and swords... Common properties might include Name, Description, Cost, BasePower... I would argue that all weapons have a Primary attack function, so really a Weapon
base class would be replacing IWeaponPrimaryAttack
:
public abstract class Weapon ()
{
public string Name { get;set; }
...
public abstract void DoAttack();
}
public interface IWeaponSecondaryAttack
{
void DoSecondaryAttack();
}
public interface IReloadable
{
void Reload();
}
Now for the concrete class implementations, I would classify the weapons a bit differently again, I like to write down a breif story style description about the classes before I write the code, using the following phrasiology to help validate that my classes and interfaces make sense and can be utilised effectivly:
A Katana
IS a Weapon
, it has a Name of 'Katana' but it does NOT have a Reload
feature, it's primary attack is a 'Slash' and it has a SecondaryAttack
that is a 'Stab'.
A Shotgun
IS a Weapon
, it has a Name of 'Shotgun' and it DOES have a Reload
feature, it only has a capacity of 2
shells before it must be reloaded, it does not have a SecondaryAttack
.
This will work with your current interfaces, but notice already we have identified an element of redundancy, the Name
is always the same as the type
. This isn't a problem, but it is an indicator that there is potential room for improvement. Instead lets create classes that categorise the weapons :
In real life and in many games, you will find weapons categorised more granularly, for instance Swords vs Guns, Two handed vs One Handed, or Melee vs Ranged... Swords might include daggers and daggers can easily be Ranged as well as Melee...
Instead if we change our concrete classes to this:
public class Sword : Weapon, IWeaponSecondaryAttack
{
public override void DoAttack() => Console.WriteLine("Slash");
public void DoSecondaryAttack() => Console.WriteLine("Stab");
}
public class Gun : Weapon, IReloadable
{
public override void DoAttack() => Console.WriteLine("Shoot");
public void Reload() => Console.WriteLine("Reload");
}
We haven't yet handled the capacity of attacks before reload, should be easy to add this into the IReloadable
interface, so we'll leave this out for.
Now we can create an array of weapons that the user might have access to that includes a 'Katana' as an instance of a 'Sword' as well as a 'Dagger' and of course our 'Shotgun' as an instance of Gun
:
even though 'Dagger' and 'Katana' have a similar attack style, one would clearly deal more damage than the other, so Damage now becomes another property that we could add...
List<Weapon> weapons = new List<Weapon>
{
new Sword { Name = "Katana" },
new Sword { Name = "Dagger" },
new Gun { Name = "Shotgun" }
};
Although it doesn't make sense in a real-world situation to talk about the reload function on a Katana, you could for the purposes of simplifying the code say that all Weapon
instances do have a Reload
function, but the Reload
function on a Katana does nothing. The same could be said for SecondaryAttack, in that case these become properties on the Weapon
base class. The argument about if it should or shouldn't be an interface could swing either way depending on how you implement it in the rest of your code.
Another scenario to consider is What is an Attack? By making the attack method a void
response type, we're really saying that anything and everything could occur when the method is executed, we have not feedback and cannot determine if the attack succeeded, or if it even ocurred at all.
If Attack returned a value or better yet an object that defined the parameters of the attack, then we can start to tell some interesting stories! Take
On the third attack, user with Shotgun equipped was out of ammo and had to use it as a 'blunt force' melee weapon, that had less damage, but required the user to be within melee range...
Write out user stories that explain the values and states of the objects over time, this will help you to identify the structures that you need, then we can talk about better implementations ;)
enum
to define types like weapon type, attack type ..etc. Also, you need to concrete implementation of attack and weapon as both have different purpose and logic, so they need to be separate. Then, you can have a container which contains both likeWeaponAttack
to control the damage, timing ..etc. \$\endgroup\$