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
Musicnamespace to organize ourPlayerclass. -
We’ve given the default message a name via the
Music::Player::MESSAGEconstant. -
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@loggerinstance variable. Only theloggeris exposed, publicly, via theloggerkeyword argument whilemessageis an internal detail. -
When we message
#call, we must supply a song. Internally, we usesongas a local variable while also usingKernel#formatto 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 foolproof as you’d need any object passed to the print_size method to respond to the #size message. There are tradeoffs but avoiding complicated type checking logic and not updating your type checks reduces CoT complexity.
CoT is most insidious 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"
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 Pipeable 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 Wholeable 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::Gitto avoid conflicts with the top-levelGitnamespace owned by the gem of the same name. -
When monkey patching
Kernelwith a#my_custom_methodmethod — 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
Strength is determined by the work required to compensate for changes applied to downstream objects. As the difficulty increases, so does the strength which increases the cost of the change to be applied. 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
Degree is determined by the number of occurances you must fix in order to reduce the complexity of your code. For example, a method with three parameters is generally accepted as the maximumn number parameters to use before needing to abstract to a new object but a method with ten parameters vastly increases the complexity especially if you have to apply the change across a high number of downstream objects that would break otherwise.
Locality
Locality is how close or far the elements of your implementation are. A simple example is when using positional parameters for private methods. The knowledge of these positions is highly localalized and you have everything you need, within a single object, to make changes without effecting objects elsewhere in your architecture. On the other hand, expanding positional knowledge across multiple objects would increase complexity and make your code less flexible by forcing the reader to decipher what positional parameters are requried and in what order they should be used. Plus, positional parameters tend to be less flexible than keyword parameters so now you’ve made your Object API less flexible as well.
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 across multiple 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.
Recap
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 you a vocabulary for further discussion and finding better ways to reduce the coupling of your code. This lowers maintenance cost while giving you a code base that is enjoyable to work with.