【问题标题】:Exporting non-public type through public API通过公共 API 导出非公共类型
【发布时间】:2011-02-14 05:59:08
【问题描述】:

如果我有几个工厂方法返回非公共类型和配对的方法集提供这种非公共类型的变量怎么办?这会在 NetBeans 中显示标题为警告消息。

结果公共 API 将仅包含两组配对方法。原因是使我的类型层次结构密封(如 Scala 中的密封类)并允许用户仅通过工厂方法实例化这些类型。所以我们在某种意义上得到了DSL。

例如,由日历字段的约束表示的 Schedule 类。有一些类型的约束 - Range、Singleton、List、FullSet - 以 NumberSet 接口为根。我们不想公开这些类型以及 Schedule 如何与它们交互。我们只需要用户的规范。所以我们将 NumberSet 包设为私有。在课程表中,我们为约束创建了一些工厂方法:

NumberSet singleton(int value);
NumberSet range(int form, int to);
NumberSet list(NumberSet ... components);

还有一些创建Schedule对象的方法:

Schedule everyHour(NumberSet minutes);
Schedule everyDay(NumberSet minutes, NumberSet hours);

用户只能通过以下方式使用它们:

Schedule s = Schedule.everyDay( singleton(0), list(range(10-15), singleton(8)) );

这是个坏主意吗?

【问题讨论】:

    标签: java api inheritance


    【解决方案1】:

    这个想法本身是合理的。但是,如果您将 root 类型(此处为:NumberSet)包设为私有,它将不起作用。要么公开该类型,要么改用公共接口。应该(并且可以)隐藏实际的实现类型。

    public abstract class NumberSet {
    
        // Constructor is package private, so no new classes can be derived from
        // this guy outside of its package.
        NumberSet() {
        }
    }
    
    public class Factories {
    
        public NumberSet range(int start, int length) {
            return new RangeNumberSet(start, length);
        }
    
        // ...
    }
    
    class RangeNumberSet extends NumberSet {
       // ... must be defined in the same package as NumberSet
       // Is "invisible" to client code
    }
    

    编辑 从公共 API 公开隐藏/私有根类型是错误的。考虑以下场景:

    package example;
    
    class Bar {
        public void doSomething() {
                // ...
        }
    }
    
    public class Foo {
    
        public Bar newBar() {
            return new Bar();
        }
    }
    

    并考虑使用此 API 的客户端应用程序。客户能做什么?它不能正确地将变量声明为类型为Bar,因为该类型对于包example 之外的任何类都是不可见的。它甚至不能在它从某个地方获得的Bar 实例上调用public 方法,因为它不知道存在这样的公共方法(它看不到该类,更不用说它公开的任何成员)。因此,客户可以在这里做的最好的事情是:

    Object bar = foo.newBar();
    

    这基本上是无用的。另一件事是有一个公共接口(或抽象类)而不是包私有接口,如上面定义的代码。在这种情况下,客户端实际上可以声明一个NumberSet 类型的变量。它无法创建自己的实例或派生子类,因为构造函数对其隐藏,但它可以访问定义的公共 API。

    再次编辑即使你想要一个“无特征”的值(从客户端的角度来看),即一个没有定义客户端可能想要调用的任何有趣 API 的值,它仍然是以上述方式公开公共基类型是一个好主意。并且只是为了编译器能够执行类型检查并允许客户端代码将此类值临时存储到(正确声明的)变量中。

    如果您不希望您的客户端调用该类型的任何 API 方法:那很好。没有什么可以阻止您不在(否则)公共基础类型上提供公共 API。只是不要声明任何东西。使用“空”抽象基类(从客户端的角度来看是空的,因为所有有趣的方法都是包私有的,因此是隐藏的)。但是你仍然必须提供一个公共的基类型,或者你应该使用普通的Object 作为返回值,但是你会放弃编译时的错误检查。

    经验法则:如果客户端必须调用某个方法来获取值并将其传递给其他 API,那么客户端实际上知道,存在一些神奇的特殊值.它必须能够以某种方式处理它(至少“传递它”)。除了(完全合适的)编译器警告之外,不为客户端提供适当的类型来处理这些值不会给您带来任何好处。

    public abstract class Specification {
    
        Specification() {
            // Package private, thus not accessible to the client
            // No subclassing possible
        }
    
        Stuff getInternalValue1() {
            // Package private, thus not accessible to the client
            // Client cannot call this
        }
    }
    

    就客户端代码而言,上述类是“空的”;它不提供可用的 API,除了 Object 已经提供的东西。拥有它的主要好处:客户端可以声明这种类型的变量,并且编译器能够进行类型检查。但是,您的框架仍然是唯一可以创建这种类型的具体实例的地方,因此,您的框架可以完全控制所有值。

    【讨论】:

    • 是的,但是看到这种类型是不受欢迎的客户。我们只需要通过工厂方法进行规范,并通过工厂方法将其传递给 Schedule 对象。
    【解决方案2】:

    我可以想到两种方法来做到这一点,但都没有真正奏效:

    1. NumberSet 声明为包私有,但将当前使用NumberSet 类型的公共API 方法更改为使用Object,并让实现将参数从Object 转换为NumberSet必需的。这依赖于调用者总是传递正确类型的对象,并且很容易出现ClassCastExceptions。

    2. 实现一个没有方法的公共NumberSet 接口,并将实现NumberSet 并定义内部所需方法的私有InternalNumberSet 封装起来。再次使用类型转换将参数NumberSet 转换为InternalNumberSet。这比以前的方法更好,但是如果有人创建了一个实现 NumberSet 的类而不实现 InternalNumberSet,结果将再次为 ClassCastExceptions。

    我个人认为您应该将NumberSet 接口声明为公开。在接口中公开 getter 并没有真正的危害,如果您希望实现类不可变,请将它们声明为 final 并且不要公开 setter。

    【讨论】:

      猜你喜欢
      • 2020-03-18
      • 2013-03-18
      • 2019-11-01
      • 2016-08-31
      • 2015-09-22
      • 2019-01-10
      • 2018-01-16
      • 1970-01-01
      相关资源
      最近更新 更多