The letter A styled as Alchemists logo. lchemists
Published November 1, 2023 Updated November 1, 2023
Cover
Connascence

In What Every Programmer Should Know About Object-Oriented Design by Meilir Page-Jones, the third and last part of the book defines connascence as the birth of two or more components which evolve together. This creates a coupling which ends up having varying levels of complexity as defined by Wikipedia:

In software engineering, two components are connascent if a change in one would require the other to be modified in order to maintain the overall correctness of the system.

Connascence — pronounced: con-nay-sense — encourages good design through loose instead of tight coupling. You can’t eradicate coupling entirely but there are multiple ways to significantly reduce the burden. This is why knowing what connascence is and how to reduce it helps produce a robust architecture. This article will give you the vocabulary for deeper thought.

Static

The static category of connascence refers to dependencies between components determined at compile or build time. These dependencies can be analyzed through static code analysis where any change requires recompiling or rebuilding to propagate.

Name

Connascence of Name (CoN) occurs when two components must agree on the same name of an entity. CoN is the lowest and weakest form of connascence. CoN is also unavoidable because we must give all components a name — objects, methods, variables, and so forth — in order to implement a solution. Here’s a basic example:

module Music
  def self.play = puts "Playing music..."
end

Music.play
# Playing music...

We can use the above implementation to output the playing of music by messaging Music.play. Should the module and/or method name change, we’d have to refactor all of our code to deal with the name change. We can take this a step further by using the Cogger gem to log the song being played:

require "cogger"

module Music
  class Player
    MESSAGE = %(Playing "%<song>s"...)

    def initialize message = MESSAGE, logger: Cogger.new
      @message = message
      @logger = logger
    end

    def call(song) = logger.info { format message, song: }

    private

    attr_reader :message, :logger
  end
end

player = Music::Player.new
player.call "Astronomica"

# 🟢 Playing "Astronomica"...

A few points to note with the above implementation:

  • We’ve introduced a Music namespace to organize our Player class.

  • We’ve given the default message a name via the Music::Player::MESSAGE constant.

  • We’ve injected the default message, via the initializer, and given it an instance variable name of @message. This includes injection of the logger via the @logger instance variable. Only the logger is exposed, publicly, via the logger keyword argument while message is an internal detail.

  • When we message #call, we must supply a song. Internally, we use song as a local variable while also using Kernel#format to format the song for output.

None of the above is cause for concern but we are using a handful of public and private names. Should the public API of this object change like Music::Player#call or the injection of the logger via Music::Player#initialize, then dependent objects would need updating.

Internally, we use message and song for logging song information and those names are much easier to change since none of the names would break dependent objects due to being highly localized.

Another potential cause for breakage is use of Kernel#format because if that method name changed, we’d have to update the Music::Player#call implementation accordingly. Chances are low since this Ruby core language feature is not likely to change anytime soon.

All of this is to say that names are everywhere and important to use in a manner that is self-describing and intuitive to understand.

Type

Connascence of Type (CoT) occurs when two components must agree on the type of an entity. In static languages, the compiler will catch and ensure type safety but in a dynamic language, like Ruby, we can use duck typing. Consider the following:

# Checks object type.
object.is_a? String

# Checks method type.
object.respond_to? :size

Notice we are checking for specific types which makes the code more brittle. We can avoid CoT through duck typing to reduce complexity. For example, if we wanted to obtain the size of different objects, we could do this:

def print_size(object) = puts object.size

print_size "demo"      # 4
print_size [1, 2, 3]   # 3
print_size a: 1, b: 2  # 2

While the above is a decent example of duck typing, this isn’t full proof as you’d need any object passed to the print_size method to respond to the #size message. So there are tradeoffs but avoiding complicated type checking logic or having to constantly update your type checking logic with new types reduces CoT complexity.

A more insidious form of CoT is when repeated in the name. The Ruby on Rails framework is the worst offender in this regard. Example:

# No
UserController
ProductController

UserJob
ProductJob

# Yes
Controllers::User
Controllers::Product

Jobs::User
Jobs::Product

The solution, as you can see, is to use modules because they are perfect for organizing components without constantly repeating yourself or losing meaning (more on this shortly). A slight variant — in an effort to emphasize this further — is when the CoT infects variables. Example:

demo = Demo.new

# No
demo_class = Demo
demo_instance = demo

# Yes
demo        # Instance
demo.class  # Class

People, falsely, believe you need to inject or pass a dependency as both class and instance when you shouldn’t be concerned about type at all, only the messages. The above solution is one remedy but the main question to ask yourself is what is the missing behavior you need to expose? Can you resolve this without caring about type? If so, great, because you can reduce down to CoN instead of being stuck with CoT. As a bonus, your tests will be easier to maintain too!

Meaning

Connascence of Meaning (CoM) — sometimes referred to as Connascence of Convention (CoC) — occurs whenever two components must agree on the meaning of specific values. Magic numbers — as a code smell — is a good example of ambiguity in the meaning of a value. For example, the following uses integers 1 and 2 to represent booleans:

  • 1 == true. Can be fixed by using TRUE = 1.

  • 2 == false. Can be fixed by using FALSE = 2.

When defining TRUE and FALSE constants, we reduce CoM to CoN complexity so the code is more self describing.

Another example in which meaning is abused — which, unfortunately, happens a lot in the Ruby core objects — is Boolean Parameter Control Coupling. For instance, consider the following:

# Includes ancestor methods.
Object.instance_methods true

# Excludes ancestor methods.
Object.instance_methods false

The fact that we have to use comments to explain the above is a clear indicator of CoM and could be easily solved by introducing a #ancestor_instance_methods method without the boolean confusion. To illustrate how far this can get out of hand, take the PStore default gem which has a confusing Object API:

# When true, ensures thread safety. When false, is not thread safe.
store = PStore.new "demo.store", true

# When true, enforces read-only mode. When false, enabled read and write.
store.transaction(true) { puts store[:key] }

Due to the above confusion, I created the Lode gem which enhances and wraps the PStore Object API in a more intuitive and self describing manner.

Algorithm

Connascence of Algorithm (CoA) occurs when two different objects/methods must agree on same algorithm. This can be solved by refactoring to a common object/method which reduces the degree of complexity down to CoN. When resolved, CoA ends up being similar to Don’t Repeat Yourself (DRY) refactorings.

CoA is easier to solve when in the same language/codebase but becomes complex when repeated across multiple languages/codebases. For example, OAuth is a complex authorization specification which requires having a client and server implementation.

For example, consider the following, contrived but simple, encryption example:

require "digest"
require "pathname"

class Encrypter
  def initialize path = Pathname("demo.txt")
    @path = path
  end

  def call(value) = path.write Digest::SHA2.hexdigest(value)

  private

  attr_reader :path
end

Notice SHA2 encryption is tightly coupled to the Encrypter which happens to write to a file. You might not want this specific behavior but might need the same encryption algorithm. Unfortunately, you’d need to use Digest::SHA2 in multiple places which increases CoA through duplication. Here’s a quick refactoring:

require "digest"

class Encrypter
  def initialize cipher: Digest::SHA2
    @cipher = cipher
  end

  def call(value) = cipher.hexdigest(value)

  private

  attr_reader :cipher
end

Encrypter.new.call "demo"
# 2a97516c354b68848cdbd8f54a226a0a55b21ed138e207ad6c5cbb9c00aa5aea

With the above, we’ve encapsulated our encryption algorithm to a single Encrypter object with safe defaults. As a bonus, the cipher can be swapped out with a different algorithm if required. This simplifies testing since we can swap out the cipher with a spy since we only care encryption is performed but not how it’s calculated since we can trust the Digest library to have sufficient testing.

Position

Connascence of Position (CoP) occurs when multiple components must agree on the order of values and definitely gets uncomfortable with more than three values. CoP can be reduced to CoN via keyword arguments, using a Hash, using a Struct, or using a Class depending on degree of behavior. Examples of CoP are:

  • Unstructured lists.

  • Method parameters.

Structs provide an excellent way to demonstrate mixing order and unordered arguments as well as the impact of Method Parameters and Arguments in general. Consider the following:

Name = Struct.new :first, :middle, :last

# Yes.
Name["Zoe", "Alleyne", "Washburne"]
#<struct Name first="Zoe", middle="Alleyne", last="Washburne">

