Thursday, October 25, 2007

Value objects WTF!!?!!2!

You might recall that here at the smallwig we recently, geologically speaking, discussed the interesting and important topic of how to model a simple "value object" in the Java[TM] Technology Platform Language Technology[TM[TM]]. (note: not its exact name. I can never remember exactly how we're supposed to refer to J*va. F*ckin' J*va.)

Now, in that post, we considered a simple example -- a class with no special behavior, only two simple attributes.

We'd like this class be simple, straightforward, well-behaved, idiomatic and correct.

Here's look at how the code came out -- actually no, let's make it immutable this time, because immutable is simpler and easier. Here goes:


public final class Foo implements Serializable {
private final String text;
private final Integer number;

public Foo(String text, Integer number) {
this.text = text;
this.number = number;
}

public String getText() {
return text;
}

public Integer getNumber() {
return number;
}

@Override public boolean equals(Object object) {
if (object instanceof Foo) {
Foo that = (Foo) object;
// oops! I cheated and used a helper class from the
// Google Collections Library!
return Objects.equal(this.text, that.text)
&& Objects.equal(this.number, that.number);
}
return false;
}

@Override public int hashCode() {
// oops! I did it again! ha ha!
return Objects.hashCode(text, number);
}

@Override public String toString() {
return String.format("[Foo: %s, %s]", text, number);
}

private static final long serialVersionUID = 0xB0B15C00L;
}


(Incidentally, this is the point in the previous post at which I proceeded to engage in the professionally dubious activity of laying down a few good old-fashioned "F-Bombs". Please note that it is generally considered inadvisable to spew "foul language" in a "technology blog" which you dream will become "respected" one "day." However, in some circumstances this approach is actually appropriate, for the basic reason that THIS IS A ****ING LOT OF CODE FOR SOMETHING SO MOTHER****ING SIMPLE WHAT THE ****.)

So anyway!

We have a problem here. Here's the problem:


  • A value object is a commonly-needed thing.
  • This is too much code to have to write for such a commonly-needed thing.
  • It easy to get some of the subtle details wrong.
  • If we write tests for these idiotic classes, we're wasting time; if we don't write tests for these idiotic classes, we find out later that they're buggy because, say, we forgot to use a null-safe equality check for a nullable field, or something.
  • Any special behavior you want to add to the class just gets lost in the sea of boilerplate.
  • Uh oh -- now you want your value object to be Comparable too, say by a lexical comparison of its fields. More code to write and rewrite.


Now, what should we do?

Solution 1: Do nothing?

But this answer makes no sense. We've all learned time and time again the perils of code duplication. And this is egregious code duplication. Why should we tolerate it? We shouldn't.

Solution 2: IDE Templates to the rescue?

But wait, you say, I don't have to write this stuff, I just click-click-click-click in my IDE and it generates all of that for me! Problem solved!

The last thing I'll do is argue against this because "not everyone uses an IDE." I'll be totally honest with you: forget people who don't use an IDE. I'm sorry, you know, I believe in "to each his own" and all that, it's just that if "your own" is to "run away from tools that are there to help you and work really well", then I just can't save you from yourself. You know what I mean?

No, that's not it. Look: IDE-generated code is copy-and-paste code. That's all it is. The IDE has a template, it copies it, it pastes it, it changes stuff around. So why people who vehemently detest copy-and-paste coders would then go and have their IDE generate equals() and hashCode() and toString() and compareTo() and clone() methods for them I don't know.

Sure I've alt-Enter'd my way through the creation of many a class. I like generating constructors and automatically extracting fields. But I like it because it's a faster way to write the code that I could have written myself, and would have written the same way anyway.

But no, the equals() and hashCode() methods I've seen IDEs generate are hideously ugly. Which brings me to my other point: IDE templates are not a solution because they only address a small part of the problem. They make classes faster to write the first time, but they do nothing at all towards making your code easier to read or maintain.

Solution 3: Pair! Triplet! Quadruplet! .... McCaugheys?

