Java Encapsulation – How to Encapsulate Method Parameters

encapsulationjavaparameters

An HTTP query can be parametrized to a list of pairs like name=value that can be encapsulated in general as Map<String, String> since an HTTP GET query is string pairs. I could need something for any combination of objects and primitives. A varargs method for example update(String s1...) could be written in general as update(Map<String, String>) where the key is the name and the value is the data.

Therefore I wonder if it could be good to use a definition as Map<K, V> instead of writing a custom object in this case wrapping the specific 3 parameters to a class in their specific types which can't be reused whereas a Map<K, V> can be used to encapsulate any 2-tuples when we must write a pair like Integer i or int i or int i=0.

But I think it will only work for classes and not for primitive types, so the primitive types must be boxed in their objects so I must also box an int to an Integer for this to also work with primitive types.

Is there a better way than Map<K, V>to encapsulate method parameters when we must select a list of unknown length and unknown types where the types could be primitives or objects?

So instead of being specific about types and their values in the method, can I use a Map<K, V> instead of a list of named pairs such as String s, int i, Object o...

Best Answer

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 Doubles rather than Integers. 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.

Related Topic