Favoring Object-oriented over Procedural Code: A Motivational Example

by Zoran Horvat

Why Objects?

In this article, we will emphasize motivation to use objects over more traditional procedural coding style. We will analyze one seemingly simple algorithm, and that will lead us to seeing a glimpse of a truly object-oriented design.

Why objects? – that is the question. In their core, computers just execute instructions. Why not simply instructing them what to do? That would be the procedural coding style. Let’s command the computer! How bad can it be?

Here is a function which is procedural all the way through. It is instructing the computer what to do with some numbers.

double FindZeroProcedural(Func<double, double> f, double x1, double x2)
{
  double lower = x1;
  double upper = x2;
  double tolerance = 1e-10;

  while (upper - lower > tolerance)
  {
    double middle = (lower + upper) / 2;
    if (Math.Sign(f(middle)) == Math.Sign(f(lower)))
    {
      lower = middle;
    }
    else
    {
      upper = middle;
    }
  }

  return (lower + upper) / 2;
}

If you spent a few minutes reading this code segment, you would realize that it is finding a zero of a monotonous function f. We don’t know what that function is, and that is why we cannot just calculate where it cuts the axis. But we are promised that it is monotonous on segment between x1 and x2, and then we are asked to calculate precise point x in which the value of the function f is zero.

On a closer look, this is the algorithm known as bisection. It is cutting the interval in halves, retaining one half or the other in each iteration. In the end, the two bounds of the interval will be so close to one another that we will officially declare that the interval has converged to a single value, and that value will be the one in which function evaluates to zero.

Outlining the Weakness of Procedural Implementation

A little bit of knowledge in mathematics would be of help trying to understand what was said above. Anyway, that is of lesser importance. What really counts is that we have a stretch of procedural code and no objects on the horizon.

And I say that that is a bad situation. Why? Why is this implementation bad?

Well, for one thing, because we had to implement a programmatic function. Do you know what programmatic function is, in precise terms? It is similar to a mathematical function, in sense that it takes inputs and produces an output.

With one distinction. It is computable, and effectively computable. It must produce an output in finite number of steps. There is no bound on how many steps that would be, but the function must produce a result before the end of time.

Which brings us back to the algorithm presented above, for which we will see that it is not a true algorithm in every case imaginable. I’m playing tricks on you, and you might feel like I’m deceiving you, but listen – that is how real world around us works. It’s full of deceptions, and this implementation is one of them.

It will keep producing the result in all cases, all until our customer comes and says that it’s not precise enough. They ask for a higher precision.

Fine, we are in control, aren’t we? There is the parameter conveniently named tolerance. We can make it more stringent, like ten to power of minus fifteenth.

double FindZeroProcedural(Func<double, double> f, double x1, double x2)
{
  double lower = x1;
  double upper = x2;
  double tolerance = 1e-15;
  ...
}

That will definitely make a difference. But we are walking on a thin ice already. Whether you know it or not, the double numeric type can only accommodate some fifteen or sixteen decimal digits, depending on actual value.

Set tolerance exponent to minus sixteen, and this function will seize to be a proper programmatic function. It will never ever produce a result, no matter what inputs you give it, so long as x1 and x2 are distinct values. Its return statement will never be reached.

Morale of the story is that when you write procedural code, you may feel like you have the power to command, and you do, but listen: Commanding a computer is a two-way street. All mistakes will be yours.

Advancing to Object-oriented Implementation

And… what am I proposing? I may propose the use of objects. What will this same algorithm look like with objects?

One possibility is to start from the line segment. Then bisect the segment using the function f. And then tell the bisection algorithm to converge to function value zero.

double FindZeroObjects(Func<double, double> f, double x1, double x2) {
  Segment range = new Segment(x1, x2);
  BisectionAlgorithm algorithm = range.Bisect(f);
  double zero = algorithm.ConvergeTo(0);
  return zero;
}

The primary benefit from this coding style is that it produces code which we know to be correct. You may say that I am cheating again, that that prior bug will still be there, only buried deeper.

But think again. That “buried deeper” thing is called encapsulation. Knowledge of low-level types, such as the double numeric type, is now encapsulated in a class which is only responsible to deal with that kind of knowledge.

Such an isolated class would be simpler, and consequently easier to test. It will be easier to ensure that it works correctly in all scenarios it promises to cover. If we continued in that direction, we would inevitably arrive at the concept of composability. But that would lead us outside the scope of this article. By this point, we have reached proper encapsulation of technical responsibilities.

As the last idea in this short demonstration, I will show you how this process can be written even more concisely using the fluent interface design method. All those intermediate objects are of little interest to us. We can ignore them and simply chain the calls without explicitly setting a reference to any object.

