勿让儿童接触:从继承中删除受保护的字段

时间:2014-01-31 14:39:46

标签: java inheritance encapsulation

本着精心设计的OO的精神,我所延伸的某个类别已经标记了其中一个受保护的字段。这堂课也慷慨地提供了一个公共的二传手,但没有吸气剂。

我正在使用一个基类扩展这个类,而基类又由几个孩子扩展。如何限制对我的孩子的受保护变量的访问,同时仍然能够私下操纵它并将其公开设置?

见下面的例子:

public abstract class ThirdPartyClass {
  protected Map propertyMap;

  public void setPropertyMap(Map propertyMap){
    this.propertyMap= propertyMap;
  }

  // Other methods that use propertyMap.
}

public abstract class MyBaseClass extends ThirdPartyClass{
// Accessor methods for entries in propertyMap.
  public getFoo(){
    propertyMap.get("Foo");
  }

  public getBar(){
    propertyMap.get("Bar");
  }

 // etc...
}

public class OneOfManyChildren extends MyBaseClass {
// Should only access propertyMap via methods in MyBaseClass.
}

我已经发现我可以通过private final中的字段MyBaseClass撤消访问权限。然而,这也妨碍了使用超级类提供的setter。

我能够通过下面的“聪明”来规避这种限制,但它也会导致维护同一地图的两个副本以及复制每个元素的O(n)操作。

public abstract class MyBaseClass extends ThirdPartyClass{

  private final Map propertyMap = new HashMap(); // Revokes access for children.

  /** Sets parent & grandparent maps. */
  @Override
  public final void setPropertyMap(Map propertyMap){
    super.setPropertyMap(propertyMap);
    this.propertyMap.clear();
    this.propertyMap.putAll(propertyMap);
  }
}

有没有更好的方法来实现这个目标?

注意:这只是真实问题的一个示例:如何在不保留多个副本的情况下限制对受保护字段的访问?

注意:我也知道,如果首先使用private访问者创建protected字段,则这不是问题。可悲的是,我无法控制它。

注意:需要IS-A关系(继承)。

注意:这可以轻松应用于任何Collection,DTO或复杂对象。

对那些误解问题的隐喻:

这类似于一个祖父母有一个饼干罐,他们可以让所有家庭成员和他们家中的任何其他人(受保护的)访问。有孩子的父母进入房子,并且由于他们自己的原因,他们希望防止他们的孩子在恶心的情况下进入饼干罐。相反,孩子应该向父母询问巧克力饼干并看到它神奇地出现;同样用于糖饼干或奥利奥。他们永远不需要知道饼干都存放在同一个罐子里,或者甚至还有一个罐子(黑盒子)。如果罐子属于父母,如果祖父母可以被说服收起饼干,或者如果祖父母本身不需要访问,那么这可以很容易地实现。如果没有创建和维护两个完全相同的罐子,那么如何对儿童进行限制,同时又不受父母和儿童的限制。祖父母?

8 个答案:

答案 0 :(得分:6)

这对您来说可能是不可能的,但如果您可以从ThirdPartyClass派生一个接口并让ThirdPartyClass实现它?

然后通过委托私有成员ThirdPartyClassImpl来实现接口,让MyBaseClass充当装饰器。

即。

public interface ThirdParty ...


public class ThirdPartyClass implements ThirdParty



public class MyBaseClass implements ThirdParty {

    private ThirdParty decorated = new ThirdPartyClass();



 public class SubclassOne extends MyBaseClass....

答案 1 :(得分:2)

好的,作弊模式: 如何覆盖de public setter并将map实现更改为MyBaseClass的内部类。这个实现可能会在你不希望你的孩子访问的所有map方法上抛出异常,你的MyBaseClass可以通过使用你的map实现的内部方法来公开他们应该使用的方法... 仍然必须解决ThirdPartyMethod如何访问这些属性,但你可以强制你的代码在使用它之前调用MyBaseClass上的finalizationMethod ...我只是在这里分开

修改

喜欢这个:

public abstract class MyBaseClass extends ThirdPartyClass{

    private class InnerMapImpl implements Map{
       ... Throw exception for all Map methods you dont want children to use

       private Object internalGet(K key){
           return delegate.get(key);
       }
    }

    public void setPropertyMap(Map propertyMap){
        this.propertyMap= new InnerMapImpl(propertyMap);
    }

    public Object getFoo(){
        return ((InnerMapImpl) propertyMap).internalGet("Foo");
    }


}

答案 2 :(得分:1)

可悲的是,你无能为力。如果此字段为protected,则它是有意识的设计决策(一个错误的IMO)或错误。无论哪种方式,你现在都无法做到这一点,因为你无法减少字段的可访问性。

  

我已经发现我可以通过在MyBaseClass中创建字段private final来撤销访问权限。

这不完全正确。你在做什么叫做变量隐藏。由于您在子类中使用相同的变量名,因此对propertyMap变量的引用现在指向MyBaseClass中的私有变量。但是,您可以非常轻松地隐藏此变量,如下面的代码所示:

public class A
    {
    protected String value = "A";

    public String getValue ()
        {
        return value;
        }
    }

public class B extends A
    {
    private String value = "B";
    }

public class C extends B
    {
    public C ()
        {
        // super.value = "C"; --> This isn't allowed, as B.value is private; however the next line works
        ((A)this).value = "C";
        }
    }

public class TestClass
    {
    public static void main (String[] args)
        {
        A a = new A ();
        B b = new B ();
        C c = new C ();

        System.out.println (new A ().getValue ()); // Prints "A"
        System.out.println (new B ().getValue ()); // Prints "A"
        System.out.println (new C ().getValue ()); // Prints "C"
        }
    }

因此,您无法“撤销”对超类ThirdPartyClass中受保护类成员的访问权限。你没有很多选择:

  • 如果您的孩子班级不需要知道MyBaseClass以上的班级层次结构(即他们根本不会参考ThirdPartyClass),如果您不需要他们要成为ThirdPartyClass的子类,您可以使MyBaseClass成为不从ThirdPartyClass延伸的类。相反,MyBaseClass将保存ThirdPartyClass的实例,并委托对此对象的所有调用。通过这种方式,您可以控制实际向您的子类公开的ThirdPartyClass API的哪一部分。

    public class MyBaseClass
        {
        private ThirdPartyClass myclass = new ThirdPartyClass ();
    
        public void setPropertyMap (Map<?,?> propertyMap)
            {
            myclass.setPropertyMap (propertyMap);
            }
        }
    

    如果您需要从propertyMap直接访问ThirdPartyClass MyBaseClass成员,那么您可以定义私有内部类并使用它来访问该成员:

    public class MyBaseClass
        {
        private MyClass myclass = new MyClass ();
    
        public void setPropertyMap (Map<?,?> propertyMap)
            {
            myclass.setPropertyMap (propertyMap);
            }
    
        private static class MyClass extends ThirdPartyClass
            {
            private Map<?,?> getPropertyMap ()
                {
                return propertyMap;
                }
            }
        }
    
  • 如果第一个解决方案不适用于您的案例,那么您应该准确记录MyBaseClass的哪些子类可以做什么,以及他们不应该做什么,并希望他们尊重所描述的合同你的文件。

答案 3 :(得分:1)

  

我能够通过下面的“聪明”来规避这种限制,但它也会导致维护同一地图的两个副本以及复制每个元素的O(n)操作。

Laf已经指出,通过将子类转换为第三方类可以很容易地避免这种解决方案。但是,如果这对您来说没问题,并且您只想在不保留地图的两个副本的情况下隐藏子类的受保护父地图,您可以尝试这样做:

public abstract class MyBaseClass extends ThirdPartyClass{

    private Map privateMap;

    public Object getFoo(){
            return privateMap.get("Foo");
    }

    public Object getBar(){
            return privateMap.get("Bar");
    }

    @Override
    public final void setPropertyMap(Map propertyMap) {
            super.setPropertyMap(this.privateMap =propertyMap);
    }
}

另请注意,如果父母地图受到保护,那并不重要。如果真的想通过子类访问此字段,可以始终使用反射来访问该字段:

public class OneOfManyChildren extends MyBaseClass {
    public void clearThePrivateMap() {
        Map propertyMap;
        try {
            Field field =ThirdPartyClass.class.getDeclaredField("privateMap");
            field.setAccessible(true);
            propertyMap = (Map) field.get(this);
        } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) {
            e.printStackTrace();
            return;
        }
        propertyMap.clear();
    }
}

所以它实际上归结为这个问题,为什么你希​​望子类不能访问该字段:

1)是否只是为了方便,所以很快就会明白你的api应该如何使用? - 那么简单地隐藏子类中的字段就好了。

2)是否因为安全原因?那么你一定要搜索另一个解决方案并使用一个特殊的SecurityManager,它也禁止通过反射来访问私有字段......

那也许你可以尝试另一种设计:不要扩展第三方类,保留这个类的最终内部实例,并提供对内部类的公共访问,如下所示:

public abstract class MyBaseClass {
    private Map privateMap;
    private final ThirdPartyClass thirdPartyClass = new ThirdPartyClass(){
        public void setPropertyMap(Map propertyMap) {
            super.setPropertyMap(MyBaseClass.this.privateMap = propertyMap);
        };
    };

    public Object getFoo(){
        return privateMap.get("Foo");
    }

    public Object getBar(){
        return privateMap.get("Bar");
    }

    public void setPropertyMap(Map propertyMap) {
        thirdPartyClass.setPropertyMap(propertyMap);
    }

    public final ThirdPartyClass asThirdPartyClass(){
        return this.thirdPartyClass;
    }

}

然后,只要您需要使用第三方类的实例访问第三方库,就可以执行以下操作:

OneOfManyChildren child;
thirdpartyLibrary.methodThatRequiresThirdPartyClass(child.asThirdPartyClass());

答案 4 :(得分:0)

  

如何限制从我的孩子访问受保护变量,同时仍能私下操作并公开设置?

所以你希望公众拥有比你更多的权利?你不能这样做,因为他们总是可以只调用公共方法......它是公开的。

答案 5 :(得分:0)

如何创建另一个名为propertyMap的受保护变量?如果您的孩子班级,那应该是阴影。您也可以实现它,以便调用它上面的任何方法都会导致异常。

但是,由于访问器方法是在基类中定义的,因此它们不会看到您的第二个阴影版本并仍然适当地设置它。

答案 6 :(得分:0)

对变量的可见性就像方法的可见性一样,您无法降低该可见性。请记住,受保护变量在直接子类之外可见。它可以通过包See this Answer for Details

的其他成员从父级访问

理想的解决方案是混淆父级别课程。您已经提到将对象设为私有是非启动的,但如果您有权访问该类但却无法缩减(可能是由于现有依赖关系),您可以通过抽象出与方法的公共接口来摆弄类结构,并且让ThirdPartyClass和BaseClass都使用此接口。或者你可以让你的祖父母班级有两个地图,内部和外部,它们指向相同的地图,但祖父母总是使用内部。这将允许父母在不破坏祖父母的情况下覆盖外部。

但是,鉴于您将其称为第三方类,我将假设您根本无法访问基类。

如果您愿意在主接口上破坏某些功能,可以使用运行时异常(如上所述)来解决这个问题。基本上,您可以覆盖公共变量,以便在他们执行您不喜欢的操作时抛出错误。上面提到了这个答案,但我会在变量(Map)级别而不是你的接口级别上进行。

如果您想允许 READ ONLY 访问地图顶部:

protected Map innerPropertyMap = propertyMap;
propertyMap = Collections.unmodifiableMap(innerPropertyMap)

显然,您可以使用map的自定义实现替换propertyMap。但是,只有当您想要禁用地图上的所有呼叫者时,这才真正有用,仅禁用某些呼叫者会很麻烦。 (我确信有一种方法可以做if(调用者是父)然后返回;否则错误;但它会非常非常混乱)。这意味着父母对课程的使用将失败。

请记住,即使您想要将其隐藏在儿童中,如果他们将自己添加到同一个包中,他们也可以解决您对以下内容的任何限制:

ThirdPartyClass grandparent = this;
// Even if it was hidden, by the inheritance properties you can now access this
// Assuming Same Package
grandparent.propertyMap.get("Parent-Blocked Chocolate Cookie")

因此,您有两种选择:

  1. 修改父对象。如果您可以修改此对象(即使您不能将该字段设为私有),您也可以采用一些结构性解决方案。
  2. 在某些用例中将属性更改为失败。这将包括祖父母和孩子的访问,因为孩子总是可以绕过父母的限制
  3. 同样,最容易将其视为一种方法:如果有人可以在祖父母身上调用它,他们可以在孙子身上调用它。

答案 7 :(得分:0)

使用包装器。反装饰器模式,而不是添加新方法,通过不提供调用它的方法来删除它们。