Object Oriented Programming: Separation of Data and Behavior
Asked Answered
M

8

30

Recently we had a discussion regarding Data and Behavior separation in classes. The concept of separation of Data and Behaviour is implemented by placing the Domain Model and its behavior into seperate classes.
However I am not convinced of the supposed benefits of this approach. Even though it might have been coined by a "great" (I think it is Martin Fowler, though I am not sure). I present a simple example here. Suppose I have a Person class containing data for a Person and its methods (behavior).

class Person
{
    string Name;
    DateTime BirthDate;

    //constructor
    Person(string Name, DateTime BirthDate)
    {
        this.Name = Name;
        this.BirthDate = BirthDate;
    }

    int GetAge()
    {
        return Today - BirthDate; //for illustration only
    }

}

Now, separate out the behavior and data into separate classes.

class Person
{
    string Name;
    DateTime BirthDate;

    //constructor
    Person(string Name, DateTime BirthDate)
    {
        this.Name = Name;
        this.BirthDate = BirthDate;
    }
}

class PersonService
{
    Person personObject;

    //constructor
    PersonService(string Name, DateTime BirthDate)
    {
        this.personObject = new Person(Name, BirthDate);
    }

    //overloaded constructor
    PersonService(Person personObject)
    {
        this.personObject = personObject;
    }

    int GetAge()
    {
        return personObject.Today - personObject.BirthDate; //for illustration only
    }
}

This is supposed to be beneficial and improve flexibility and provide loose coupling. I do not see how. According to me this introduces extra coding and performance penalty, that each time we have to initialize two class objects. And I see more problems in extending this code. Consider what happens when we introduce inheritance in above case. We have to inherit both the classes

class Employee: Person
{
    Double Salary;

    Employee(string Name, DateTime BirthDate, Double Salary): base(Name, BirthDate)
    {
        this.Salary = Salary;       
    }

}

class EmployeeService: PersonService
{
    Employee employeeObject;

    //constructor
    EmployeeService(string Name, DateTime BirthDate, Double Salary)
    {
        this.employeeObject = new Employee(Name, BirthDate, Salary);
    }

    //overloaded constructor
    EmployeeService(Employee employeeObject)
    {
        this.employeeObject = employeeObject;
    }
}

Note that even if we segregate out the behavior in a seperate class, we still need object of the Data class for the Behaviour class methods to work on. So in the end our Behavior class contains both the data and the behavior albeit we have the data in form of a model object.
You might say that you can add some Interfaces to the mix , so we could have IPersonService and an IEmployeeService. But I think introducing interfaces for each and every class and inherting from interfaces does not seem OK.

So then can you tell me what have I achieved by seperating out the data and behavior in above case that I could not have achieved by having them in the same class ?

Medarda answered 9/7, 2012 at 6:22 Comment(2)
I've seen this attempted at least once - it fell apart under its own weight pretty quickly, not least because it's incompatible with most (maybe all) ORMs.Embroidery
Check out the justification for the strategy pattern en.wikipedia.org/wiki/Strategy_patternMotheaten
E
19

I agree, the separation as you implemented is cumbersome. But there are other options. What about an ageCalculator object that has method getAge(person p)? Or person.getAge(IAgeCalculator calc). Or better yet calc.getAge(IAgeble a)

There are several benefits that accrue from separating these concerns. Assuming that you intended for your implementation to return years, what if a person / baby is only 3 months old? Do you return 0? .25? Throw an exception? What if I want the age of a dog? Age in decades or hours? What if I want the age as of a certain date? What if the person is dead? What if I want to use Martian orbit for year? Or Hebrew calander?

None of that should affect classes that consume the person interface but make no use of birthdate or age. By decoupling the age calculation from the data it consumes, you get increased flexibility and increased chance of reuse. (Maybe even calculate age of cheese and person with same code!)

As usually, optimal design will vary greatly with context. It would be a rare situation, however, that performance would influence my decision in this type of problem. Other parts of the system are likely several orders of magnitude greater factors, like the speed of light between browser and server or database retrieval or serialization. time / dollars are better spent refactoring toward simplicity and maintainability than theoretical performance concerns. To that end, I find separating data and behavior of domain models to be helpful. They are, after all, separate concerns, no?