# No (order matters).
Name["Alleyne", "Washburne", "Zoe"]
#<struct Name first="Alleyne", middle="Washburne", last="Zoe"

Thankfully, with Structs, we can use keyword arguments to avoid CoP via the lesser cost of CoN:

Name[middle: "Alleyne", last: "Washburne", first: "Zoe"]
#<struct Name first="Zoe", middle="Alleyne", last="Washburne">

As you can see, despite the order in which the keyword arguments were used, we get a proper first, middle, and last name regardless of position. In this case, we don’t have to refactor our code to support keyword arguments to avoid CoP. Otherwise, you’d have to.

Dynamic

The dynamic category of connascence refers to dependencies determined at runtime between components. Any change to these components propagate immediately without the need for recompilation. Additionally, changes are much harder to analyze through static techniques and can lead to runtimes errors if broken.

Execution

Connascence of Execution (CoE) occurs when the order of execution is important for multiple components. Examples, which reduce CoE, are:

  • Ordered queues which process items in a specific order.

  • Stacks which push and pop operations.

  • State machines.

Here’s a few simple examples:

# Failure: Email must be generated before being published.
article.publish
article.generate

# Failure: Report format must be set before being generated.
report.generate
report.format = :csv

Here’s a concrete example. Consider the following implementation:

total = Struct.new(:value).new 10
adder = -> value, store: total { store.value += value }
multiplier = -> value, store: total { store.value *= value }

Assuming total is reset before each execution of operations, notice we get different results:

# First pass.
adder.call 2
multiplier.call 5
total.value        # 60

# Second pass.
multiplier.call 5
adder.call 2
total.value        # 52

The Transactable gem, for example, helps alleviate the CoE burden by encapsulating order of operations within a single pipe (or multiple pipes if you need to compose them).

Timing

Connascence of Timing (CoTime) occurs when the timing of execution is important for multiple components. Examples:

  • Race conditions in concurrent programming.

  • Dealing with timeouts when talking to hardware or slow network connections.

CoTime is similar to CoE especially in terms of parallel processing. Here are a few examples:

# Server times out after 60 seconds.
server = Server.new
sleep 61
server.request  # Request timed out.

# Cache expires after 60 seconds.
cache.write key, data
sleep 61
cache.read key  # Expired, answers nil.

Values

Connascence of Values (CoV) occurs when the values of two components are related. The good news is CoV is rarely widespread and tends to be more localized to the class or method that needs to work with multiple values at once. Here are a few examples:

# Database configuration is coupled to environment value.
database_url = ENV.fetch "DATABASE_URL"
configure database_url

# The same value is duplicated across classes.
class DemoOne
  DEFAULT_SIZE = 100
end

class DemoTwo
  DEFAULT_SIZE = 100
end

# 0.5 means 50% for this method but is $0.50 in other methods.
discount_by 0.5

Identity

Connascence of Identity (CoI) occurs when two components must reference the same entity.

person_one = Person.new name: "Bill"
person_two = Person.new name: "Bill"

# False because they are different objects.
person_one == person_two  # false

# Mutation means the original bag has changed and no longer has the same content.
bag = [1, :b, "c"]
bag.append 4.4

Notice, in both examples above, we have CoI due to creating multiple objects with different different object IDs (first example) and mutating the same object with different values (second example). The first can be solved by constantizing and/or freezing while the second would benefit from being a Whole Value Object where identity is determined by the values and not object ID.

💡 If you need identity by value instead of by object ID, then the generic Wholable or specific Versionaire gems might be of interest.

Contranascence

Occurs when two components must agree on different names or when these names collide and can be either static or dynamic. Examples:

  • With Ruby Gems, you can’t use the same gem name because all gems exists in the global namespace. You can introduce your own namespace which wraps or provides an alternative to a namespace already occupied by a gem. For example, you could use MyNamespace::Git to avoid conflicts with the top-level Git namespace owned by the gem of the same name.

  • When monkey patching Kernel with a #my_custom_method method — instead of using Refinements — because nearly all objects in the application will inherit this behavior.

Properties

Connascence is comprised of three properties: strength, degree, and locality. Knowing what each are and how they interacts with each other determines how tight or loose your coupling is.

Strength

From Wikipedia:

A form of connascence is considered to be stronger if it is more likely to require compensating changes in connascent elements. The stronger the form of connascence, the more difficult and costly it is to change the elements in the relationship.

For example, CoN is the weakest of all forms because refactoring an object’s name, method, variable, and so forth is trivial using modern Integrated Development Environment (IDEs). CoM, on the other hand, is harder to decouple because no IDE can help extract meaning from ambiguous values so you have to manually refactor the code, on a case-by-case basis, to properly give definition to confusing code.

Degree

From Wikipedia:

The acceptability of connascence is related to the degree of its occurrence. Connascence might be acceptable in limited degree but unacceptable in large degree. For example, a function or method that takes two arguments is generally considered acceptable. However, it is usually unacceptable for functions or methods to take ten arguments. Elements with a high degree of connascence incur greater difficulty, and cost, of change than elements that have a lower degree.

Put plainly, the degree of occurrence is worse when 100 components are effected versus only 1 component. While you can’t absolve yourself of complete connascence, you always want to refactor down to a lower degree of connascence whenever possible.

Locality

From Wikipedia:

Locality matters when analyzing connascence. Stronger forms of connascence are acceptable if the elements involved are closely related. For example, many languages use positional arguments when calling functions or methods. This connascence of position is acceptable due to the closeness of caller and callee. Passing arguments to a web service positionally is unacceptable due to the relative unrelatedness of the parties. The same strength and degree of connascence will have a higher difficulty and cost of change, the more distant the involved elements are.

In general, you want to increase locality. A fair amount of locality within a module, class, and/or method is acceptable versus having this knowledge spread throughout your application in other components. Being able to see the coupling directly is better than being obscured through indirect, or multiple references, throughout your application.

Measurement

Connascence — in addition to being a vocabulary for discussing coupling — gives us a way to measure complexity. In terms of strength, the measurement scale goes from weakest (easiest to refactor) to strongest (hardest to refactor) which mirrors the structure of this article:

  • Static

    • Name

    • Type

    • Meaning

    • Algorithm

    • Position

  • Dynamic

    • Execution

    • Timing

    • Value

    • Identity

With the above in mind, we can distill this into a single formula when calculating code complexity:

(<strength> * <degree>) / <locality> = <connascence>

If we use a range of 1..3 where 1 is the lowest value and 3 is the highest value, we can run quick calculations on a few examples starting with CoN.

# Strength: 1 (only one component).
# Degree: 1 (only one component).
# Locality: 3 (highly local due to being isolated to the module).
# Equation: (1 * 1) / 3
# Connascence: 0.33
module Music
  def self.play = puts "Playing music..."
end

Had we not used self, our connascence would have been slightly worse:

# Strength: 2 (the module name is repeated twice instead of using `self`).
# Degree: 1 (only one component).
# Locality: 3 (highly local due to being isolated to a single module).
# Equation: (2 * 1) / 3
# Connascence: 0.66
module Music
  def Music.play = puts "Playing music..."
end

Switching to CoM — and using the same example as discussed earlier — we have the following complexity when using PStore default functionality:

# Strength: 3 (effects multiple components because "true" has difficult meaning).
# Degree: 3 (effects multiple components because "true" has difficult meaning).
# Locality: 1 (due to being used by other components).
# Equation: (3 * 3) / 1
# Connascence: 9
PStore.new "demo.store", true

On the flipside, the same code has low CoP because the first argument is always required while the thread safe boolean parameter is optional. So the CoP calculation looks more like this:

# Strength: 1 ("true" is optional and can be easily added as a second argument).
# Degree: 1 ("true" is optional).
# Locality: 2 (components may or may not use this argument).
# Equation: (1 * 1) / 2
# Connascence: 0.5
PStore.new "demo.store", true

All of the above should be taken with a grain of salt since there is subjectivity at play here. Also the 1..3 measurement range could be 1..5 or 1..10 depending on specificity desired.

Conclusion

Given all that has been discussed, connascence shouldn’t be taken as an official mandate to adhere to and avoid at all costs because connascence isn’t always bad. As mentioned earlier, CoN is the most common and isn’t always meant to be avoided due to being the weakest to work with.

In the end, connascence gives us a vocabulary to further discussion and find better ways to reduce the coupling of our code. This lowers our maintenance cost while giving us a code base that is enjoyable to work with.