【问题标题】:Limitations in subclasses OOP子类 OOP 的限制
【发布时间】:2013-06-09 10:54:03
【问题描述】:

在java中,围绕子类进行设计以减少超类功能的好方法是什么?

例如,考虑具有“See”功能的“Man”类和不应该具有该功能但应该具有“Man”具有的所有其他功能的子类“BlindMan”。

我能想到的唯一解决方案是拥有一个抽象类“Man”,以及两个子类“SeeingMan”和“BlindMan”,其中 SeeinMan 添加了一个函数“See”。

然而,这个解决方案的问题是,如果我现在想添加一个“DeafMan”类 - 它扩展了什么?见人?如果那个人又聋又瞎怎么办?

【问题讨论】:

  • 您当然可以将BlindDeafMan 作为基类。在这个例子中不是很优雅。另一种(丑陋的)方法是在 BlindMan.see 中抛出 UnsupportedOperationException (就像某些迭代器对删除操作所做的那样)。最后一个(可能也是最好的)选择是让一个人引用一个Eyes 类。然后你可以做man.setEyes(new BlindEyes())
  • 你尝试过与接口结合吗?
  • 您可以创建 Man 的其他子类为 SingleHandicapMan 和 DoubleHandicapMan,如果 DeafMan 和 BlindMan 都出现,则将它们扩展为 DoubleHandicapMan,如果不将它们扩展为 SingleHandicapMan
  • 另见经典的Rectangle-Square继承难题。
  • 我说的是Rectangle-Square吗?显然我的意思是circle-ellipse problem

标签: java oop


【解决方案1】:

如果您不希望调用该函数,则可以抛出异常。

例如在您的场景中:InablityException

像这样:

public interface Man {

    public Object see() throws InabilityException;
    public Object hear() throws InabilityException;

}

public class BlindMan implements Man {
    public Object see() throws InabilityException {
       throw new InabilityException("blind people don't see");
    }

    public Object hear() {
       return "what I hear";
    }
}

对于聋人来说,反之亦然。

如果 Man 是 class 而不是 interface,则同样有效。

如果hasWorkingEyes() 返回 false,您可以(并且应该)将此与 @Pataschu 答案 https://stackoverflow.com/a/17080626/881272throw Exception 结合使用。