Even with such priorities, thing are muddled. Now the class that wants the persons age has another dependency, the calc class. Ideally, fewer class dependencies are desirable. Also, who is responsible instantiating calc? Do we inject it? Create a calcFactory? Or should it be a static method? How does the decision affect testability? Has the drive toward simplicity actually increased complexity?

There seems to be a disconnect between OO's instance on combining behavior with data and the single responsibility principle. When all else fails, write it both ways and then ask a coworker, "which one is simpler?"

Euphorbia answered 19/2, 2013 at 4:16 Comment(0)
H
19

Actually, Martin Fowler says that in the domain model, data and behavior should be combined. Take a look at AnemicDomainModel.

Hibachi answered 9/7, 2012 at 6:30 Comment(0)
E
19

I agree, the separation as you implemented is cumbersome. But there are other options. What about an ageCalculator object that has method getAge(person p)? Or person.getAge(IAgeCalculator calc). Or better yet calc.getAge(IAgeble a)

There are several benefits that accrue from separating these concerns. Assuming that you intended for your implementation to return years, what if a person / baby is only 3 months old? Do you return 0? .25? Throw an exception? What if I want the age of a dog? Age in decades or hours? What if I want the age as of a certain date? What if the person is dead? What if I want to use Martian orbit for year? Or Hebrew calander?

None of that should affect classes that consume the person interface but make no use of birthdate or age. By decoupling the age calculation from the data it consumes, you get increased flexibility and increased chance of reuse. (Maybe even calculate age of cheese and person with same code!)

As usually, optimal design will vary greatly with context. It would be a rare situation, however, that performance would influence my decision in this type of problem. Other parts of the system are likely several orders of magnitude greater factors, like the speed of light between browser and server or database retrieval or serialization. time / dollars are better spent refactoring toward simplicity and maintainability than theoretical performance concerns. To that end, I find separating data and behavior of domain models to be helpful. They are, after all, separate concerns, no?

Even with such priorities, thing are muddled. Now the class that wants the persons age has another dependency, the calc class. Ideally, fewer class dependencies are desirable. Also, who is responsible instantiating calc? Do we inject it? Create a calcFactory? Or should it be a static method? How does the decision affect testability? Has the drive toward simplicity actually increased complexity?

There seems to be a disconnect between OO's instance on combining behavior with data and the single responsibility principle. When all else fails, write it both ways and then ask a coworker, "which one is simpler?"

Euphorbia answered 19/2, 2013 at 4:16 Comment(0)
L
6

Funnily enough, OOP is often described as combining data and behavior.

What you're showing here is something I consider an anti-pattern: the "anemic domain model." It does suffer from all the problems you've mentioned, and should be avoided.

Different levels of an application might have a more procedural bent, which lends themselves to a service model like you've shown, but that would usually only be at the very edge of a system. And even so, that would internally be implemented by traditional object design (data + behavior). Usually, this is just a headache.

Lunarian answered 9/7, 2012 at 6:31 Comment(0)
H
5

I realize I am about a year late on replying to this but anyway... lol

I have separated the Behaviors out before but not in the way you have shown.

It is when you have Behaviors that should have a common interface yet allow for different (unique) implementation for different objects that separating out the behaviors makes sense.

If I was making a game, for example, some behaviors available for objects might be the ability to walk, fly, jump and so forth.

By defining Interfaces such as IWalkable, IFlyable and IJumpable and then making concrete classes based on these Interfaces it gives you great flexibility and code reuse.

For IWalkable you might have...

CannotWalk : IWalkableBehavior

LimitedWalking : IWalkableBehavior

UnlimitedWalking : IWalkableBehavior

Similar pattern for IFlyableBehavior and IJumpableBehavior.

These concrete classes would implement the behavior for CannotWalk, LimitedWalking and UnlimitedWalking.

In your concrete classes for the objects (such as an enemy) you would have a local instance of these Behaviors. For example:

IWalkableBehavior _walking = new CannotWalk();

Others might use new LimitedWalking() or new UnlimitedWalking();

When the time comes to handle the behavior of an enemy, say the AI finds the player is within a certain range of the enemy (and this could be a behavior as well say IReactsToPlayerProximity) it may then naturally attempt to move the enemy closer to "engage" the enemy.

