Abstract data types are not good things in Java for passing around complex objects or parameters.
Type safety
Lets compare two bits of code
String foo(Integer a, Integer b, String s) {
if(a.compareTo(b) == 0) {
return s;
} else {
return "" + (a - b); // just as an example
}
}
vs
String foo(Map<String, Object> m) {
if(m.get("a").compareTo(m.get("b")) == 0) {
return (String)m.get("s");
} else {
return "" + (((Integer)m.get("a") - (Integer)m.get("b"));
}
}
In the second example, we have to use Object
as the value because its the only object that String
and Integer
share in common. This means that all objects further down need to be cast. This has the distinct possibility of throwing a ClassCastException
(javadoc) anywhere or everywhere in your code... unless you add a ton of boiler plate code to prevent that (guard conditions for instanceof
all over).
Whats worse, is that those exceptions (if you don't check everything - and the paths you follow if you do) are runtime errors. The errors of your types won't be found until you run the program rather than when you compile it. Errors found when you compile are easier to fix than ones you discover at runtime. For that matter, the static analysis tools and inspections that most IDEs give you will catch them for you as you type them (and throw up horrendous warnings about trying to do it with the abstract data type).
Overloads
Passing all of the arguments as a single ADT means you have a single method. In the example above, what if you had:
String foo(Integer a, Integer b) { ... }
String foo(Integer a, Integer b, String s) { ... }
String foo(Integer a, Integer b, String s, String t) { ... }
as different ways to call the function. With passing one ADT, you've got just
String foo(Map<String, Object> m) { ... }
There is no way to differentiate the overloads. This leads to code that will look like:
String foo(Map<String, Object> m) {
if(m.containsKey("t")) { ... }
else if(m.containsKey("s")) { ... }
else { ... }
}
The complexity of the function will go through the roof (unless you start extracting those to other methods that are somehow indicating what type they're dealing with and my head is starting to hurt thinking about the hungarian notation you're going to be sticking into the method names).
This further gets problematic when you have different types that are valid arguments.
String foo(Integer a, Integer b, String s) { ... }
String foo(BigInteger a, BigInteger b, String s) { ... }
Typo safety
Long ago, I was a perl programmer - back in the days before OOP found its way into perl (bless
its reference). The only way to pass around complex objects was with %hashes
, and @arrays
. And you had to pull out the keys of the hash. There were numerous bugs that could only be caught at runtime (and the joys of autovivication made that challenging at times).
A simple typo in a key would mean some parameter was there wasn't found (when it was there) or was just created in the wrong spot and passed to another method.
String plusOne(Map<String, Integer> m) {
if(m.containsKey("type") && m.containsKey("1") {
return m.get("typo") + m.get("I");
}
return 0;
}
These are not fun bugs to have to find. No bugs are really fun, but these bugs just smack you in the face because they are completely preventable if you had just used a proper parameter list and work with the language and the compiler.
Calling Packaging
So far, I've talked about the dangers in the method itself. What about work that the callee has to do.
System.out.println(foo(1,2,"bar"));
And we're done with the simple parameter list.
Map<String, Object> m = new HashMap<String, Object>();
m.put("a", 1);
m.put("b", 2);
m.put("s", "bar");
System.out.println(foo(m));
Now, you want to debug this? What are you passing into foo? You've got to look back through the code and track it. Besides being a significantly larger block of code to do any method call of this type, its also obscufcating what is going into this.
Refactors
foo
now takes Double
s rather than Integer
s. You change the method signature and fix all the compile time errors. You've found them all and it compiles correctly (see type safety above).
However, the map version works just as well with Integer
as it does with Double
. You can't find out if you've fixed the refactoring or not.
Why are they in a Map
together?
This is more a philosophical question. Why are these objects in a map together? What common attribute do they share? If they really are part of some other data structure
class Cell {
int x, y;
String value;
}
make them into a proper data structure that sticks together. If they aren't things that are honestly related to each other, well... don't.
Putting things together in some structure makes them related to each other in our mind, even if they aren't. If they aren't this adds significant mental gymnastics in order to keep what things are related to each other and what ones aren't apart.
Best Answer
I see what you're doing with 2. You're using classes as packages and packages as modules so you can isolate yourself within the package but still organize within the package using classes.
That's very clever. Beware of clever.
This will force you to jam multiple classes in the same source file (which you might prefer) and the path will have an extra capitalized word.
This will also force you to write any test code within the package unless you use reflection to hack your way in from outside.
Other then that, this will work. It will just seem weird.
People are more used to inner classes being used like EntrySet in Hashtable. It's private so I can't create it but it implements a public interface so I just talk to it through the interface and have something grab one for me.
But you're describing classes you don't want me talking to even through an interface. So no interface for me. This means I have nothing to look at and be confused by (unless you provide me with source).
Biggest issue I foresee is this confusing newbies maintaining the API. You can throw documentation and comments at them but don't be supersized when they don't read or trust either one.
You've created yet another pattern that makes up for a deficiency in the language. Java has no access modifier that grants access to a group of packages. I've heard that a "module" access modifier had been proposed but see no sign of it happening.
The default access modifier (no modifier) is likely what you'll use here unless you don't mind me sneaking in through inheritance, in which case protected.
What you really want is module access. That way you could keep your tests in one package and code in another. Sadly we don't have it in Java.
Most people just do 1 and expand the API. Proper use of interfaces keeps the pressure off the implementation.
Hacking what you want into 1 is even uglier. Peek at the call stack and throw an exception whenever what called you is from a package you don't like. Eeew.