Perhaps it is my scripting background, but my perspective is that the purpose of an object should be to contain data. Classes are necessary in that they provide templates and standard ways to classify and construct objects with particular data attributes. Standard protocol in object oriented design seems to be to include business logic in class definitions.
In a system that depends on state, I understand the motivation behind binding methods with the sole purpose of altering or retrieving the state of an object to the class definition. So let's exclude simple getters, setters, and constructors from the scope of the conversation.
The problems, as I see them, start when processes are bound to an object.
Consider a simple "string.equals()" method that accepts a string as a parameter, and returns a boolean indicating whether strings are equivalent. I think this simple method can cause enormous problems.
Let's say I have a LocationString class that inherited from String, where equals() is overridden as follows:
Boolean equals(LocationString otherLocation) {
return extractCountry().equals(otherLocation.extractCountry());
}
String extractCountry(){
return this.getCountry();
}
Now, assume I have a thousand such different objects with different equals() definitions, some of which are the same, and some of which are vastly different. Suddenly, the definition of equality changes for some of my classes. It becomes extremely painful to work out which equals definitions need to be changed. It becomes painful to work out whether other internal processes which called the equals method need to be changed. And eventually as the system expands, more hours are wasted searching through the code than analyzing it.
As such, to me it makes far more sense that a generic equals rule is defined that modifies its behavior polymorphically depending on the inputs it receives. Consider the following definition:
public static Boolean equals(Object a, Object b) {
List locationClasses = Arrays.asList("LocationString")
if (locationClasses.contains(a.getClass().getName()) && locationClasses.contains(b.getClass().getName()){
return equals(a.extractCountry(), b.extractCountry());
}
else {
return StringUtils.equals(a,b);
}
}
If all processes were defined externally, a change in process would not be all that difficult to correct and provided these more generic rules could be stored and categorized logically, they would be easy to find and would only need to change once as the business logic changes.
This approach seems relatively simple and straight forward. Why then, has it been generally rejected in favor of the typical object oriented coupling of data with methods?
equals()
is a known design mistake; an equivalence relation needs to be reflexive (this.equals(this)
), symmetric (a.equals(b) == b.equals(a)
) and transitive (ifa.equals(b)
andb.equals(c)
thena.equals(c)
) but a subclass overridingequals()
with a stricter notion of equality breaks symmetry. In order to get a real equivalence relation you must ask both objects if they're equal, which is necessarily a static function:public static boolean correctEquals(Object a, Object b) { return a.equals(b) && b.equals(a); }
. That's how the D language does it.