0

Assume there's a public void process method on a Java class called A that currently takes a single String argument id.

class A {
  public void process(final String id) {
    // Some implementation
  }
}

The existing method's String id argument has a specific format: type#integer where type can be foo or bar (so foo#1 and bar#1).

While the underlying implementation does not change based on the format of id, having the method take a String seems less accurate than representing the argument as a concrete type, especially when unfamiliar readers examine the public interface of the class.

One approach is to parse the id value into one of two classes (FooId and a BarId) and to represent the id argument as the Union of two input types - a FooId and a BarId.

public class FooId {
}

public class BarId{
}

public class IdParser {
  public Union<FooId, BarId> parse(final String id) {
    final String parts = id.split("#");
    if (2 == parts.length) {
      final int value = Integer.parseInt(parts[1]);
      if ("foo".equals(parts[0])) {
        return Union._1(new FooId(value));
      }

      if ("bar".equals(parts[0])) {
        return Union._2(new BarId(value));
      }
      
    }
    throw new IlllegalArgumentException("Invalid id value");
  }
}
public void process(final Union2<FooId, BarId> id)

Another approach is to represent the id value as a "generic" Id class that wraps the two types and keeps track of the type based on an internal Type enum like

class Id {
  private static final enum Type {
    Foo,
    Bar;
  }

  private final Type type;
  private final int value;

  private Id(final Type type, final int value) {
    this.type = type;
    this.value = value;
  }

  public static Id from(final String formattedId) {
    // parse the type and value and return an Id instance
  }
}

What are the tradeoffs between these two approaches?

Would you instead recommend using method overloading instead of a Union?

public void process(final FooId id)
public void process(final BarId id)
10
  • can you clarify where you have two types of id, foo and bar? or if "foo" and "bar" are just possible first parts of an id? Union2<FooId, BarId? seems wrong?
    – Ewan
    Jun 14, 2022 at 18:01
  • @Ewan The example input argument String values have two different prefixes. One could make the argument that these two prefixes represent two different types of ids where the foo-prefixed id String values can be represented by a FooId class and the bar-prefixed id String values can be represented by a BarId class. Jun 14, 2022 at 18:07
  • so do you mean Union2<string,int>
    – Ewan
    Jun 14, 2022 at 18:14
  • @Ewan no. Let's not use foo and bar prefixes - let's say the prefixes are car and motorcycle (so the inputs could look like car#1234 and motorcycle#1234). You could imagine that instead of expressing car#1234 as a String, you could create a new class CarId with a private int member value variable (1234, for example). You could do the same thing with motorcycle#1234 and MotorcycleId. So now, instead of an input where the String has two different formats, there are concrete classes that can express the different input forms. Jun 14, 2022 at 18:52
  • ok, but you would never have union<fooType,barType>. union<string,int> would be x.first = foo x.second = 1
    – Ewan
    Jun 14, 2022 at 19:01

2 Answers 2

2

Ignoring some of these implementation details, the advantage of the second approach is the ease with which a BazId can be added.

Of course none of that matters unless Id's being different types actually means something. If you scatter around a lot of type testing code you're right back to adding BazId being difficult.

By which, I mean, these types need code that uses them to be useful. Be it polymorphic code or if type==bazId code. Without that the types are pointless. This is missing from the examples in the question. If such code must know all the types and appears in many places all those places must be changed to add a new type.

That concern drives you back to the first approach. Some even do it with a big switch statement.

Ideally you could simply add one new file and poof BazId works. The closest I've seen anything come to that is a class loader. But, well now you've simply moved the code that knows all the types to a configuration file.

The fundamental problem is something must create an ordering for these types to give them their numbers. And that something can't be allowed to renumber old ones with new numbers or old strings become invalid.

So what we're left with is each Id class must declare it's own number. And carefully avoid using any other Id types number.

Do that and when adding new types you can avoid forcing rewrites of old code. Without that, either way ends up just as messy in the end.

This is not a new problem. It's the serialVersionUID part of Java Serialization all over again.

3
  • I'm not following do you mind explaining "If you scatter around a lot of type testing code you're right back to adding BazId being difficult." ? Also, "So what we're left with is each Id class must declare it's own number. And carefully avoid using any other Id class's number." is unclear to me - I never said there were any requirements around integer value uniqueness. In fact, in my examples, I intentionally used the same integer id values. Jun 14, 2022 at 21:33
  • If the integer values are not unique then they aren’t IDs. You made them unique when you made them enum’s. Note edit. Jun 14, 2022 at 22:30
  • My intent was to convey that the integer values are unique for all foo prefixes and all bar prefixes (in this hypothetical example). Assume the process method takes a String value in the form tableA#primaryKey or tableB#primaryKey. I'm going through the tradeoffs of replacing that String with some non-String model object. Jun 15, 2022 at 4:06
0

If you want separate Types for Foo and Bar the union approach is flawed. After you parse the string you need to check for null to see what if any Type was parsed. As you add more types you will have to extend to a UnionN<foo,bar,baz,whatever....>

The correct approach would be to use polymorphism

Interface Id
{
    string StringRepesentation
    void ParseString(string id)
    object RealId
    void Process()
}

class FooId : Id
{
    bool ParseString(string id)
    {
       If(id.StartsWith("foo") { this.RealId = //parse out the int ..
       else { return false;
    }
    void Process() {}...
}

class IdParser
{
     public Id Parse(string id)
     {
        //loop through known types and return the one that matches
     }
}

//now you can call the correct processing logic
var Id = IdParser.Parse(string);
Id.Process()

But overloading is ugly and the parsing at least in your example is pretty meaningless.

You could use a Union if all your Ids have the same pattern

Union<string, int> id
id.first = string.split("#")[0];
id.last = string.split("#")[1];

if(id.first = foo) { ..process foo

But you will need all the extra parsing and re serialising logic somewhere. The generic id class is a better approach because it can hold that logic.

However. You don't really give a good reason to convert the string at all. However you do things you are going to have some sort of switch, if block or loop to sort out the types. overloading and polymorphism are not really in good favour these days, its seen as complex and cumbersome compared to simple composition.

If your Ids are a simple combined key with two parts just keeping them as strings and testing for startsWith() when you load whatever object they refer to, which presumably will benefit from a Type, is the simplest approach.

1
  • "You don't really give a good reason to convert the string at all." - I'd like the method interface to not take a String and to instead use classes to represent the data type(s) that is being represented by the String value. I don't think your Union example holds up since a Union type from a functional programming perspective is values of the first type, or values of the last type. You don't get both a value for the first type and a value of the last type. Your Union example looks more like a Pair. Jun 15, 2022 at 4:01

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.