All that is needed is for the _walking.Walk(int xdist) method to be called and it will automagically be sorted out. If the object is using CannotWalk then nothing will happen because the Walk() method would be defined as simply returning and doing nothing. If using LimitedWalking the enemy may move a very short distance toward the player and if UnlimitedWalking the enemy may move right up to the player.

I might not be explaining this very clearly but basically what I mean is to look at it the opposite way. Instead of encapsulating your object (what you are calling Data here) into the Behavior class encapsulate the Behavior into the object using Interfaces and this gives you the "loose coupling" allowing you to refine the behaviors as well as easily extend each "behavioral base" (Walking, Flying, Jumping, etc) with new implementations yet your objects themselves know no difference. They just have a Walking behavior even if that behavior is defined as CannotWalk.

Harpole answered 3/8, 2013 at 5:58 Comment(1)
Monospaced has a different example of the same approach: monospacedmonologues.com/2016/01/why-couple-data-to-behaviour.Sink
D
4

Age in intrisic to a person (any person). Therefore it should be a part of the Person object.

hasExperienceWithThe40mmRocketLauncher() is not intrinsic to a person, but perhaps to the interface MilitaryService that can either extend or aggregate the Person object. Therefore it should not be a part of the Person object.

In general, the goal is to avoid adding methods to the base object ("Person") just because it's the easiest way out, as you introduce exceptions to normal Person behavior.

Basically, if you see yourself adding stuff like "hasServedInMilitary" to your base object, you are in trouble. Next you will be doing loads of statements such as if (p.hasServedInMilitary()) blablabla. This is really logically the same as doing instanceOf() checks all the time, and indicates that Person and "Person who has seen military service" are really two different things, and should be disconnected somehow.

Taking a step back, OOP is about reducing the number of if and switch statements, and instead letting the various objects handle things as per their specific implementations of abstract methods/interfaces. Separating the Data and Behavior promotes this, but there's no reason to take it to extremes and seperate all data from all behavior.

Dublin answered 10/7, 2012 at 12:57 Comment(0)
M
3

The approach you have described is consistent with the strategy pattern. It facilitates the following design principles:

The open/closed principle

Classes should be open for extension but closed for modification

Composition over Inheritance

Behaviours are defined as separate interfaces and specific classes that implement these interfaces. This allows better decoupling between the behaviour and the class that uses the behaviour. The behaviour can be changed without breaking the classes that use it, and the classes can switch between behaviours by changing the specific implementation used without requiring any significant code changes.

Motheaten answered 13/7, 2020 at 21:23 Comment(0)
C
1

The answer is really that it's good in the right situation. As a developer part of your job is to determine the best solution for the problems presented and try to position the solution to be able to accommodate future needs.

I don't do this often follow this pattern but if the compiler or environment are designed specifically to support the separation of data and behavior there are many optimizations that can be achieved in how the Platform handles and organizes your scripts.

It’s in your best interest to be familiarize yourself with as many Design Patterns as possible rather than custom building your entire solution every time and don’t be too judgmental because the pattern doesn’t immediately make sense. You can often use existing design patterns to achieve flexible and robust solutions throughout your code. Just remember they are all meant as a starting point so you should always be prepared to customize to accommodate the individual scenarios you encounter.

Centavo answered 14/11, 2018 at 18:20 Comment(0)
G
0

When there is a single class and it does not interdependent with other class, I think we can put data and behavior together.

But if two classes are interdependent, we can separate data and behaviour.

Suppose I have two classes, Teacher and Student.

//teacher.cs
public class Teacher {
   public Student[] getStudents() {
      ...
   }
}

//student.cs
public class Student {
   public Teacher[] getTeachers() {
      ...
   }
}

We then have to separate them into for classes

// teacher.cs
public class Teacher { ... }

// teacherService.cs
public static class TeacherService {
    public static Student[] getStudents() {...}
}

// student.cs
public class Student { ... }

// studentService.cs
public class static StudentService {
    public static Teacher[] getTeachers {...}
}

in conclusion, when dealing with interdependent, separation of data and behaviour is better than combining.

Genna answered 12/4 at 9:38 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.