基于SOLID DIP原则的游戏实体接口复用方案咨询:通用实体基类设计是否合理?
StdEntity Class a Valid Approach? Great question—let’s break this down from both a SOLID compliance and practical maintenance perspective.
Your Initial Approach is Absolutely Valid
First off: yes, creating a StdEntity base class that implements the shared getATK(), setATK(), getDEF(), setDEF() logic, then having StdPlayer inherit from it and implement IPlayer, and StdMonster do the same with IMonster, is a totally reasonable and sound design. Here’s why it works well with your existing setup:
- Follows DRY Principle: You eliminate duplicate code for the core stats logic, so if you ever need to adjust how ATK/DEF are handled (e.g., add validation, tie them to a formula), you only have to modify one place (
StdEntity) instead of two. - Preserves DIP Compliance: Your commands like
atkPotion(IEntity),expPotion(IPlayer), etc., still depend on abstractions (the interfaces), not concrete classes.StdPlayerandStdMonsterare just implementations of those interfaces, so you’re not violating the dependency inversion principle at all. - Single Responsibility:
StdEntityhandles the core entity stats, whileStdPlayerfocuses solely on player-specific EXP logic, andStdMonsterhandles monster-specific element logic. This keeps each class focused on one job, aligning with the single responsibility principle.
Here’s what this implementation might look like in code:
// Common base class for shared stats logic public class StdEntity implements IEntity { protected int atk; protected int def; @Override public int getATK() { return atk; } @Override public void setATK(int atk) { this.atk = atk; // You could add validation here later without touching Player/Monster } @Override public int getDEF() { return def; } @Override public void setDEF(int def) { this.def = def; } } // Player implementation public class StdPlayer extends StdEntity implements IPlayer { private int exp; @Override public void setEXP(int exp) { this.exp = exp; } @Override public int getEXP() { return exp; } } // Monster implementation public class StdMonster extends StdEntity implements IMonster { private String element; @Override public void setElement(String element) { this.element = element; } @Override public String getElement() { return element; } }
A More Flexible Alternative: Composition Over Inheritance
While your inheritance approach works great for your current needs, if you anticipate future changes to your entity system (e.g., adding entities with different stat logic, or allowing dynamic stat changes), a composition-based approach might be more robust.
Instead of inheriting the stats logic, you’d encapsulate it in a separate component class, then have StdPlayer and StdMonster hold an instance of this component and delegate the IEntity method calls to it. This follows the "composition over inheritance" principle, which avoids potential issues with rigid inheritance hierarchies down the line.
Example of this approach:
// Reusable stats component public class BaseStats implements IEntity { private int atk; private int def; @Override public int getATK() { return atk; } @Override public void setATK(int atk) { this.atk = atk; } @Override public int getDEF() { return def; } @Override public void setDEF(int def) { this.def = def; } } // Player using composition public class StdPlayer implements IPlayer { private IEntity statsComponent; private int exp; public StdPlayer() { // Use the base stats by default this.statsComponent = new BaseStats(); } // Delegate IEntity methods to the component @Override public int getATK() { return statsComponent.getATK(); } @Override public void setATK(int atk) { statsComponent.setATK(atk); } @Override public int getDEF() { return statsComponent.getDEF(); } @Override public void setDEF(int def) { statsComponent.setDEF(def); } // Player-specific logic @Override public void setEXP(int exp) { this.exp = exp; } @Override public int getEXP() { return exp; } } // Monster using composition public class StdMonster implements IMonster { private IEntity statsComponent; private String element; public StdMonster() { this.statsComponent = new BaseStats(); } @Override public int getATK() { return statsComponent.getATK(); } @Override public void setATK(int atk) { statsComponent.setATK(atk); } @Override public int getDEF() { return statsComponent.getDEF(); } @Override public void setDEF(int def) { statsComponent.setDEF(def); } @Override public void setElement(String element) { this.element = element; } @Override public String getElement() { return element; } }
The biggest advantage here is flexibility: if you later need a BossMonster with a custom stat calculation (e.g., ATK scales with health), you can create a BossStats class that implements IEntity and swap it into BossMonster without modifying the base StdMonster or StdEntity code. This aligns with the open/closed principle (open for extension, closed for modification).
Final Recommendation
- Stick with inheritance if your stat logic is stable, and you don’t foresee needing multiple variations of ATK/DEF behavior anytime soon. It’s simple, clean, and fits your current requirements perfectly.
- Switch to composition if you expect your entity system to grow in complexity (e.g., adding unique stat types, dynamic stat modifiers). It’s a bit more code upfront but pays off in maintainability later.
Either way, both approaches keep your code compliant with the dependency inversion principle, since all your commands still depend on the abstract interfaces rather than concrete classes.
内容的提问来源于stack exchange,提问作者Juan Andres Flores Gutierrez