【讨论】:

    【解决方案2】:

    在 Man 类中添加 hasEyes,默认情况下为 true,在 BlindMan 中默认设置为 false(或者让盲人实例化 Man 之后立即被抢眼,例如 BlindManFactory)。然后在 Man 的 See 中编码,这样如果他没有眼睛,它就会自动失败。

    【讨论】:

    • 他们都有眼睛。问题在于行为,而不是结构。
    • @bmartins 并非所有盲人都有眼睛。
    【解决方案3】:

    我认为在这种情况下你应该使用组合而不是继承,或者拥有组成人类的各种子类。

    虽然我确实理解你的逻辑,但 BaseClass 是一种契约,它保证该类型的所有类都应该遵守这种行为,拥有一个删除父方法的子类是一个很大的 NO NO..

    虽然您可以抛出各种异常,但我根本不会走这条路。 这样想,假设我是一个只需要访问 Human 对象的开发人员,我期望某种行为,突然我调用了一个接口方法并 get..an Exception 只是因为我调用了它??您不应该知道派生类的实现以及何时可以或不能调用它们。

    以下是一些解决方案:

    让 Human 由 BasicHumanFunctions、VisionSystem 等组成。那么盲人将只有其中的几个。

    class Human {
      private BasicHumanFunctions _basicFunctions; //contains breathe funcitonality.
      private VisionSystem _vision; //contains see
    }
    
    class BlindMan {
       private BasicHumanFunctions _basicFunctions;
    }
    

    让 Human 基类只包含所有人类都希望呼吸等的相同行为,然后创建一个 HealthyHuman 和一个 BlindHuman 等,每个都创建自己的方法。 然后,您可以根据需要进一步使用 HealthHuman 和子类。

    class Human {
       void breathe() {};
       // other Human basic functions;
    } 
    
    class HealthyHuman extends Human {
       void see() {};
       //other healthy human functions
    }
    
    class BlindHuman extends Human {
      void useCane() {};
    }
    

    对于第二种情况,您仍然可以使用组合来分享行为:

    class BlindHuman extends Human {
       private VoiceSubsystem _voice = new VoiceSybsystem();
       void speaker() {  _voice.speaker();}
    }
    

    【讨论】:

    • 当异常是 RuntimeException 并因此意外时,我认为它只是一个 NO NO。如果接口声明了异常,您无论如何都必须处理它。 (因为即使是看到的人也会在这一刻失明)
    • 我宁愿创建一个没有该方法的对象,而不是抛出一个异常,这在某种程度上表明他应该知道哪个派生类实现了它......盲人没有远见,它不是他有引发异常的远见:) IMO 您不想更改基本合同..如果您需要不同的合同,这意味着您可能需要另一种方式来处理它。不是说我的是最好的,还有其他选择..但我永远不会让基本方法突然不起作用..
    • 那么你会做一个三级的 BlindAndDeafHuman 来将两者结合起来吗?如果你需要一组失聪的人怎么办?您的 List 将不包括聋哑人和盲人,除非 BlindAndDeafHuman 扩展了 DeafHuman,这将我们带到了方格 1
    • 我喜欢这个组合,但问题(从所问的)更多是行为而不是结构。您编写类的属性和特性,但从客户的角度(类的用户),您必须处理具体实例,错过每个人 TYPE(盲人或健康人)的行为;我建议添加接口,明确定义每种类型的人能够执行(或不执行)什么。
    • @indieman 我明白你在说什么,我相信添加接口会解决这个问题,这是在 Java 中执行多继承之类的方法。
    【解决方案4】:

    你可以使用布尔值

    例如:isSeeing()

    在你的函数中,你可以在 if else 语句中使用它

    【讨论】:

    • 多么盲目的建议^_^
    • 积极思考:isSeeing() 比 isBlind() 好 -> 适用于所有消极的事物:使用积极的形式来避免 if(!isNegative()) 之类的结构 -> if(isPositive()) 更容易理解和维护。
    • 这就是整个问题......你不希望 SeeingMan 扩展 BlindMan(因为能看见的人不是能看见的盲人)并且你不希望 Man 缺乏功能看到
    【解决方案5】:

    接口

    我将通过接口定义这两种行为(BlindMan 和 SeeingMan),只公开定义类型的调用(BlindMan 和 SeeingMan)。您不应该真正关心下面的实际实例类型是什么。

    抽象父类

    现在,您可以将这些接口与具有所有默认行为的“父”抽象类结合起来(盲和看见;如果您也选择的话)

    两者结合

    public interface BlindMan {
             public void useBlindStick();
        }
    
       public interface SeeingMan {
            public void see();
       }
    
    
      public abstract class Man {
           public void useBlindStick() {
               //work
           }
    
           public void see() {
               //work
           }
      }
    
    
      public class SomeBlindMan extends Man implements BlindMan {
            //bla bla
      }
    
    
     public class SeeingMan extends Man implements SeeingMan {
           //bla bla
     }
    

    希望对你有帮助

    【讨论】:

      【解决方案6】:

      寻找接口

      为具有公共属性的CommonMan定义接口,并为具有特定方法的单独子类使用单独的接口。然后您可以获得两个接口方法。

      示例:

      public class SpecificMan extends CommonMan implements specifiManInter
      
      {
      
      //methods of CommonMan
      
      //Methods of SpecificMan 
      
      }
      

      CommonMan 类:

      public class CommonMan implements CommonManInter {
      
       //methods of CommonMan
      
      }
      

      【讨论】:

        【解决方案7】:

        这样的事情怎么样:

         public class Man {
        
            Ears ears;
        
            Eyes eyes;
        
            }
        
            public class Eyes {
        
            public boolean canSee()
        
            }
        
            public class Ears {
        
            public boolean canListen()
            }
        

        那么逻辑就是……

        if(man.getEyes().canSee()){ //not blind
        
        }
        else
        {
        //blind logic
        }
        
        if(man.getEars().canListen())
        {
        //Not Deaf
        }
        

        【讨论】:

          【解决方案8】:

          最重要的原则,是Lyskov的替换原则。

          一旦获得并应用它,您就可以随心所欲地构建模型。

          例如,您可以说盲人仍然可以看到,只是他只能看到黑色(或灰色)的图片。还有一个问题是,如果图片模糊或不完整,您希望合法盲人仍能看到某些东西。

          你也可以说对象可以做的动作是看不见。我们要做的就是观察。观看是观看的(成功)结果。即使有工作的眼睛,你也可能看不到任何东西!盲人仍然可以通过聆听(因此听力,并在心理上构建“图像”)来“看到”没有眼睛或没有功能的眼睛的东西。

          所以,忘掉“看”和“听”吧。使用观察和聆听。并且对此没有任何期望。 (广泛的先决条件)。

          【讨论】:

            【解决方案9】:

            在java中,围绕子类进行设计以减少超类功能的好方法是什么?

            这基本上是在问“继承如何减少功能?”。我能想到的 Java 中的一个例子是 Collections#unmodifyableList 方法,它说:

            返回指定列表的不可修改视图。此方法允许模块为用户提供对内部列表的“只读”访问权限。对返回列表的查询操作“通读”到指定列表,并尝试修改返回列表,无论是直接还是通过其迭代器,都会导致 UnsupportedOperationException

            所以当我想要删除某个功能时,我会这样做。但是,请确保它的用途是明确的,例如在BlindMan 的Javadoc 中声明see() 将导致异常。

            然而,如果你想“减少”一个以上的特性,也许是独立的,并随着时间改变它们,我宁愿使用变量而不是继承。最后,您没有扩展超类,那么为什么要为此滥用继承呢?你所拥有的是一个具有不同属性的人。

            需要记录方法的行为。我可以为您的用例考虑以下类:

            public class Human {
                private boolean seeing;
            
                public void setSeeing(boolean seeing) { ...}
                public boolean isSeeing() { ... }
            
                /**
                 *  Returns what the human sees at the moment, or null
                 *  if he/she can't see (seeing is false).
                 */
                public String see() {
                    if(seeing){
                        return "I can see a tree!";
                    }else{
                        return null;
                    }
                }
            
                // same goes for hearing
            }
            

            【讨论】:

              【解决方案10】:

              让人类按照他的方式做事,给他任务,而不是命令。

              视觉是某些活动的一部分,所以让他独立使用(或不使用)视觉。

              例如告诉他去某个地方旅行。健康的人会使用视觉,盲人会使用其他感官并且效率会降低(除非它处于完全黑暗中)。

              如果特定任务需要视力(例如守卫塔上的警卫),您需要询问他们的视力有多好(没有视力不佳的盲人)。

              有时你想问他们看到了什么(而不在乎他们听到什么等),然后问他们并期望盲人(和其他视障者)什么都看不见。

              如果您希望人类成为被动对象并由其他对象处理它们,一种尚未提及的方法是: 查看游戏编程中使用的实体组件编程。它基本上是以通用方式使用组合。实体仅保存身份,组件保存有关实体某些方面的数据(例如,视力可能是屈光度数或更有用的统计信息),系统处理具有给定系统所需的组件组合的实体集。 Sight 系统可能需要Sight 组件并创建包含当前实体现在看到的实体的组件 seenEntities。 其他系统可以合并所有感官的结果并使用它来执行一些有用的事情。

              【讨论】:

                【解决方案11】:

                例如,考虑类“Man”,它具有函数“See”和 不应该具有该功能的子类“BlindMan”

                你没有完全正确。盲人有“看”的功能,它什么都不做(因为传感器和适当的大脑部分之间没有联系,或者传感器没有工作(返回空的像素列表或 0 大小的图像)。

                一个很好的类比:Writer 具有函数 flush(),但是是什么使 StringWriter 具有该函数?

                /**
                 * Flush the stream.
                 */
                public void flush() { 
                }
                

                嗯,没什么。没有什么可以冲洗的。好的,但为什么 close() 会抛出 IOException

                public void close() throws IOException {
                }
                

                它并不完美,但该代码适用于所有类型的作者,即使是那些与 flush 不相关的作者。

                但是等等,即使是看到 man 函数 see() 也可能返回 null 或空列表。例如,当它太暗时。只需检查 null/empty 值,代码就可以在该设计中正常工作。

                【讨论】:

                  【解决方案12】:

                  Dory 认为组合优于继承是正确的。您的问题/问题与飞鱼问题几乎相同。在这个例子中,我们应该如何处理一个会飞游泳的生物?

                  将 FlyingFish 放入继承层次结构中会很尴尬。与其试图做到这一点,不如利用组合的力量:

                  现在,每个具体类都可以指定它们的行为。根据信息专家的原则,这样做是合适的:Information Expert will lead to placing the responsibility on the class with the most information required to fulfill it

                  注意:从上面的截图中很难看出好处,所以这里再次显示:

                  好处
                  - 支持对象组合而不是继承
                  - 强制开发人员在编码时对行为做出有意识的决定(而不是在开发人员必须记住覆盖的基类中创建默认实现)。
                  - 通过抽象不同的行为,它现在可供其他客户端使用。
                  - 更松散的耦合意味着更容易遵守开放/封闭原则,即代码应该对扩展开放但对修改关闭。

                  【讨论】:

                  • 飞鱼绝对不是鸟。听不见的人是聋子,看不见的人是瞎子,听不见、看不到的人怎么办?您是否将他们定义为盲人并给他们一个 HearNoWay?如果我是一个遇到这种结构的程序员,我会假设我可以收集所有在 ArrayList 中看不到的人,但事实并非如此……你必须有 ArrayList 将包含 BlindMan 和 BlindDeafMan 对象。
                  • 我猜这只是对 OOP 和一般分类的警告
                  • 使用合成,比如我的例子,是什么阻止你列出盲人名单?
                  【解决方案13】:

                  要实现一项功能,请使用接口

                  要扩展或重新定义功能,请通过扩展基类来使用子类。

                  如果您想在运行时动态添加行为,请实现Decorator 模式。

                  相关 SE 帖子:

                  Implements vs extends: When to use? What's the difference?

                  When to Use the Decorator Pattern?

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2016-03-12
                    • 1970-01-01
                    • 2015-04-26
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多