Don't laugh (all right, laugh). A lot of people really are doing this. They're getting their equals() and all that for free by subclassing their objects from classes like Pair and Triplet and.... well, God, I really hope they just stop there. This brings up all kinds of subtle trouble. For example, you don't want someone's FooPair("a", 1) to be considered as equals() to someone else's BarPair("a", 1), but they kind of have to be, since Pair is a useful (if degenerate) collection class in its own right which demands the customary equals() behavior and subclasses deviating from that breaks "substitutability" and blah blah blah blah.

It's even worse when they don't bother with even this much, have Pair showing up in their public API and all kinds of garbage.

Anyway, this is totally unscalable, so it's a non-solution and I don't think we should spend another column inch talking about this one.

Solution 4: We need a language change!

I once had this friend, a female friend, who was one of those rare people who had brains and beauty and a fun personality and wasn't a stark raving bitch, etc. But she had this problem that some people have, where she was incapable of ever falling in love with a guy unless that guy was somehow completely unavailable to her. She'd be smitten with him as long as he was married, or gay, or her faculty advisor, or her psychiatrist, or a minor, etc. But she kept never noticing the people who were right in front of her who she could have had any time she wanted. It was really sad.

Huh? Where was I? Oh yeah...

Solution 5: Reflectoporn

Here the idea is that you implement equals(), hashCode(), toString(), and compareTo() like this:


@Override public boolean equals(Object obj) {
return Reflectomatic.equals(this, obj);
}


And these libraries would use reflection to look at all the fields of your class and do the expected fieldwise thing. If you had a field you didn't want to be considered for these purposes, perhaps you could annotate it to that effect.

And in fact, these methods could be defined in an abstract base class which you could extend so you'd have to write even less code. Our example above might turn into:


public final class Foo extends AbstractValueObject {
private final String text;
private final Integer number;

public Foo(String text, Integer number) {
this.text = text;
this.number = number;
}

public String getText() {
return text;
}

public Integer getNumber() {
return number;
}

private static final long serialVersionUID = 0xC11FF15C00L;
}


Hrm. Well.... this isn't tooo, bad, actually. We're supposed to abhor reflection, though, aren't we? Demonized as being slow, isn't it? Doesn't it just feel like cheating?

Let's bookmark this idea and just go ahead and see if we can do any better.

Oh noe! This post is another goddamn teaser again!

Tune in next time when I discuss "classic" code generation, bytecode generation... and an idea which, unlike all the rest of these, might possibly be new to you! You can expect that post in... oh, certainly if not this year then definitely in the next one, I'm sure.

21 comments:

JodaStephen said...

Another one for your list - repetitive code generation. We have a tool that will generate the get/set/equals/hashcode/clone/property methods for you based on just the field definitions. And if you change the field definitions, it rewrites all the generated methods, so its not just dead code from a template.

(property methods allow many of the features of the big property debate, but mostly they allow access to fields without reflection enabling object to XML, XML to object and XPath querying of objects)

Eric Burke said...

