【问题标题】:How can I avoid constantly having to change an interface when adding new features to a system?在向系统添加新功能时,如何避免不断更改界面?
【发布时间】:2019-02-11 05:51:54
【问题描述】:

在我的工作中,我正在尝试创建更多模块化系统,因为我们倾向于在游戏中使用类似的机制,但差异很小。为此,我一直在使用接口,但一直被某些问题困扰,尤其是与添加小功能有关的问题。

示例:

以我们的进化系统为例。我创建了 IEvolvable 接口,它有一个进化级别的属性和一个 Evolve() 方法。

public interface IEvolvable
{
    int evolution { get; }

    bool IncreaseEvolution(int numEvolutions);
}

然后我在 Character 类上实现了这个接口,并且通过我的 Evolution 处理类基于某些条件,我想进化我的角色。

public class EvolutionHandler
{
    public IEvolvable evolvable;

    public void TryEvolveCharacter
    {
        if(someCondition)
        {
            evolvable.IncreaseEvolution(1);
        }
    }
}

然后,稍后我们会说,我们希望角色根据等级进化!极好的。我们有一个 ILevellable 接口,其中包含 Level、xp 等。

public interface ILevellable
{
    int Level{ get; }
    int MaxLevel{get;}
    int XP {get;}
    bool LevelUp(int numLevels);
}

我们可以根据级别的变化使用这些数据来控制进化发生的时间。但这是我的问题:

我的进化处理程序类接口与 IEvolvable...不是 ILevellable...那我该怎么办?

我可以让 IEvolvable 扩展 ILevellable,反之亦然...或者我可以创建一个扩展 IEvolvable 和 ILevellable 的新接口。现在我还必须修改我的进化处理程序以适应这些变化。

但是,如果我们不想让进化处理程序在我们的新游戏中再考虑关卡,会发生什么?使用旧代码吗?我是否应该扩展我的旧代码以包含 Ilevellable 接口?

public interface ILevelEvolver : ILevellable, IEvolvable
{
}

public class EvolutionHandler2
{
     public ILevelEvolver levelEvolvable;

    public void TryEvolveCharacter
    {
        if(levelEvolvable.Level > 10)
        {
            evolvable.IncreaseEvolution(1);
        }
    }
}

【问题讨论】:

  • 每个 ILevelable、IEvolveable 都可以吗?我猜有些 ILevelable 不应该进化?
  • @EmrahSüngü 在我们制作的某些游戏中,Ilevelables 并不总是可以进化的。
  • 你能告诉我IEvolveableint evolution { get; }属性中的含义
  • @EmrahSüngü 所以一个可进化的可能有许多进化,这个数字将代表当前的进化。
  • Eric Lippert 写了一组很棒的帖子,标题为 Wizards and Warriors(链接到第 1 部分)。本系列的结论是 C# 的规则不太可能与您的游戏规则相匹配。因此,另一种方法是构建一个 rules 系统,您可以在其中实际建模您的规则,但不要尝试将其绑定到 C# 的类型系统。

标签: c# interface architecture


【解决方案1】:

在许多情况下,拥有一个包含对某些实现有意义但对其他实现没有意义的成员的接口可能比尝试为不同的功能组合使用不同的接口更好的模式。举个简单的例子,如果 Java 或 .NET 在它们的基本可枚举接口中包含了一个报告计数(如果可用)的函数,以及一个指示是否以及如何执行计数的函数,那么连接两个枚举的包装类可以有效地如果组成枚举支持计数函数,则报告组合枚举中有多少元素,并且还可以让客户端知道它的计数函数是否有效和/或可缓存。

另一种有用的模式是让接口包含asXX 函数,类可以将其实现为返回对自身的引用(如果它支持 XX 功能)或构造合适类型的包装器对象。如果 XX 是包装类类型,则可以将功能添加到包装类中,而无需更改包含 asXX 成员或其实现的接口。

