This is a summary of the L in SOLID, the 5 core rules of OO.[Interfaces describe how a thing is to work, everything must be completely interchangeable. It IS-A thing. In all ways it is interchangeable with any other thing. For example, a 13/16 spark plug must be interchangeable with all other 13/16 spark plugs, even though they may vary in their implementation details.
Upon first reading these don't really give any practical advise as to how to fix your code... But it's worth keeping them in the back of your mind at all times so that eventually with experience you'll come to grok them.
There's lots of ways. Perhaps Potions don't need to act upon the Character interface at all; instead they could act upon an Attributecollection interface which the Character implements. This is the D in SOLID - remove dependencies between components by creating a common interface instead.If I hide the drinking of potions behind one method, how would I determine what attribute the potion was meant for? Health or Mana. Let's keep it simple, and say Health and Mana are int variables. How would I write my function to determine which attribute to update based on Potion, and then update it?
The potion could have a name (enum, int, string, etc) specifying which attribute it wants to act on. It could ask the AttributeCollection to retrieve an attribute with that name, or null if it doesn't exist,and then the potion can then apply a change to that attribute. This would also allow potions to be used on any other game object that implements the AttributeCollection interface and contains a "mana" attribute.
In your example, this is true but it's also true that the code that is controlling the character is also aware of these details... Which is a hint that the character abstraction is pretty weak.Shouldn't the character be aware of what weapon it's using? and who it's using it on?
If the character is fully aware of its weapon and its enemy, then the controlling code could just write:
character.attack(); // it knows which weapon and who