How the f*ck do you write "EricIsCool" in Hex? Help! (see, it's ok if I use that asterisk...think of the children)

Brian said...

Channelling Steve Yegge?

Rob Jellinghaus said...

Java equality semantics are evil anyway. You can't possibly get a subclassed equals method right. Don't even try! Declarative object identity for the win!

paulo said...

I just did this large completely evil thing (reusable) (use with static import Tuples.*;)
package util;

import java.io.Serializable;

/**
* Type safe, inmutable, tuple classes
* can be serialized with xmlencoder (>=1.6)
* or objectoutputstream
* @author i30817
*/
public final class Tuples {

private Tuples() {
}

public static class T1 X implements Serializable {

private static final long serialVersionUID = 262498860763181265L;
private X first;

@java.beans.ConstructorProperties({"first"})
public T1(X first) {
this.first = first;
}

public X getFirst() {
return first;
}

@Override
public String toString() {
return "(" + first.toString() + ")";
}

@Override
public int hashCode() {
final int PRIME = 31;
int result = 1;
result = PRIME * result + ((first == null) ? 0 : first.hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final T1 other = (T1) obj;
if (first == null) {
if (other.first != null) {
return false;
}
} else if (!first.equals(other.first)) {
return false;
}
return true;
}
}

public static class T2 X, Y implements Serializable {

private static final long serialVersionUID = 363498820763161265L;
private X first;
private Y second;

@java.beans.ConstructorProperties({"first","second"})
public T2(X first, Y second) {
this.first = first;
this.second = second;
}

public X getFirst() {
return first;
}

public Y getSecond() {
return second;
}

@Override
public String toString() {
return "(" + first.toString() + ", " + second.toString() + ")";
}

@Override
public int hashCode() {
final int PRIME = 31;
int result = 1;
result = PRIME * result + ((first == null) ? 0 : first.hashCode());
result = PRIME * result + ((second == null) ? 0 : second.hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final T2 other = (T2) obj;
if (first == null) {
if (other.first != null) {
return false;
}
} else if (!first.equals(other.first)) {
return false;
}
if (second == null) {
if (other.second != null) {
return false;
}
} else if (!second.equals(other.second)) {
return false;
}
return true;
}
}

public static class T3 X, Y, Z implements Serializable {

private static final long serialVersionUID = 162498830763181765L;
private X first;
private Y second;
private Z third;

@java.beans.ConstructorProperties({"first","second","third"})
public T3(X first, Y second, Z third) {
this.first = first;
this.second = second;
this.third = third;
}

public X getFirst() {
return first;
}

public Y getSecond() {
return second;
}

public Z getThird() {
return third;
}

@Override
public String toString() {
return "(" + first.toString() + ", " + second.toString() + ", " + third.toString() + ")";
}

@Override
public int hashCode() {
final int PRIME = 31;
int result = 1;
result = PRIME * result + ((first == null) ? 0 : first.hashCode());
result = PRIME * result + ((second == null) ? 0 : second.hashCode());
result = PRIME * result + ((third == null) ? 0 : third.hashCode());
return result;
}

@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
final T3 other = (T3) obj;
if (first == null) {
if (other.first != null) {
return false;
}
} else if (!first.equals(other.first)) {
return false;
}
if (second == null) {
if (other.second != null) {
return false;
}
} else if (!second.equals(other.second)) {
return false;
}
if (third == null) {
if (other.third != null) {
return false;
}
} else if (!third.equals(other.third)) {
return false;
}
return true;
}
}
}

Daniel said...

Great rant. Not sure what Kevin has in mind, but here's my solution:

http://javablog.co.uk/2007/11/05/equality/

Uses annotations, reflection and sanity.

Den Orlov said...

Yep, I think that "Declarative object identity" could be implemented with annotations + bytecode manipulation + some lib (might be extension of lib that mentioned in prev post)

Jordan said...

Immediately after I read this, I bookmarked it — now, when I find myself frustrated with Java, I read this blog post. For some perverse reason, reliving your frustration gives me great joy and serenity, especially when I reach your priceless "f-bombs".

Thank you for unselfishly releasing a little more Schadenfreude into the world, Kevin!

Doug Clinton said...

I'm new to your blog and have been looking through your past posts. Now I'm wondering if you're ever going to post the promised part 3 of this series?

Dan said...

I usually write a lot of code as it is in yours example and never thought about shortening it a little bit...

Here is the idea i had while reading this post:

interface Foo {
String getText();
Integer getNumber();
}

Foo instance = ValueObject.create(Foo.class, ...);

where "..." are the initial values for thoose two fields.


ValueObjects.create() is a static factory method that manages map of implementations for different value objects. I think value objects can be implemented using dynamic proxy that accesses the array of values.
array of values has the types populated from return types of the methods of interface using reflection.

You can add 6 section called "David Blane and dynamic proxies"

nasha said...

Why was there no follow on bankruptcy then? The bailout of AIG FP went to (wow power leveling) hedge funds that bound credit swaps on Lehman failing or others betting on rating (wow power leveling) declines. AIG has drained over 100 billion from the government. Which had to go to (wow power leveling) those who bet on failures and downgrades. Many of whom (power leveling)were hedge funds. I-banks that had offsetting swaps needed the money from the AIG bailout or they would have been caught. Its an (wow powerleveling) insiders game and it takes just a little bit too much time for most people to think (wow gold) through where the AIG 100 billion bailout money went to, hedge funds and players, many of whom hire from the top ranks of DOJ, Fed, Treasury, etc. ZHANG XIAO CHEN

Affordable Luxurious Wedding Dress Blog said...

cheap wedding gowns,
discount bridal gowns,
China wedding dresses,
discount designer wedding dresses,
China wedding online store,
plus size wedding dresses,
cheap informal wedding dresses,
junior bridesmaid dresses,
cheap bridesmaid dresses,
maternity bridesmaid dresses,
discount flower girl gowns,
cheap prom dresses,
party dresses,
evening dresses,
mother of the bride dresses,
special occasion dresses,
cheap quinceanera dresses,
hot red wedding dresses

cheap wow gold said...

my wow gold cheapest wow power leveling cheap wowgold

products said...

China Wholesalers has been described as the world’s factory. buy products wholesaleThis phenomenom is typified by the rise ofbusiness. Incredible range of products available with China Wholesale “Low Price and High Quality” not only reaches directly to their target clients worldwide but also ensures that wholesale from china from China means margins you cannot find elsewhere and China Wholesale will skyroket your profits.china wholesale productsbuy china wholesalewholesale chinawholesale productsbuy products

snekse said...

Where's "Value objects WTF!!?!!3!"?

snekse said...

Oh, forgot to ask, if this is an immutable object, is there really a need to make the member fields private with getters?

combattery84 said...

Dell Inspiron 600m battery
Dell Inspiron 8100 Battery
Dell Y9943 battery
Dell Inspiron 1521 battery
Dell Inspiron 510m battery
Dell Latitude D500 battery
Dell Latitude D520 battery
Dell GD761 battery
Dell NF343 battery
Dell D5318 battery
Dell G5260 battery
Dell Inspiron 9200 battery
Dell Latitude C500 battery
Dell HD438 Battery
Dell GK479 battery
Dell PC764 battery
Dell KD476 Battery
Dell Inspiron 1150 battery
Dell inspiron 8500 battery
Dell Inspiron 4100 battery
Dell Inspiron 4000 battery
Dell Inspiron 8200 battery
Dell FK890 battery
Dell Inspiron 1721 battery

Dell Inspiron 1300 Battery
Dell Inspiron 1520 Battery
Dell Latitude D600 Battery
Dell XPS M1330 battery
Dell Latitude D531N Battery
Dell INSPIRON 6000 battery
Dell INSPIRON 6400 Battery
Dell Inspiron 9300 battery

dainfo said...

workout at home with dumbbells
at home workouts
lose weight at home
abs workout at home
exercise machines at a gym
chest workout at home
get fit with exercise
how to exercise at home
run a marathon to shed pounds and improve cardio
how to lose weight at home
a healthy heart comes with exercise
medicine ball abs
fatigue your muscles by using weights or your body
upper body workout at home
be sure to hydrate your muscles as often as possible
dandruff home remedy
Good share you have here hope you keep it up, and don't forget to use dumbbells while exercising, even better though you can do these lose weight at home
where to buy venapro
buy venapro
venapro
venapro reviews
venapro review
best tooth whitening system
bleaching your teeth
teeth bleaching products
teeth bleaching trays
tooth whitening bleach

zhengbin said...

Other ways to unlock trapped cash thomas sabo is in the form of selling thomas sabo shop silverware, silver flatware, sterling silver thomas sabo jewellery and scrap silver. Each of these thomas sabo schmuck will fetch different values depending on charm club thomas sabo the product and purity factors. sabo charm club With the current economic condition, selling thomas sabo 2010 precious metals, either pure or scrap, has gained thomas sabo sales a lot of importance since it thomas sabo reduziert has great intrinsic value attached to it and selling the scrap is one of the smartest ways of making money.

Berrty Gawill said...

mulberry
Mulberry Alexa Bag
Mulberry Bayswater Bags
Mulberry Briefcases Bags
Mulberry Roxanne Bags
Mulberry Somerset Bags

Jerry Gene said...

Your blog is so excellent that I like it very much, you must be good at writing.

Asus - 15.6" Laptop - 4GB Memory - 500GB Hard Drive - IMR Matte Dark Brown Suit (K53E-BBR21)

Asus - 15.6" Laptop - 4GB Memory - 500GB Hard Drive - IMR Matte Dark Brown Suit (K53E-BBR19)