Orthogonality in Software

In mathematics, orthogonality refers to two lines at right angles to each other. This relation can be generalized into n dimensions. It is used to express the independence between different dimensions. It can be thought of as describing non-overlapping, uncorrelated or independent objects. This implies, an object moving along the x-axis in a three-dimensional space does not change its y and z coordinates.

Let's now look at the definitions of Orthogonality in programs found in books. The Art of UNIX Programming defines Orthogonality as:

Orthogonality is one of the most important properties that can help make even complex designs compact. In a purely orthogonal design, operations do not have side effects; each action (whether it's an API call, a macro invocation, or a language operation) changes just one thing without affecting others. There is one and only one way to change each property of whatever system you are controlling.

Programming Language Pragmatics defines it as:

Orthogonality means that features can be used in any combination, that the combinations all make sense, and that the meaning of a given feature is consistent, regardless of the other features with which it is combined.

On Lisp defines it as:

An orthogonal language is one in which you can express a lot by combining a small number of operators in a lot of different ways.

Extensible Software

Orthogonality is often used to describe modular software. You can think about systems as points in a multi-dimensional space, spawned by independent, orthogonal dimensions. It helps us to ensure that changes to one aspect of the system will not have side-effects on another. It enables us to establish a simple mental model for complex use cases. We can focus on one dimension while ignoring other aspects. It is useful in testing, because it reduces the number of combinations to test. Each functionality can be tested once in isolation. It makes combining functionality with different implementations possible. For instance, combining a shopping cart provider with a different payment processor in an eCommerce system. So, the software is designed to be extensible.

The Role of Abstraction

Abstraction is the key to developing software that is orthogonal in nature. Each dimension of an orthogonal system addresses one particular area of interest in the system. In object oriented languages, the dimension is usually represented by a class. The classes represent a dimension, while the objects represents the points within the given dimension.

Example

Let's look at an example where a person can own many credit cards, can have an address and a spouse. The orthogonal code will look like:

class CreditCard
  attr_reader :type, :expiration

  def initialize(type, expiration)
    @type = type
    @expiration = expiration
  end
end

class Address
  attr_reader :street, :state, :city, :zip

  def initialize(street, state, city, zip)
    @street = street
    @state = state
    @city = city
    @zip = zip
  end
end

class Person
  attr_accessor :address, :spouse
  attr_reader :credit_cards, :name

  def initialize(name)
    @name = name
    @credit_cards = []
    @address = 'No Address'
    @spouse  = 'No Spouse'
  end

  def add_credit_card(credit_card)
    @credit_cards << credit_card
  end

  def to_s
    "My name is #{@name}. I own #{@credit_cards.size} credit cards."
  end  
end

bugs = Person.new('Bugs Bunny')
credit_card = CreditCard.new('Visa', 'July 10, 2020')
bugs.add_credit_card(credit_card)

puts bugs

daffy = Person.new('Daffy')

puts daffy

The three dimensions in this code are credit card, address and spouse. The credit card and address are application level objects whereas the spouse is a Ruby builtin string object. We have write programs that combines these dimensions as follows:

  • A person with many credit cards, an address and a spouse.
  • A person with no credit cards, address or spouse

and so on. There are all valid combinations resulting in a valid person object in our application. We can vary these dimensions independent of each other. The person class uses composition to achieve orthogonality.

Non Example

The example for code that violates Orthogonality is shown below:

class VirtualClock
  attr_accessor :hour

  def morning?
    hour <= 12
  end
end

class FizzBuzz
  def initialize(clock, output=$stdout)
    @output = output
    @clock = clock
  end

  def transform(n)
    result = sequence
    if @clock && @clock.morning?
     result = "#{result} Morning"
    end
    result
  end    

  def sequence
    'Fizz'
  end
end

clock = VirtualClock.new
clock.hour = 9
fb = FizzBuzz.new(clock)
puts fb.transform(10)

In this case, there are two dimensions, time and output device type. We cannot vary them independently in this program. The following rewritten example shows one way to vary them independently.

class VirtualClock
  attr_accessor :hour

  def morning?
    hour <= 12
  end
end

class TimeDimension
  def initialize(clock)
    @clock = clock
  end

  def transform(n)
    result = n
    if @clock && @clock.morning?
     result = "#{result} Morning"
    end
    result
  end      
end

class FizzBuzz
  def initialize(output=$stdout)
    @output = output
  end

  def transform(n)
    result = sequence
  end    

  def sequence
    'Fizz'
  end
end

fb = FizzBuzz.new
f = fb.transform(3)

clock = VirtualClock.new
clock.hour = 9
td = TimeDimension.new(clock)

puts td.transform(f)

References

The Principles of Modular and Maintainable Design by Jens Dietrich


Related Articles


Create your own user feedback survey