Help! My Code Gained Connascence!
What should you do if you think your program has gained connascence? That's right, connascence. Not conscience!
What even is connascence? Why should I know about it?
Well, lets break this down. Big words are can be murky and a bit challenging to grasp, especially when they're pulled out during a meeting, or worse, an interview! However, the ideas behind this particular big word are really helpful.
Most of the definitions in this post either come from connascence.io, an Alchemists post on Connascence, or from the Wikipedia page.
Just tell me what it means!
In my own words, connascence is a way of describing the complexity caused by dependency relationships in object oriented programming.
An example is, the number of changes you need to make if you change the interface of one object in your system. If you need to make lots of changes across the system, you have high connascence, but if you only make one or two changes, you have low connascence.
Why does this matter?
First off, don't be that person that says "this has a connascence of position" to someone without first explaining what you mean!
Knowing what connascence means helps us break down software complexity and pick strategies for making things easier to maintain.
There are nine types of connascence--separated into two categories--and three properties. This nomenclature allows us to identify complex dependency relationships and understand how challenging they may be to untangle.
Properties of Connascence
The three properties below can help you describe how challenging it will be to resolve the complexity of your code.
Strength
No one wants strong connascence! This is tricky to fix because it is hard to identify and hard to fix. You'll notice that the order of the types of connascence in this blog post match almost every other post on the subject. That's because this order is considered to be ordered from easiest to hardest to fix.
Locality
Locality can hep you predict where to find connascence. Typically, you would expect code in the same class, module, or function to have an interrelated complexity. The further apart code is, the less related complexity you can expect.
But what if you find complexity far away? For example, class A calls class B which triggers a job that calls class C. Complexity that is distant from the source can be very challenging to identify and fix.
Degree
Degree is a way of saying how bad it is. Are we talking about one entity coupled to another or one entity coupled to 500 others? A high degree of connascence often results in:
And we don't want that.
Types of Connascence
Static Connascence
Static connascence can be found by reading and reasoning about the code. Sometimes code linters can help us find static connascence.
Connascence of Name (CoN)
This is an easy one! We all know that multiple entities in a system need to agree on the name of a method or function. How would code work otherwise! Here's an example in Ruby:
class Product
def delete!
# execute the SQL you need here
end
end
Let's say you wanted to change the delete!
function so that it only hides a given item instead of deleting it. You should rename delete!
to archive!
so that things are clear. Since a few other objects might call the Product#delete!
method, you'll need to search and replace all calls to delete!
Another way of doing this is to make delete!
an alias of archive!
, however, that can get very confusing! If you are going to do that, it's best to put a deprecation warning on the delete!
method so that you can trace all calls down and change them to archive!
.
Connascence of Type (CoT)
Connascence of type happens when entities need to agree on the type of another entity. You might hear "type" and think, aha! Typescript must solve this. But that's not quite correct. Typescript helps us find connascence of type when we pass the wrong type or change types, but it doesn't make connascence of type go away.
I'm going to stick to Ruby here, so that we don't have to switch to thinking in another language. Here's an example of connascence of type in Ruby:
class Product
def add_to_category(name_or_id)
if name_or_id is_a?(String)
c = Category.find_by(name: name_or_id)
update(category: c)
elsif name_or_id is_a?(Integer)
update(category_id: name_or_id)
else
raise "Incorrect name_or_id provided"
end
end
end
In this example callers of add_to_category
need to know that the argument can only be a String
or an Integer
. If we want to pass an instance of a Category
instead, we'll need to update the method to work correctly with that type.
Connascence of Meaning (CoM)
Connascence of meaning happens when developers have to choose variable names and then call methods on that variable. Each developer might choose a slightly different word with similar meaning.
Here's an example of what that might look like:
class Product
attr_accessor :category_id
def initialize(category)
@category = category
end
end
class Category
attr_accessor :name
def initialize(name)
@name = name
end
end
# Usage
pants_category = Category.new("Pants")
pants = Product.new(pants_category.name)
puts pants.category_id # Outputs: "Pants"
The two classes must agree that the string "Pants"
is a category, but there is no shared data between the Category
and the Product
that helps us identify that. category.name
and pants.category
are variables with different names but, surprisingly, can have the same meaning.
Connascence of Position (CoP)
Oh! Remember my post on Weird Method Signatures in Ruby with Keyword Arguments? Keyword Arguments in Ruby comes out of a need to solve for connascence of position.
Imagine a method signature that looks like this:
class Product
def add_to_cart(cart_id, customer_id, store_id)
# Implement cart logic here
end
end
Callers of these methods need to know the right position of each argument. If they pass a store_id
in as the first argument, bad things will likely happen! This can get even more confusing when some arguments are optional:
class Product
def add_to_cart(cart_id, customer_id=nil, store_id=nil)
# Implement cart logic here
end
end
You might call add_to_cart
like so:
my_product.add_to_cart(1, nil, 4)
Eeep! What is that second argument? Why do we have to pass a nil
? If we don't pass nil
there, there is no way for the program to know which argument the 4
was meant for!
Connascence of position can be a bit tricky to solve because you need to track down all the callers and update them. Keep your argument list short, please! And use keyword arguments whenever you can.
Connascence of Algorithm (CoA)
Connascence of algorithm is a neat one. You know how when you connect to most (I hope, all???) websites, you connect to https://
? Your browser and the remote server have to agree on an encryption algorithm so that they can communicate.
In day-to-day software applications, you'll see this when we need to encrypt and decrypt data. If two entities need to read and write the same data, they'll need to use the same encryption algorithm to do so.
Dynamic Connascence
Dynamic connascence is harder to spot because it only surfaces during runtime. Although, I'd argue that we might be able to identify some of these forms of connascence with good entity relationship diagrams and sequence diagrams.
Connascence of Execution (CoE)
This happens when things need to happen in a specific order to get the right outcome. Connascence of Execution is especially prevalent in distributed systems where separation of responsibilities is not clear.
A common example in a Ruby on Rails project that uses Sidekiq and Redis is that Sidekiq jobs can be executed before the correct data is in the database. This is because after_commit
lives in the application layer and not the database, so it is likely that Sidekiq will pickup the job before the database is finished writing.
Connascence of Timing (CoT)
Connascence of Timing is a big word for race conditions. We often see race conditions when we're caching data and more than one reader is trying to update the cache. If two readers see the cache is expired, they both might try update it with slightly different values.
Connascence of Values (CoV)
This is a common one when entities have to move through various states. You may have seen a test that looks like this in the past:
# post.rb
class Post
attr_accessor :state
def initialize
@state = 'draft'
end
def publish!
@state = 'published'
end
end
# post_spec.rb
post = Post.new
expect(post.state).to eq('draft')
post.publish!
expect(post.state).to eq('published')
The tests here expect the initial state to be 'draft'
, which seems fine until we change the initial state of the article. A better solution would be to store the initial state in a variable. Acts As State Machine (AASM) is a library commonly used for simple state transitions like this.
Using AASM, we can rework our code to look like this:
class Post
include AASM
aasm do
state :draft, initial: true
state :published
event :publish do
transitions from: [:draft], to: :published
end
end
end
# post_spec.rb
post = Post.new
expect(post.state).to eq(Post.aasm.initial_state)
post.publish!
expect(post.published?).to be(true)
There are two key changes here. We've defined an initial state and the test no longer references a string, but instead the initial_state
method from AASM. Second, we can now use the published?
method from AASM so that the next test doesn't reference a string either.
Connascence of Identity (CoI)
"Identity" here refers to the identity of an object. There are two ways to determine the identity of an object
Object Equality
Every object is distinct even if their values match.
When you need to reference a single instance across your application, you can use a Singleton. However, be very careful with a Singleton class. They tend to be considered an anti-pattern in Ruby and often result in non-thread safe code!
Whole Value Equality
However, there may be cases where you need two distinct objects to be considered the same, in which case you'd want to use whole value equality. Wholable is a good library to help you with that.
Conclusion
Wow! Lotsa big words here. Or, well, just losta saying "connascence" over and over. I hope this post helps you identify different types of complexity and talk about it in plain language while you're working your way through your code base.
References: