I've been told few days ago that using is
is a code smell and anti-pattern.
I used it in an game to filter some elements of a collection (the inventory of the player).
Classes structure
The base class of an element of the collection is Item
:
public abstract class Item
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
From that class are derived 3 classes, Potion
, Armor
and Weapon
A potion is IUsable
public interface IUsable
{
void Use(Character target);
}
And IStackable
public interface IStackable
{
int Quantity { get; set; }
int MaxQuantity { get; set; }
}
The class Potion
is, in example, defined that way :
public class Potion : Item, IUsable, IStackable
{
//Some properties and implementations of interfaces
}
The weapons and armors are IEquipable
public interface IEquipable
{
void TakeOn(Character target);
void TakeOff(Character target);
}
Here is a shortened example of Armor
public class Armor : Item, IEquipable
{
//Some properties and implementations of interfaces
}
And one for Weapon
public class Weapon : Item, IEquipable
{
//Some properties and implementations of interfaces
}
Collection access
Now, the part that smells, considering what was told to me :
To access some specific items in the collection, I'm using a generic method SpecificInventoryItems<T>()
, which contains this part to get the expected T
items :
foreach (Item item in player.Inventory)
{
if (item is T)
{
string stack = "";
if (item is IStackable)
{
var stackable = item as IStackable;
stack = "(" + stackable.Quantity + "/" + stackable.MaxAmount + ") ";
}
options += "[" + i++ + "] - " + item.Name + " " + stack + ": " + item.Description;
if (item is Armor)
options += " " + ArmorDetail(item as Armor);
else if (item is Weapon)
options += " " + WeaponDetail(item as Weapon);
options += "\n";
items.Add(item as T); //items is List<T>
}
}
What's wrong with it? How could (should) I redesign this structure ? I though using separates lists such as List<IUsable>
and so on, instead of just a List<Item> Inventory
Another example, after displayed the specific elements of the inventory, the user can select an item, and depending of its type, the item will be either used or taken on :
switch (selected) //selected type is T
{
case IEquipable eq:
eq.TakeOn(player);
break;
case IUsable us:
us.Use(player);
//Manage the deletion of the IUsable if not IStackable, or IStackable and quantity = 0
break;
default:
break;
}
}
Testable code
The code below can be copied/pasted in VS as is, to be used for testing purposes
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;
namespace SOTests
{
public abstract class Item
{
public int Id { get; set; }
public string Name { get; set; }
public string Description { get; set; }
}
public interface IUsable
{
void Use(Character target);
}
public class Character
{
public List<Item> Inventory { get; set; }
public Character()
{
Inventory = new List<Item>
{
new Potion
{
Id = 1,
Name = "Lesser HP potion",
Description = "Heals 42 HP",
HPRestored = 42,
MaxQuantity = 20,
Quantity = 10
},
new Potion
{
Id = 2,
Name = "Greater HP potion",
Description = "Heals 100 HP",
HPRestored = 100,
MaxQuantity = 20,
Quantity = 10
},
new Armor
{
Id = 3,
Name = "Lesser Armor",
Description = "A basic armor",
Defense = 10
},
new Armor
{
Id = 4,
Name = "Regular Armor",
Description = "An armor",
Defense = 25
},
new Armor
{
Id = 5,
Name = "Superbe Armor",
Description = "A nice shiny armor",
Defense = 50
},
new Weapon
{
Id = 42,
Name = "Lesser Sword",
Description = "A basic sword",
Attack = 50
},
new Weapon
{
Id = 43,
Name = "Regular Sword",
Description = "A sword",
Attack = 75
},
new Weapon
{
Id = 44,
Name = "Superbe Sword",
Description = "A nice shiny sword",
Attack = 100
}
};
}
}
public class Potion : Item, IUsable, IStackable
{
public int HPRestored { get; set; }
public int Quantity { get; set; }
public int MaxQuantity { get; set; }
//This is not exactly how I implemented it, but it's sufficient for testing purpose
public void Use(Character target)
{
--Quantity;
Console.WriteLine("Used " + Name);
}
}
public interface IStackable
{
int Quantity { get; set; }
int MaxQuantity { get; set; }
}
public interface IEquipable
{
void TakeOn(Character target);
void TakeOff(Character target);
}
public class Armor : Item, IEquipable
{
public int Defense { get; set; }
//This is not how I implemented it, but it's sufficient for testing purpose
public void TakeOn(Character target)
{
Console.WriteLine("Wearing armor : " + Name);
}
public void TakeOff(Character target) { }
}
public class Weapon : Item, IEquipable
{
public int Attack { get; set; }
//This is not how I implemented it, but it's sufficient for testing purpose
public void TakeOn(Character target)
{
Console.WriteLine("Wearing weapon : " + Name);
}
public void TakeOff(Character target) { }
}
public class AbsentFromInventory : Item
{
}
class Program
{
static Character player = new Character();
static string ArmorDetail(Armor armor)
{
return (armor.Name + " : " + armor.Description + " -> Defense : " + armor.Defense);
}
static string WeaponDetail(Weapon weapon)
{
return (weapon.Name + " : " + weapon.Description + " -> Attack : " + weapon.Attack);
}
/// <summary>
/// Displays some elements of the inventory depending of its type
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="type">This is only for friendly display purpose</param>
static void SpecificInventoryItems<T>(string type) where T : class
{
string error = "";
string input;
List<T> items;
bool back = false;
do
{
do
{
items = null;
items = new List<T>();
string options = type + " :\n";
int i = 1;
if (player.Inventory.Count > 0)
{
foreach (Item item in player.Inventory)
{
if (item is T)
{
string stack = "";
if (item is IStackable)
{
var stackable = item as IStackable;
stack = "(" + stackable.Quantity + "/" + stackable.MaxQuantity + ") ";
}
options += "[" + i++ + "] - " + item.Name + " " + stack + ": " + item.Description;
if (item is Armor)
options += " " + ArmorDetail(item as Armor);
else if (item is Weapon)
options += " " + WeaponDetail(item as Weapon);
options += "\n";
items.Add(item as T);
}
}
if (i > 1)
{
Console.WriteLine(options);
if (error != "")
Console.WriteLine(error);
Console.WriteLine("Please select an item or go [[B]ack]");
}
else
Console.WriteLine("No " + type.ToLower() + " found. Please go [[B]ack]");
}
else
Console.WriteLine("No item in inventory. Please go [[B]ack]");
input = Console.ReadLine();
error = "";
} while (!Regex.IsMatch(input, "^(([0-9]+)|(b(ack)?))$", RegexOptions.IgnoreCase));
if (int.TryParse(input, out int result))
{
if (result > 0 && result <= items.Count)
{
var selected = items[--result];
switch (selected)
{
case IEquipable eq:
eq.TakeOn(player);
back = true;
break;
case IUsable us:
us.Use(player);
if (us is IStackable)
{
var stackable = us as IStackable;
if (stackable.Quantity <= 0)
player.Inventory.Remove(stackable as Item);
}
else
player.Inventory.Remove(us as Item);
back = true;
break;
default:
break;
}
}
else
error = "Invalid number.";
}
else
back = true;
} while (!back);
}
static void Main(string[] args)
{
do
{
SpecificInventoryItems<Potion>("Potion");
SpecificInventoryItems<Weapon>("Weapon");
SpecificInventoryItems<Armor>("Armor");
SpecificInventoryItems<AbsentFromInventory>("Super item");
Console.WriteLine("Again? Y/N");
} while (Console.ReadKey().Key != ConsoleKey.N);
}
}
}
SpecificInventoryItems<T>()
? \$\endgroup\$items.Add(item as T); //items is List<T>
, this one isn't defined anywhere. It'd be great if you could make it copy/pasteable. \$\endgroup\$is
is NOT a code smell OR an anti-pattern. Whoever told you that needs to leave the industry. \$\endgroup\$