9/18/2020 by jdabulis

State and Behavior in OOP: A Frequently Unasked Question

The State/Behavior Distinction

 

In the OO paradigm, an object has the state and behavior defined by its class, and these are clearly distinguished.  Consider the following C# class definition:

public class Car {
       public float Speed;
       
       public void Accelerate(float metersPerSecond, int seconds) {
           Speed += (metersPerSecond * seconds);
       }
   }

Here, we have a class Car with one exposed "field" (i.e. state variable) and one method. The Speed of Car objects can be changed directly, or through the Accelerate() method:

Car car = new Car();
car.Speed = 15;
car.Accelerate(10.0f, 10);

 

However, notwithstanding the fact that in C#, functions are first-class objects, Accelerate() is not a field just like Speed in that I cannot assign it some new "value" (i.e. a pointer to a different function) even though it is public.

car.Accelerate = someOtherFunction;     //the compiler calls "bullshit"
car.Accelerate(10.0f, 10);              //supposed to call someOtherFunction instead.

Contrast this with assigning a function to a property in Javascript:

function Car() {  //class
 var speed = 0.0;
 
 this.Accelerate = function(acceleration, seconds) {
  this.AddSpeed(acceleration * seconds);
 }
 
 this.GetSpeed = function() {
  return speed;
 }
 
 this.AddSpeed = function(newSpeed) {
  speed += newSpeed;
 }
}

//usage example:
var car = new Car();
car.Accelerate(10, 10);

console.log("Speed 1: " + car.GetSpeed());
car.Accelerate = function(acceleration, seconds) { }

car.Accelerate(5, 10);
console.log("Speed 2: " + car.GetSpeed());

 

In Javascript a function serves the same role as a class in other common OO languages.  It is the function in Javascript that has properties and methods.

In C#, I could define Accelerate as a delegate (Action<T1,T2>):

   public class Car {
       protected float speed = 0.0f;
       public Action<float, int> Accelerate { get; set; }
       public Car() {
           Accelerate = (acceleration, seconds) => {
               speed = acceleration * seconds;
           };
       }
       public float GetSpeed() {
           return speed;
       }
   }

I've given Accelerate() a "default" implementation, assigned in the constructor.  Now, the implementation of Accelerate() can be replaced by assignment, just as in Javascript:

Car c1 = new Car();
var speed = Accelerate(10.0f, 20);
...
c1.Accelerate(10.0f, 20);
var x = c1.GetSpeed();

c1.Accelerate = (a, b) => { };      //now Accelerate() does nothing.
c1.Accelerate(10.0f, 20);
x = c1.GetSpeed();

 

History of Encapsulation and Behaviors

Object-oriented thinking began in the 1960s with a language based on ALGOL called "Simula."  Simula was invented to better model simulations in which events may occur non-deterministically.  It thus became possible to send a message to an object such that the message could itself be an object with its own state.  The ability to easily implement event-driven applications became especially important with the advent of GUIs.  Smalltalk then followed and was based on Simula.  

The key concept of OOP as envisioned by Alan Kay and realized in Smalltalk is *message passing*.  An object sends messages to other objects.  These messages may *answer* a return value, typically the *receiver*(the object that received the message) so that "calls" can be chained:

|inAnHour|
inAnHour := DateTime now
 addHours: 1;

 

A message is a side effect, and its single purpose is to cause changes. Messages would be useless if they couldn't mutate the state of other objects.  It is impossible to make use of OOP without causing state

mutations.  To get the state of an object, you send it a message that answers the value.

Most strongly typed object-oriented languages which came later such as C++, Java, Python, Objective-C and C# borrowed the notion of distinguishing behavior (methods or messages) from state (or data) within a class.  

This is the key feature that is "added" to procedural programming languages to make them object-oriented.  Without encapsulating behavior and state within a class, one is left to work with disparate structs, variables, and functions with global scope.  In languages such as C and Pascal, it is common to see function naming and signature conventions used to achieve a similar, if compiler unenforced, effect.

In Python, for example, you define instance methods with the receiver (i.e. self) explicitly defined as the first argument of the signature.  This gives the impression that a class is just a namespace containing a set of ordinary (i.e. procedural-style) functions to which expressions of the form: 

obj.method(arg1)

is syntactic sugar for something like method(obj, arg1).

Of course, a class does not merely serve as a namespace in an object-oriented programming language. They typically support a hierarchy of classes (inheritance), method overloading and static or dynamic method binding (polymorphism).

