Wie Josh Bloch schreibt, ist es bei Geschäftsobjekten "notwendig, die Methode equals zu überschreiben, um die Erwartungen der Programmierer zu erfüllen".(Effektives Java, Punkt 7). Abgesehen von den von ihm erwähnten Vorteilen - Konformität mit den Erwartungen, korrekte Verwendung in Maps und Sets usw. - Ich habe festgestellt, dass die Implementierung von equals (und hashCode) Sie wirklich zum Nachdenken darüber bringt, was die Klassen repräsentieren. Bei Geschäftsobjekten, d.h. Objekten in Ihrem Domänenmodell, ist der Versuch, eine Identität auf Geschäftsebene für Ihre Klassen zu definieren, eine gute Möglichkeit, um zu überprüfen, ob Sie ein geschäftsrelevantes Konzept korrekt erfasst haben. Eine equals-Definition kann auch als nützliche Dokumentation zur Beschreibung Ihrer Domäne dienen. Hier werden wir einen Ansatz betrachten, der versucht, dies so sauber und bequem wie möglich zu tun... deklarativ!
Das Problem mit den Gleichen
Die Gleichheitsmethode ist im Wesentlichen selbstdokumentierend. Nun, irgendwie schon. Aber wie einfach ist es eigentlich, aus der folgenden (von Eclipse automatisch generierten) Implementierung zu erkennen, dass eine PhoneNumber durch die Landesvorwahl, die Ortsvorwahl und die Nummer bestimmt wird?
public class PhoneNumber {
private int countryCode;
private int areaCode;
private int number;
@Override
public boolean equals(Object obj) {
if (this == obj)
true zurückgeben;
if (obj == null)
return false;
if (getClass() != obj.getClass())
return false;
PhoneNumber other = (PhoneNumber) obj;
if (areaCode != other.areaCode)
return false;
if (countryCode != other.countryCode)
return false;
if (Zahl != andere.Zahl)
return false;
true zurückgeben;
}
// hashCode() ...
}
Die folgende "Standard"-Implementierung ist vielleicht ein wenig besser lesbar
public boolean equals(Object obj) {
if (this == obj) {
true zurückgeben;
}
if (!(obj instanceof PhoneNumber)) {
return false;
}
PhoneNumber other = (PhoneNumber) obj;
return ((countryCode == other.countryCode) && (areaCode == other.areaCode)
&& (Zahl == andere.Zahl));
}
oder mit Apache Commons' EqualsBuilder,
public boolean equals(Object obj) {
if (this == obj) {
true zurückgeben;
}
if (!(obj instanceof PhoneNumber)) {
return false;
}
PhoneNumber other = (PhoneNumber) obj;
return new EqualsBuilder().append(countryCode, other.countryCode)
.append(areaCode, other.areaCode).append(number, other.number).isEquals();
}
aber das ist immer noch eine Menge Standardcode, wenn wir eigentlich nur sagen wollen: "Verwenden Sie diese drei Eigenschaften!"
Ein anderer Ansatz
Wie wäre es dann mit etwas in dieser Art?
@BusinessObject
public class PhoneNumber {
@BusinessField
private int countryCode;
@BusinessField
private int areaCode;
@BusinessField
private int number;
@Override
public boolean equals(Object obj) {
return BusinessObjectUtils.equals(this, obj);
}
@Override
public int hashCode() {
return BusinessObjectUtils.hashCode(this);
}
// Getter und Setter usw...
}
Wenn wir schon einmal hier sind, warum nicht auch
@BusinessObject
public class PhoneNumber {
// ...
@Override
public String toString() {
return BusinessObjectUtils.toString(this);
}
}
was bei der Fehlersuche sehr nützlich sein kann?
Ähm...warum?
Was ist daran so schön? Nun, der Business Key für das Objekt ist sichtbar definiert, und zwar in den Feldern, die er umfasst, und nicht indirekt in einer Methode. Da die Annotation zur Laufzeit beibehalten wird, ist die Definition auch für jeden anderen Code zugänglich, der sich dafür interessiert. Und Boilerplate-Code ist fast auf Null gesunken, nach ein wenig Refactoring sogar noch mehr:
@BusinessObject
public abstract AbstractBusinessObject {
@Override
public boolean equals(Object obj) {
return BusinessObjectUtils.equals(this, obj);
}
@Override
public int hashCode() {
return BusinessObjectUtils.hashCode(this);
}
@Override
public String toString() {
return BusinessObjectUtils.toString(this);
}
}
public class PhoneNumber extends AbstractBusinessObject {
@BusinessField
private int countryCode;
@BusinessField
private int areaCode;
@BusinessField
private int number;
// Getter und Setter usw...
}
Natürlich gibt es nichts Neues unter der Sonne, und ich bin mir sicher, dass es etwas Ähnliches in der Codeosphäre gibt (es gab diesen Beitrag im Hibernate-Forum, aber er scheint verschwunden zu sein. Wenn Sie ein anderes Beispiel kennen, fügen Sie bitte einen Kommentar hinzu). Ich stieß auf tt>@BusinessObject in einem Projekt, das von Vincenzo Vitale, einem ehemaligen Kollegen, entwickelt wurde. Ich habe nur einige der Interna neu implementiert, um die Leistung zu verbessern, und die Semantik leicht verändert und erweitert.
Wow... erklären Sie das bitte!
Und wie funktioniert das alles? Laut BusinessObjectUtils.equals sind zwei Objekte nur unter den folgenden Umständen gleich:
- wenn eines der Objekte null ist, müssen beide null sein
- andernfalls müssen beide Objekte Business-Objekte sein (d.h. mit @BusinessObject annotiert oder eine solche Klasse erweitern) und
- sie müssen dieselben Geschäftsfelder haben, d.h. die Menge der Namen der mit @BusinessField annotierten Felder (unabhängig davon, ob sie in der Klasse definiert oder geerbt sind) muss für beide Klassen gleich sein und
- die Werte von Geschäftsfeldern mit demselben Namen müssen alle gleich sein
Beachten Sie, dass diese Bedingungen reflexiv, symmetrisch und transitiv sind, d.h. eine gültige Implementierung von ist gleich: a hat eindeutig den gleichen Satz an Geschäftsfeldern wie a, und wenn a und b die gleichen, gleichen Geschäftsfelder haben, und b und c auch, dann haben a und c natürlich auch dieselben . Also
BusinessObjectUtils.equals(null, null) == true
BusinessObjectUtils.equals(new TelefonNummer(), null) == false
BusinessObjectUtils.equals(new PhoneNumber(), "31 35 538 1921") == false
BusinessObjectUtils.equals("31 35 538 1921", "31 35 538 1921") == false (!)
BusinessObjectUtils.equals(new PhoneNumber(), new PhoneNumber()) == true
Beachten Sie, dass der Typ des Geschäftsobjekts bisher nicht erwähnt wurde! In der Tat ist das Folgende möglich:
class CountryAreaCodeNumberFragment extends AbstractBusinessObject {
@BusinessField
protected int countryCode;
@BusinessField
protected int areaCode;
}
@BusinessObject
public class LocalNumber extends CountryAreaCodeNumberFragment {
@BusinessField
private int number;
}
PhoneNumber xebiaClosedDialingPlan = new PhoneNumber();
xebiaSales.countryCode = 31;
xebiaSales.areaCode = 35;
xebiaSales.number = 5381921;
LocalNumber xebiaOpenDialingPlan = new LocalNumber();
xebiaLocal.countryCode = 31;
xebiaLocal.areaCode = 35;
xebiaLocal.number = 5381921;
BusinessObjectUtils.equals(xebiaClosedDialingPlan, xebiaOpenDialingPlan) == true
"Geschlossener Wählplan"? "Offener Wählplan"? Wovon redet er da?!? Siehe Dr. Wiki, aber ich schweife ab... Wenn Ihnen dieses letzte Beispiel "falsch" vorkommt, willkommen im Club. Aber es steht im Einklang mit der Idee, dass, wenn Sie Ihre Definition von Gleichheit auf Feldwerte stützen, dies alles ist, was Sie berücksichtigen sollten , sofern nicht anders angegeben. Natürlich gibt es viele Situationen, in denen dies überhaupt nicht angemessen ist.
@BusinessObject
public class Ferrari {
@BusinessField
private String-Lizenz;
}
@BusinessObject
public class Windows95 {
@BusinessField
private String-Lizenz;
}
Ferrari fxx = new Ferrari();
fxx.licence = "HL-34-F3";
Windows95 vorinstalliertOs = new Windows95();
preinstalledOs.license = "HL-34-F3";
BusinessObjectUtils.equals(fxx, vorinstalliertOs) == true (?!?)
OK, das Beispiel ist unglaublich künstlich, aber Sie verstehen schon: Oft ist es notwendig, die Typen einzuschränken, denen Ihr Geschäftsobjekt gleich sein kann.
Wer bist du, Objektfremder?
Eine Gleichheitsdefinition, die nur auf Feldwerten basiert, wäre in der Tat ein recht seltsamer Fall. Schließlich enthalten alle "kanonischen" Gleichheitsimplementierungen eine Art Instanceof-Prüfung, die die Gleichheit auf (Unter-)Klassen der Klasse, mit der verglichen wird, einschränkt1. Daher scheint ein mustBeInstanceOf-Attribut2 in der tt>@BusinessObject Annotation eine naheliegende Wahl zu sein. Bei näherer Betrachtung wirft dies jedoch einige interessante Fragen auf. mustBeInstanceOf, ja. Aber Instanz von was? Die Klasse, mit der verglichen wird? Oder der Klasse, mit der die tt>@BusinessObject Annotation definiert ist (tt>@BusinessObjectwenn auch nicht ein tt>@Erbschaft Annotation von vornherein eine transitive Semantik3)? Die Verwendung der (Laufzeit-)Klasse des zu vergleichenden Objekts ist ein absolutes No-No, da dies sehr schnell zu Symmetrieverletzungen führt. Wenn PhoneNumber mit tt>@BusinessObject annotiert ist und MobilePhoneNumber PhoneNumber erweitert, dann
- new PhoneNumber().equals(new MobilePhoneNumber()) == true aber
- new HandyNummer().equals(new HandyNummer()) == false
denn
- MobilePhoneNumber instanceof PhoneNumber == true aber
- PhoneNumber instanceof MobilePhoneNumber == false
was ein bekanntes Problem ist. Es bleibt also die Klasse, die mit tt>@BusinessObject annotiert ist. Dies löst das obige Problem(PhoneNumber und MobilePhoneNumber sind natürlich beide PhoneNumber-Instanzen ), erfordert aber einige Überlegungen beim Entwurf Ihres Domänenmodells - wenn alles einfach AbstractBusinessObject erweitert, sind sie alle miteinander vergleichbar und Sie sind wieder da, wo Sie waren, bevor wir die ganze mustBeInstanceOf-Diskussion begonnen haben.
Wählen Sie Ihre eigenen Gleichen
Wie auch immer Sie es drehen und wenden, ein mustBeInstanceOf-type-Attribut beschränkt Sie auf die Klassenhierarchie Ihrer Domänenobjekte. Das kann durchaus als gut angesehen werden (lassen Sie uns die Diskussion über die Modellierung von Domänenobjekten für den Moment zurückstellen), aber es ist auch eine verschenkte Chance. Wenn Sie sich schon die Mühe machen, eine zusätzliche "Markierung" für Ihre Domänenobjekte einzuführen, sollten Sie dann nicht auch in der Lage sein, vergleichbare Sätze von Domänenklassen aus allen Klassen mit dieser Markierung zu deklarieren? Um es kurz zu machen, tt>@BusinessObject bietet dies in der Tat, und zwar in Form eines Attributs equivalentClasses. Wenn eine Klasse als4 annotiert ist
@BusinessObject(equivalentClasses = { A.class, B.class, C.class })
public class A {
...
}
dann sind Instanzen von A vergleichbar mit Instanzen von B(b instanceof B ) und C . B und C müssen allerdings Geschäftsobjekte sein.
- a.equals(b) == b.equals(a) == true(B ist eine äquivalente Klasse für A und umgekehrt) und
- b.equals(c) == c.equals(b) == true (C ist eine äquivalente Klasse für B und umgekehrt).
// keine gleichwertigen Klassen würde "vergleichbar mit allen Klassen" bedeuten
@BusinessObject(equivalentClasses = { Parent.class })
Klasse Parent {
// Geschäftsfelder
}
// @BusinessObject-Anmerkung von Parent übernommen
class Child1 extends Parent { }
// @BusinessObject-Anmerkung von Parent übernommen
class Child2 extends Parent { }
In diesem Szenario sind die Instanzen von Parent, Child1 und Child2 sicher miteinander vergleichbar, d.h. sie sind gleich, wenn die Werte der Geschäftsfelder gleich sind. Dies gilt unabhängig davon, ob Parent eine abstrakte Klasse ist oder nicht.
"Vergleichbarer Klassenpool"
@BusinessObject(equivalentClasses = { Foo.class, Bar.class, Baz.class })
Klasse Foo {
// Geschäftsfelder
}
@BusinessObject(equivalentClasses = { Foo.class, Bar.class, Baz.class })
Klasse Bar {
// Geschäftsfelder
}
@BusinessObject(equivalentClasses = { Foo.class, Bar.class, Baz.class })
Klasse Baz {
// Geschäftsfelder
}
// @BusinessObject-Anmerkung von Baz übernommen
class BazChild extends Baz { }
Hier sind die Instanzen von Foo, Bar und Baz und BazChild ebenfalls sicher miteinander vergleichbar. Natürlich kann die Pflege der equivalentClasses-Attribute des "Klassenpools" etwas mühsam sein.
Als allgemeine Faustregel gilt: Wenn Sie eine Menge von untereinander vergleichbaren Klassen definieren und sicherstellen, dass die equivalentClasses der Mitglieder dieser Menge gleich sind, sollte alles in Ordnung sein. Wenn einige dieser Klassen von einem gemeinsamen Elternteil erben, müssen Sie eigentlich nur diesen Elternteil korrekt annotieren.
Moral: Mit großer Macht kommt große Verantwortung und eine Fülle von Möglichkeiten, die Dinge zu vermasseln, wenn Sie nicht aufpassen!
Der Preis der Freiheit - Mittagessen
Ich deklariere also meine Geschäftsfelder, und ihre Werte werden abgerufen, wenn die Objekte verglichen werden... hm, wie kommen sie wohl an die Werte?... mal sehen... (an dieser Stelle starten Sie Ihre Lieblings-IDE) oh nein, sie verwenden reflec... nun, ich wäre neugierig, wie das wohl funktionieren kann! Ich erspare Ihnen die Mühe, das herauszufinden: Die Leistung ist schlecht. Um ein Vielfaches schlechter5.
Wie zu erwarten, ist es die Reflexion (die in PropertyUtils.getSimpleProperty), die schmerzt.
Sie sollten sich also zweimal überlegen, ob Sie diese Funktion in einem leistungskritischen Kontext verwenden wollen.
Quellen
Das (gezippte) Maven-Projekt finden Sie hier. Vincenzos ursprünglicher Code, der inzwischen verfeinert und in "simplestuff" umbenannt wurde, ist bei Google Code zu finden.
- Klassen zu zwingen, gleich zu sein (im Gegensatz zu assignableFrom), ist von Anfang an eine Sackgasse, da es nicht sinnvoll mit den Proxy-Unterklassen umgehen kann, die von so vielen der beliebten Frameworks erstellt werden.
- Warum nicht einmal mit default = true? Nun, die Verwendung von Annotationen ist im Grunde ein Opt-in-Ansatz. Der Gedanke dahinter ist, dass Sie nicht relevant sind, wenn Sie nicht als @BusinessField annotiert sind. In diesem Szenario wäre es ziemlich inkonsequent, das Klassenattribut standardmäßig "magisch" einzubinden.
- Der Grund dafür, dass die @BusinessObject-Annotation nicht über @Inherited an Kinder weitergegeben wird, ist, dass die Menge der für ein Domänenobjekt relevanten Geschäftsfelder genau die @BusinessFieldssind, die in der Klasse des Objekts und allen übergeordneten Klassen bis einschließlich der ersten, die die @BusinessObject-Annotation enthält, definiert sind. Das bedeutet, dass Domänenobjekte, die Geschäftsfelder von ihren Eltern erben sollen, die Annotation nicht enthalten dürfen!
- Tatsächlich ist A.class nicht erforderlich - sofern equivalentClasses nicht unspezifiziert bleibt, wird die mit @BusinessObject annotierte Klasse automatisch zu den kompatiblen Klassen hinzugefügt (andernfalls wären die Unterklassen der Klasse nicht gleich sich selbst, was gegen die Reflexivität verstößt!) Dennoch ist es der Klarheit halber eine gute Idee, die Klasse explizit aufzunehmen.
- Gleiche Zeiten wurden durch den Vergleich von 50000 Objektpaaren ermittelt, die mit der gegebenen Wahrscheinlichkeit gleich(nicht identisch) waren. hashCode-Zeiten wurden durch die Berechnung des Hash-Codes für die Paare gemessen.
Verfasst von
Andrew Phillips
Unsere Ideen
Weitere Blogs
Contact



