开发者

Understanding Java Packages and Factories

I'm reading Bruce Eckel's Thinking in Java and there's an exercise I'm just not getting:

Pg. 161: Exercise 8: (4) Following the form of the example Lunch.java, create a class called ConnectionManager that manages a fixed array of Connection objects. The client programmer must not be able to explicitly create Connection objects, but can only get them via a static method in ConnectionManager. When the ConnectionManager runs out of objects, it returns a null reference. Test the classes in main( ).

I came up with the following solution:

// TestConnection.java
import java.util.*;

public class TestConnections {
    public static void main( String[] args ) {
        Connection cn = Connection.makeConnection();

        for (int i = 0; i != 6; ++i) {
            Connection tmp = ConnectionManager.newConnectiton();
            if ( tmp == null )
                System.out.println("Out of Connection objects");
            else {
                System.out.println("Got object: " + tmp );
            }
        }
    }
}

And a second file in the same directory meaning everything ends up in the same default package:

// ConnectionManager.java
class Connection { 
    private Connection() {}

    static Connection makeConnection() {
        return new Connection();
    }
}

public class ConnectionManager {
    static private Connection[] _connections = new Connection[5];

    private ConnectionManager() {}

    static public Connection newConnectiton() {
        for ( int i = 0; i != _connections.length; ++i ) {
            if ( _connections[i] == null ) {
                _connections[i] = Connection.ma开发者_如何转开发keConnection();
                return _connections[i];
            }
        }
        return null;
    }
}

The thing is that the client program can directly create Connection objects via the static Connection.makeConnection factory, which seems to violate the exercises goals. Yet if I make the ConnectionManager.java a separate package then import it complains that it can't find a definition for Connection.

I feel like something is going over my head here, but I'm not sure what.

Here is the code for Lunch.java which is referenced in the question:

//: access/Lunch.java
// Demonstrates class access specifiers. Make a class
// effectively private with private constructors:

class Soup1 {
  private Soup1() {}
  // (1) Allow creation via static method:
  public static Soup1 makeSoup() {
    return new Soup1();
  }
}

class Soup2 {
  private Soup2() {}
  // (2) Create a static object and return a reference
  // upon request.(The "Singleton" pattern):
  private static Soup2 ps1 = new Soup2();
  public static Soup2 access() {
    return ps1;
  }
  public void f() {}
}

// Only one public class allowed per file:
public class Lunch {
  void testPrivate() {
    // Can't do this! Private constructor:
    //! Soup1 soup = new Soup1();
  }
  void testStatic() {
    Soup1 soup = Soup1.makeSoup();
  }
  void testSingleton() {
    Soup2.access().f();
  }
} ///:~


Each class has a scope. In your code, the Connection class has package scope (i.e., it can only be accessed by classes within that same package). This means that moving the ConnectionManager class to another package moves it outside the scope that can see the Connection class; that's the failure you see. Those connections have to be co-located with their factory.

In reality, what you would actually do is you create a public interface that defines what operations on the Connection are, and a private implementation of that interface inside the ConnectionManager; the newConnection method can then just declare that it returns an instance of the interface, and the package protection mechanism stops anyone from prying behind that interface (well, not without using the funkier parts of reflection).


The trick: Connection shouldn't be a concrete class but an interface. A second class (ConnectionImpl) provides an implementation of this interface.

Only the ConnectionManager can instantiate instances of this concrete class and returns the interface.

Example (reduced to show access modifiers):

public interface Connection() {
}

public class ConnectionManager {
  private static class ConnectionImpl implement Connection {
    private ConnectionImpl() {
    }
  }
  public static Connection createConnection() {
    return new ConnectionImpl();
  }
}


Another way is to make Connection a public nested class inside the ConnectionManager. But you might not want that. Andreas_D's and Donal's suggestions are better and more practical, indeed.

public class ConnectionManager {
    private static Connection[] _connections = new Connection[5];

    private ConnectionManager() {}

    public static Connection newConnectiton() {
        for ( int i = 0; i != _connections.length; ++i ) {
            if ( _connections[i] == null ) {
                _connections[i] = new Connection();
                return _connections[i];
            }
        }
        return null;
    }

    public static class Connection { 
        private Connection() {}
    }

}

Edited to show how the TestConnections will look like,

public class TestConnections {

    public static void main(String[] args) {
        Connection conn = ConnectionManager.newConnectiton();
    }
}


This is following Lunch.java more in a sense that its done by placing some classes under a different with appropriate access modifiers. Assuming the student/reader of the book isn't introduced to interfaces, yet.

// File cm.Connection.java
package cm;

public class Connection {
    // The constructor has package access and so is available to 
    // ConnectionManager, but not to any class outside package cm
    Connection() { }
}

// File cm.ConnectionManager.java
package cm;

public class ConnectionManager {
    static private Connection[] _connections = new Connection[5];

    private ConnectionManager() {}

    static public Connection newConnectiton() {
        for ( int i = 0; i != _connections.length; ++i ) {
            if ( _connections[i] == null ) {
                _connections[i] = new Connection();
                return _connections[i];
            }
        }
        return null;
    }
}

Both, Connection and ConnectionManager are in the same package cm.

// File different.TestConnections.java
package different;

import cm.*;

public class TestConnections {

    public static void main(String[] args) {
        Connection conn = ConnectionManager.newConnectiton();
    }
}

Notice, TestConnections is in the different package.


You said

The thing is that the client program can directly create Connection objects via the static Connection.makeConnection factory

I think if it is via a static method, it is not directly.

Also, looking back at the task

The client programmer must not be able to explicitly create Connection objects

You solution hides the fact that new connections are created. You can emphasize this by renaming "newConnection" to "getConnection" or something.

Maybe the expected solution should reuse connection objects, but it does not say so in the excercise. (And it does not make much sense for connections).


The purpose of getting the connection via ConnectionManager is to have the resource related things in a separated class. In this case you can mock the connection when writing a unit test.


The makeConnection() method has default visibility, which means it cannot be used directly by code in different packages. (Actually the same is true for the Connection class itself, which I don't think you want).

Note that client programmers can get around this by placing their code in the same pacakge; but code visibility should generally be seen as an aid to help client programmers see the API rather than the implementation details, not as a way to prevent willful stupidity or malice. Though if you really need to run untrusted code, injecting code into your packages can be prevented by signing your JARs.

0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