【讨论】:

    【解决方案2】:

    接口不应相互扩展。让他们分开。此外,您应该将概念分开。这样,EvolutionHandler 应该只接受IEvolable。 在TryEvolveCharacter 方法中,您可以检查属性是否为ILevelable。 看一下代码:

    class EvolutionHandler
    {
        public IEvolable Evolable { get; set; }
    
        public void TryEvolveCharacter()
        {
            if (Evolable is ILevelable levelable && levelable.Level > 10)
            {
                Evolable.IncreaseEvolution(1);
            }
            else if (someCondition)
            {
                Evolable.IncreaseEvolution(1);
            }
        }
    }
    

    所以将来,如果一个字符扩展ILevelable,将考虑该级别,如果没有,someCondition发生。

    【讨论】:

      【解决方案3】:

      一旦您遇到这些类型的问题,我认为 OOP 有局限性,或者说它使某些事情变得过于简单,就会变得很明显。这并不意味着它应该被完全废弃并采用其他东西,我们仍然可以使用它来做很多事情。如果不是使用接口,而是直接进行有意义的更改,而是传递一个服务接口,该接口充当内部接口的适配器。

      public interface IEvolutionService {
          TryEvolveCharacter(IEvolvable evolvable); 
      }
      

      具体的实现可以有类似

      public void TryEvolveCharacter(IEvolvable evolvable){
              if (evolvable.Level > 10) {
                  evolvable.IncreaseEvolution(1);
                  ..Maybe do something new that the IEvolvable just exposed but without changing our consumed interface!
              }
      }
      

      它确实添加了代码和东西来制作这些,但你也有选择,一个服务可以代表多个接口,但是你违反了 SOLID 中的单一责任原则,基本上只是让事情变得比他们更复杂应该努力使它不那么复杂。

      您可以将此方法设为静态类,尽管这会干扰可测试性,所以我会说重构并添加一个新服务来处理诸如 service.TryEvolveCharacter(someIEvolvable) 之类的事情。您仍然需要在面向公众的服务上维护接口,但这可能比前面没有任何抽象的原始接口更易于管理。

      我给出的答案尽可能接近您的问题,但对我来说仍然不太理想。我会考虑为数据使用不可变的结构(可以有接口,也可以使用 L2 CPU 缓存)并将它们传递给服务(这将是纯函数,也就是说无状态,它们只处理传入的内容)。如果您正在编写游戏代码并且性能是一个问题,那么这将非常有用。 如果您只是将游戏用作隐喻,那么可能会少一些:) A helpful article on structs, L2, and performance

      【讨论】:

        【解决方案4】:

        关键词是:

        • 区分不同的和保持不变的
        • SOLID 原则之一:对扩展开放,对修改关闭

        最后在你的情况下会使用 Strategy 模式:

        public interface IEvilutionChecker{
        
            bool AllowEvolution();
        }
        
        
        public class EvolutionCheckerA : IEvilutionChecker{
            private ILevellable levelEvolvable;
            public EvolutionCheckerA(ILevellable levelEvolvable){
                this.levelEvolvable = levelEvolvable;
            }
            public bool AllowEvolution(){
                return levelEvolvable.Level > 10;
            }
        }
        
        public class EvolutionCheckerB : IEvilutionChecker{
            private IEvolvable evolvable;
            public EvolutionCheckerB(IEvolvable evolvable){
                this.evolvable = evolvable;
            }
            public bool AllowEvolution(){
                return someCondition;
            }
        }
        
        public class EvolutionHandler2
        {
            public IEvolvable evolvable; 
            public IEvilutionChecker EvolutionChecker {get;set;};
        
            public void TryEvolveCharacter
            {
                if(EvolutionChecker.AllowEvolution())  
                {
                    evolvable.IncreaseEvolution(1);
                }
            }
        }
        

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 2011-07-17
          • 2021-11-24
          • 1970-01-01
          • 2012-04-04
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多