
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 ourPlayer
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 thelogger
is exposed, publicly, via thelogger
keyword argument whilemessage
is an internal detail. -
When we message
#call
, we must supply a song. Internally, we usesong
as a local variable while also usingKernel#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 usingTRUE = 1
. -
2 == false
. Can be fixed by usingFALSE = 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-levelGit
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.