double FindZeroFluent(Func<double, double> f, double x1, double x2) =>
  Segment.Between(x1, x2)
    .Bisect(f)
    .ConvergeTo(0);

This was just a simple motivational example. It doesn’t really tell what object-oriented programming is, and why it’s so immensely important to us. But it demonstrates with great power that objects can be used to contain complexity, to divide the problem into smaller subproblems, and then to compose partial solutions into a larger block of logic.

The Rest of the Classes

If you wonder what those Segment and BisectionAlgorithm classes are, here they are – and, as you will see, they are fairly simple. Each of the classes is dedicated to a single, limited problem, and it is solving it and exposing the solution through its public members, letting all other entities in the solution to consume their functionality through a simple method call.

class Segment: IEquatable<Segment> {
  private double Low { get; }
  private double High { get; }

  public Segment(double low, double high) {
    this.Low = low;
    this.High = high;
  }

  public static Segment Between(double low, double high) =>
    new Segment(low, high);

  public double ValueAtLower(Func<double, double> f) =>
    f(this.Low);

  public double ValueAtMiddle(Func<double, double> f) =>
    f(this.Middle);

  public Segment LowerHalf =>
    new Segment(this.Low, this.Middle);

  public Segment UpperHalf =>
    new Segment(this.Middle, this.High);

  public bool IsDivisible =>
    this.Middle != this.Low && this.Middle != this.High;

  public double Middle =>
    (this.Low + this.High) / 2;

  public BisectionAlgorithm Bisect(Func<double, double> usingFunction) {
    return new BisectionAlgorithm(this, usingFunction);
  }

  public override bool Equals(object other) =>
    other is Segment segment && this.Equals(segment);

  public bool Equals(Segment other) =>
    other is Segment segment &&
    segment.Low == this.Low &&
    segment.High == this.High;

  public override int GetHashCode() =>
    this.Low.GetHashCode() ^ this.High.GetHashCode();
}

class BisectionAlgorithm
{
  private Segment InitialSegment { get; }
  private Func<double, double> Function { get; }

  public BisectionAlgorithm(
      Segment initialSegment, Func<double, double> function) {
    this.InitialSegment = initialSegment;
    this.Function = function;
  }

  public double ConvergeTo(double value) {
    Segment currentSegment = this.InitialSegment;
    double signFactor = this.SignAtLower(currentSegment, value);

    while (currentSegment.IsDivisible) {
      currentSegment =
        signFactor * this.SignAtMiddle(currentSegment, value) >= 0
          ? currentSegment.UpperHalf
          : currentSegment.LowerHalf;
    }

    return currentSegment.Middle;
  }

  private double SignAtLower(Segment segment, double goalValue) =>
    Math.Sign(segment.ValueAtLower(this.Function) - goalValue);

  private double SignAtMiddle(Segment segment, double goalValue) =>
    Math.Sign(segment.ValueAtMiddle(this.Function) - goalValue);
}

If you compare implementation of the bisection algorithm in the BisectionAlgorithm class with the implementation from the beginning of this article, you will notice that the two implementations differ in termination condition. It appears that the second implementation, that ConvergeTo method in the BisectionAlgorithm class, has been implemented with more attention to detail. It guarantees to return the highest precision possible under the double numeric type.

Such implementation was easy to conceive once all the other responsibilities have been moved away from the core of the bisection algorithm.

Summary

In this article we have demonstrated how procedural coding style can lead to unexpected problems due to technical details which do not relate to the business problem we are addressing. Mixing technical details and domain-related code is dangerous, as it obscures the view and makes it harder to focus on what is important.

In the rest of the demonstration, you have seen how a concrete implementation can be separated into two planes – the high-level, policy plane, which is dealing with business-level abstractions; and the low-level, implementation plane, which is dealing with technical details, such as numeric precision and implementation of a numerical algorithm.

Needless to say, objects were of great use in this process, as they could encapsulate technical details behind an interface which is designed at appropriate level of details for each object in the system.

The quality of object-oriented design can be observed in encapsulation of state, encapsulation of behavior, and composition of functions.

State is encapsulated in such way that we cannot even tell whether the state is present and which objects are holding it.

Behavior is encapsulated in such way that we cannot distinguish one algorithm implementation from the other, as implementation is hidden behind a general method signature.

Finally, composition of functions was enabled through use of small objects, where each object is exposing a convenient method which it can implement.


If you wish to learn more, please watch my latest video courses

About

Zoran Horvat

Zoran Horvat is the Principal Consultant at Coding Helmet, speaker and author of 100+ articles, and independent trainer on .NET technology stack. He can often be found speaking at conferences and user groups, promoting object-oriented and functional development style and clean coding practices and techniques that improve longevity of complex business applications.

  1. Pluralsight
  2. Udemy
  3. Twitter
  4. YouTube
  5. LinkedIn
  6. GitHub