Accessors

Let's refine the Car class to have two fields, Id and Description declared with get/set accessors.  As a language in the lineage of C, C# gives us the ability to expose these instance variables without doing so (i.e. declaring them as public variables).  However, many developers will consider this bad practice because interfaces cannot contain instance variables (i.e. fields).

   public class Car {
       public int Id { get; set; }
       public string Description { get; set; }
       protected float speed = 0.0f;
       public Action<float, int> Accelerate { get; set; }
       public Car() {
           Accelerate = (acceleration, seconds) => {
               speed = acceleration * seconds;
           };
       }
       public float GetSpeed() {
           return speed;
       }
   }

 

The same is true for Java, in which interfaces can declare only methods and constant-valued fields. While properties (i.e. accessors) in C# may not look like methods as is apparent in Java, they are. Therefore, we can say that if popular programming practices are followed in an object-oriented programming language, classes expose only a set of methods-- not a combination of variables and methods.

Design Implications

 

In this article, I introduced the unusual practice of defining a class such that its methods are declared as delegates, each of which is assigned an implementation in the constructor.  Then, we saw that we can invoke instance methods on objects of such classes in the same manner as conventionally implemented classes.

Suppose we want to subclass Car, defining a Racecar.  Let's say a Racecar accelerates twice as fast as a base Car:

public class Racecar : Car {
   public Racecar() {
       Accelerate = (acceleration, seconds) => {
           speed = acceleration * 2 * seconds;
       };
   }
}

Notice that Accelerate is not a <code>virtual</code> member of Car and so override is neither permitted nor required in Racecar.

With this insight, we could flatten the class hierarchy by introducing a property that replaces the implementation of methods:

public class Car {
   public int Id { get; set; }
   public string Description { get; set; }
       
   protected float speed = 0.0f;
   private bool _racecar;

   public Action<float, int> Accelerate { get; set; }
       
   public bool IsRacecar {
       get { return _racecar; }
       set {
           if (value) {
               Accelerate = (acceleration, seconds) => {
                   speed = acceleration * 2 * seconds;
               };
           } else {
               Accelerate = accelerate;
           }
       }
   }

   public Car() {
       Accelerate = accelerate;
   }

   public float GetSpeed() {
       return speed;
   }

   private void accelerate(float a, int s) { //default implementation
       speed = a * s;
   }
}

You might think that by introducing a level of indirection by means of this "method as delegate" creational pattern, you gain some clear advantage over the customary way of defining classes.  The behaviors  (methods) have themselves become state.  But upon reflection (no pun intended), since both properties (accessor methods) and (regular) instance methods are functions, the apparent state/behavior distinction was illusory to begin with.

 

Summary

Both traditionally and conceptually in popular object-oriented languages, classes can be defined to have only State (data structures-- the so-called AnemicDomainModel by Martin Fowler), only Behaviors (stateless "services"), or both State and Behaviors.  We've seen, however, that State (via Properties/Accessors) is really Behavior, trivial as this behavior may be.  Classes can be thought of small programs, and each object as an running instance of a program.  A program that doesn't produce side-effects doesn't DO anything.

This article isn't about what to think about OO vs Procedural vs Functional programming.  After all, you can use any Turing-complete language with adequate access to machine resources to solve any computable problem.  Object-oriented programming languages are the most popular today and the organization (data and logic) of the programs you write will be driven by the features and idioms of that languages.  Thus, continued use and mastery of Java or C# inexorably leads to the perception that object-oriented analysis and design is the most natural and comfortable, and therefore most appropriate for any given problem.

But what you think "object-oriented" programming is, depends on the object-oriented language you choose because a language has direct support for a set of abstractions.  Smalltalk with its multiple inheritance and lack of interfaces feels a lot different than Java.  Java is a lot different than Javascript and its duck typing.  You'll find C# programs peppered with many constructs from functional programming like closures. Python, Scala, PHP-- the list is long, and the differences are not just a matter of syntax-- a language often forces you to think about (*i.e.* analyze) a problem in a particular way.

It's useful to be familiar with a variety of languages or language families.  Even as you're more or less forced to implement an application in a particular language.  As you're designing and coding the solution, your mind will be drawn to how you could have solved it in some other language.  This may lead you to switch to that other language if possible, or emulate a feature of that language in the one you're stuck with.

© Lognosys LLC. All rights reserved.