2

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?

1
  • 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 (if a.equals(b) and b.equals(c) then a.equals(c)) but a subclass overriding equals() 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.
    – Doval
    Commented Jul 9, 2014 at 20:04

5 Answers 5

2

First, what you are describing sounds quite a lot like functional approach to things. Separating data and functions operating on those data is modus operandi of all functional language. Try looking at Haskell for inspiration.

Second thing that I see is that you seem to lack concept of abstraction. If you have identified multiple classes, which have same or similar behavior, it should be your top priority to refactor this behavior into a reusable form in either inheritance or composition. This is actually the hardest part of software and OO design.

And why is separation of data and behavior frowned upon in OO design? First, encapsulation. Making sure there is only limited scope, which can access and change a state, simplifies reasoning about code, because you don't have to worry about variables changing without your knowledge. By having outside functions access this data, it means encapsulation is broken. Second are virtual methods. Virtual methods are ways to achieve late binding in OO languages. You cannot have virtual methods if they are not parts of the class.

And last thing, every time I see someone talking about OO design, I cannot help myself and link to this excersise.

And some final words: Just because some approach might seem simple and straightforward on simple and straightforward example doesn't mean it will work on complex and huge example. Try applying your approach to project which has thousands of classes with millions of functions and extremely complex business logic. You will soon realize it is far from simple.

18
  • You're right, I am proposing using more functional principles in natively imperative projects. Commented Jul 9, 2014 at 11:37
  • Regarding abstraction: The problem with relying on abstraction is that all is well and good if the system was modeled well to begin with. If not, you're working in a mess. And I have never worked on a perfectly modeled system. Commented Jul 9, 2014 at 11:39
  • Regarding encapsulation: If encapsulation is an issue, take some more cues from functional programming and make everything immutable. Commented Jul 9, 2014 at 11:40
  • Regarding virtual methods: Why do you need binding, static or dynamic, at all if the methods are separate from the data? Commented Jul 9, 2014 at 11:42
  • 1
    @Doval Major difference is manipulation of mutable state. Managing mutable state is massive challenge in big software projects. Joining state and operations allows you to limit the change of the state just to those operations. This makes design and reasoning of software much simpler task. Of course you can have only immutable state, but then, you have to approach the design from completely different perspective, using completely different tools. Functional languages with mutable state are actually extremely close to OOP and vice-versa.
    – Euphoric
    Commented Jul 9, 2014 at 21:00
1

There are two points to say about this.

  1. In some cases thinking about processes as belonging to objects makes understanding the system indeed easier. The objects become responsible for their own processes.
  2. Unfortunately most object-oriented programming languages do not support multi-methods, i.e. the dispatch is only dynamic in regard to one parameter (this).

But, of course, an object oriented approach is not always suitable for all kinds of systems. Personally, I think, it mostly pays off for transactional systems with very complicated business rules. The business logic can then be distributed across multiple classes and, assuming that the system is well designed, even if you don't understand the whole system at once, it is still possible to maintain it by working with limited number of classes (each encapsulating specific behavior) at a time.

0

To me, any given object has a sort of behavioral profile which is the domain and/or scope of varying functions it can participate in. The breadth and character of this behavioral profile will vary widely depending on the object. Object oriented design certainly has some relation with cognitive science, since it is concerned with how the programmer models the given problem space. You might consider this behavioral profile as a collection of affordances: http://en.wikipedia.org/wiki/Affordance, but over a more abstract rather than concrete domain.

With that notion on board, I would say that the decision to couple data and behavior depends on an evaluation of the domain of potential affordances for the object. Sometimes the properties of a given object are so action implying over a comparitively restricted domain that it makes sense to tightly couple the data and functionality. In other cases, the data is more abstract, and the scope of possible functions is large.

Pragmatically speaking, I definitely prefer to keep data and behavior separate if possible. I am partial to using plain data-only classes along with a healthy use of generic fluent interfaces, and especially higher order functions / monadic constructs. Thats my short answer..

0

I understand that the case of equals is just an example and that your approach is about any logic besides simple getters and setters.

I see some serious problems/limitations with what you propose:

  • That "general equals" method should reside in an utility class.

  • That class would need to be changed every time a class is added to the system ( if you will ever need to compare them )

  • Many APIs need you to overwrite equals in order for generic collections to work with your classes in sort operations. No way an APIs will look at the utility class's equals method to compare objects.

  • Not unlike the API example I gave above, plugin architectures, which allow for escalability and extensibility, would not exist as we know it. Explanation: Were your approach used in the development of, say, Firefox, then a plugin would only work if its existence were known to Firefox's developers and they added the corresponding case. Since that approach was not used, a plugin can work even when the developer of Firefox didin't know anything about such a plugin's existence. Even plugins developed after a given Firefox version was released, would work with it.

  • Also you seem to imply that every logic inside a method is "business logic". That's not the canonical definition of business logic.

2
  • 1
    +1 "Also you seem to imply that every logic inside a method is "business logic". That's not the canonical definition of business logic." This is the kind of things that one can use to identify high-reputation user. Just because some Business Logic is done in a en.wikipedia.org/wiki/Mediator_pattern class doesn't mean there is no method in the data class.
    – InformedA
    Commented Jul 9, 2014 at 17:01
  • You basically explain effects of lack of late binding/dynamic dispatch I discussed in my answer.
    – Euphoric
    Commented Jul 9, 2014 at 18:33
0

my perspective is that the purpose of an object should be to contain data.

Yes, but that's not all.

Standard protocol in object oriented design seems to be to include business logic in class definitions.

Not protocol. It's central to Object Orientation. Data + Operations.

The problems, as I see them, start when processes are bound to an object.

You are touching on a central issue - what Data and what operations belong in a given class? As a general guideline I want operations that are "atomic" - I can't think of a better word - to that class.

Consider a simple String.Equals()

You are not thinking object oriented!

// this is C#
public class Location {
    public string Country { get; set; }

    public override bool Equals(object other) {
        if(other == null)
            return false;
        if(! other is Location)
            return false;

        return this.Country == other.Country;  // we're taking advantage of String.Equals(). cool.
    }
}

// sample use
// Given: Odessa, OdessaUSA, OdessaRussia are Location objects.

if(Odessa.Equals(OdessaUSA))
    Console.WriteLine ("Hot damn! I'm in Texas");

if(Odessa.Equals(OdessaRussia))
    Console.WriteLine ("Hot damn! Cheap Vodka");

... And eventually as the system expands, more hours are wasted searching through the code than analyzing it.

The way you're presenting it, I agree. This is a good example of why you'd want to write Object Oriented.

it makes far more sense that a generic equals rule is defined that modifies its behavior polymorphically depending on the inputs it receives.

No, this is anathema to Object Orientation. Each class has it's own "equals" definition. Any changes affect objects of that class only. It does not affect every string property, or equals method, of every class. An essential characteristic of a Class is that it encapsulates it's own data and operations. Changes to one class do not affect another.

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.

All objects of the same class have the same equals() definition. In OO you cannot compare objects of different classes - i.e. types. It makes no sense any more than trying to perform equals on a string to a numeric.

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.