Estimated reading time: 16 minutes
Connascence: Rules for good software design
Despite uncountable good books to subject the design of software in day-to-day business is still more art than science. Since software is developed by teams and with art, taste is rarely the same, this is a very unfortunate situation. In more than one project, after a year, the “greenfield software” looks like Picasso, George Lucas and Mozart united to co-create a…
Despite uncountable good books to subject the design of software in day-to-day business is still more art than science. Since software is developed by teams and with art, taste is rarely the same, this is a very unfortunate situation. In more than one project, after a year, the “greenfield software” looks like Picasso, George Lucas and Mozart united to co-create a Japanese garden. There are probably rules per team, that say in which layer my newly written ten lines of code belong. But do these lines actually belong together and are these ten lines of code good or bad code? Where is the common theory and common language with which we can differentiate in our daily business whether we go left, right or rather not at all when writing code?
Experience does not answer this sufficiently, since it is not transferable. Design Patterns do not answer the question as well, because they describe a solution for only one context and do not help in unknown situations. That leaves terms like tight or loose coupling. These are gladly used, but what tight coupling means lies in the eye of the beholder; how to get from tight to loose is nebulous, and if there is something between tight and loose remains unclear. Let’s have look at an example of code for a video store. What are the criteria that make this code good or bad?
Martin Fowler shows the above code in a blog post and approaches this question via so-called Code Smells.
“A code smell is a hint that something has gone wrong somewhere in your code. Use the smell to track down the problem. […] Highly experienced and knowledgeable developers have a “feel” for good design. Having reached a state of “UnconsciousCompetence,” where they routinely practice good design without thinking about it too much, they find that they can look at a design or the code and immediately get a “feel” for its quality, without getting bogged down in extensive “logically detailed arguments. […] If something smells, it definitely needs to be checked out, but it may not actually need fixing or might have to just be tolerated.”– C2 Wiki
So when we are experienced, we have a feeling for good code or bad code. So if we are not, do we have no idea how to design code? That’s pretty unsatisfactory.
Experienced developers have written down explanations for known smells, classified them in a taxonomy and thus made them a kind of reference book for potentially bad software design.
So in this respect, code smells are very helpful. I myself like to use this collection of experience to evaluate a design afterwards. But to really understand how to design, to weigh one design against another, or to create a completely new design, that’s not enough.
So we are back to our initial question: What are the criteria for good and bad code? In search of an answer by chance I stumbled over Connascence.
Elements from Connascence
Connascene is a software quality metric and taxonomy of the connections that are created in our code. It was described in 1992 by Meilir Page-Jones in a paper , later became part and appears in the successor. A few years ago Jim Weirich re-intriduced them and jokingly referred to them as the Grand Unifying Theory of Software Design.
That is high praise so what is hiding behind this strange term? Nascency means emergence. The prefix Co comes from the Latin and means together or together. So Connascence seems to be about the common origin of code. The official definition also confirms this:
“2 elements A,B are connascent if there is at least 1 possible change to A that requires a change to B in order to maintain overall correctness” — Meilir Page-Jones
At first glance, this sounds similar to coupling mentioned at the beginning. In fact for Meilir Page-Jones, Connascence is the generalization of coupling and cohesion. Unlike the latter, Connascence is not nebulous; with Connascence we can evaluate and compare a design and derive refactoring suggestions. We will see that Connascence can be created between elements, even if they do not communicate with each other.
Static forms of connascence
Connascence of Name (CoN)
CoN is the weakest form and at the same time the connection with the greatest versatility, because names appear in software in many variants.
In our example, the function print()
has a dependency on the function titles()
. If I rename titles()
, I must also adapt the call in print()
, otherwise my program would no longer be correct.
Let’s make our example a little more complicated and add a database.
Now variables and also “harmless” strings increase the CoN. The variables result
and db
also have a CoN on another element. However, we did not include this in our example.
Connascence of Type (CoT)
CoT means that two elements must agree on the type.
movieRentalDaysSince(new Date('2013-11-6'))
is correct, movieRentalDaysSince(2013, 11, 6)
creates a SyntaxError.
Connascence of Convention (CoC)
CoC is present when the interpretation of data in two elements must be identical. A typical example is the Switch Case.
Robert Martin once said “Switch Cases are like the Sith, always two there are“. This means that case "regular"
and case "children"
will appear in at least two elements, and the movie
business logic will be randomly distributed among elements.
We can solve this problem by replacing movie.code
and thus the switch case with the polymorphic call amount()
: let thisAmount = movie.amount();
. We went from CoC to CoT.
In the example there are other CoC. if(thisAmount > 25)
contains a Magic Number, we stumbled on a known code smell. What is the meaning behind the 25, and does it perhaps reappear at another location?
At this point it is appropriate to replace the 25 by a named constant and to take a step towards CoN. Despite the named constant, we were still able to reduce CoC completely not. We still have access to a primitive value. At another place we can write another if(thisAmount > 25)
or if(movie.code === ...)
. Therefore, we can only prevent CoC by making movie.code no longer a primitive value or by removing this getter completely, thus transorming it into a method call.
CoC also occurs with functions that return zero or with functions that change their behavior based on the meaning of an input parameter. The parameter then has some semantic meaning, but it is not apparent. Because CoC deals with the meaning of elements, it is also known as Connascence of Meaning. Especially with functions, the meaning can be made clear by explicit names, which reduces it to CoN:
Convention | Name | |
---|---|---|
titles(“Cornetto”); | → | titlesContaining(“Cornetto”); |
titles(true); | → | availableTitles(); |
titles(false); | → | rentedTitles(); |
titles(0); | → | mostRecentRentedTitle(); |
titles(-1); | → | leastRentedTitle(); |
titles(null); | → | allTitles(); |
Connascence of Algorithm (CoA)
CoA occurs when two elements view or manipulate data in the same way. In our example, both server and client must agree on what a valid password is.
We could reduce the connascence of the password validation to CoC by having the server send the validation to the client in the form of a regex: let validPassword = (.{19,})
. If the password validation changes, we now only need to adjust the server, not the client. Another example are JSON Web Tokens (JWT). Here, too, the creation and evaluation of the token must run in the same way on the client and server side.
CoA is not limited to the validation of data. Rather, it is about two elements agreeing on which explicit parameters or implicit context variables (both preconditions) and which explicit return values or implicit side effects (both postconditions) result from them. This is also known from the Design by Contract (DbC) defined by Bertrand Meyer.
The element that calls isPasswordValid
on the server or client side guarantees the condition that the password is not zero and provides the property length. isPasswordValid
in turn guarantees the postcondition that no exception will fly if the input parameters are correct. The result will also be a Boolean and describe whether password is valid.
Some properties of this contract between calling code and called code can be modeled with a type system. Others must be considered by developers. CoA requires us to be aware of this implicit contract. Jim Weirich therefore also referred to CoA as Connascence of Contract.
Connascence of Position (CoP)
CoP occurs when the sequence of elements cannot change. In our first example, this occurs when the function is called and when the return of the function is processed.
If my programming language supports named parameters, I can convert CoP to CoN in the function call. Alternatively I can introduce a Parameter Object. Our second example is the return value of frequentRenterPoints
. Again, it might be useful to ‘tone down’ to CoT and define that an object is returned. But we could also decide to allow CoP here, if the other two connascence rules allow this.
Dynamic forms of connascence
The forms of Connascence shown so far count as static. They are defined by the lexical structure of the code and can be recognized at compile time in some laguages. More serious forms of connascence are dynamic and are created at runtime by the order in which code is executed.
Connascence of Execution (Order) (CoE)
CoE is created if the call sequence is relevant.
The application does not behave correctly if save()
is called before movie.title
and movie.updatedOn
has been changed. Because of the strong localization the connascence here is bearable but we can still reduce it by removing save()
from the function. Now changeMovie()
returns the changed movie object save(changeMovie(...))
.
Further examples of CoE are State Machines, locks on resources or shared mutable states like Singletons.
Connascence of Timing (CoTi)
CoTi occurs when the timing affects our code. In our example, the client expects the server to respond after a certain timeout.
Without rethinking our basic client/server architecture, we cannot reduce the connascence here. But we could delay the immediate effects on the user by caching. Unfortunately, we are increasing to CoI (Connascence of Identity), as we will see in a moment.
In contrast to CoE, CoTi only occurs with concurrent elements. Through threading, CoTi can therefore also occur with a server-only architecture. The negative effect is then also called a Race Condition.
Connascence of Value (CoV)
CoV occurs when an invariant (permanent condition) states that two or more values must change simultaneously.
The problem at this point is that the connascence is far apart. If we move moviesRentedByCustomer
and moviesRentedByAllCustomers
into the same function, the invariant is better protected.
CoV occurs with such invariants, or when values in tests are directly coupled to values used in the implementation under test. We notice the latter, for example, when we refactor the implementation and suddenly the tests run red.
Connascence of Identity (CoI)
The already mentioned CoI is the strongest form and occurs when the same object must be referenced at two or more locations.
Which title is displayed when calling displayMovie()
? In our example it is the old title, because the two movie objects in userRequestedTitleChange()
and changeMovieTitle()
do not have the same identity. If changeMovieTitle()
returns a movie object, we can pass it to displayMovie
and avoid CoI. In this example, it would also be better if userRequestedTitleChange()
did not expect a movie object, but instead directly returned the movieId.
Other typical scenarios for CoI are data replications between client and server, as they also occur through caching, or O/R mappers without an identity map. These save a transferred object in the database, but always generate a new object with a new identity for each query.
Contranascence
Another form of Connascene is Contranascence or Connascence of Difference. Here, two elements must always be different for the program to be correct. For example, the constant GENRE_ACTION
must never have the value 1, otherwise we might see drama.
Another example are name conflicts. In Objective-C, class names must be unique throughout the project, including all frameworks and libraries used. Therefore, all classes must indicate where they come from with a prefix. Apple reserves all 2-letter prefixes for their own purposes (UIViewController, NSObject etc.). Other programming languages use namespaces instead. In this case, a method with the same name or signature must not exist twice within a file or a code block.
Rules
The first three rules presented were derived by Jim Weirich from the three guidelines of Meilir Page-Jones. For me the rules are more precise and helpful than the guidelines. Therefore I would like to introduce the rules here:
The first rule is the Rule of Strength (RoS). It tells us that we should try to create the weakest possible connascence. Strength is defined according to how difficult it is to detect the connascence and then to refactor it.
The previous examples have shown, however, that it is not always possible or useful to reduce the strength level. This is not too bad at first, as long as we then follow the Rule of Locality (RoL). This says that code that is close together may have a stronger form of connascence. The further apart code is, the weaker the connascence should be. Conversely, we should move code with strong connascence closer together. This rule therefore has a direct effect on how we structure our code in folders and files.
Code in the same method counts as the highest location, code in the same file less, code in the same folder even less, code from another team much less. This is relevant because the probability of two teams communicating is inversely proportional to the distance between them. The further apart teams are from each other, the lower the connascence should be. So we can also apply Connascence to teams or companies.
The Rule of Degree (RoD) states that elements with a high degree of connectivity are more difficult to understand and change than elements with a low RoD. Two function parameters or return values can still be accepted, especially when they are close together. The more there are, the more RoD is involved and we should refactor.
Then there is the Rule of Stableness (RoS) designed by Jim Weirich. This is especially useful if the first three rules cannot be followed. If an element has a strong Form, low Locality and high Degree, then it should not change at all to little.
Evaluate Software Design with 9+(3+1)
Connascence provides us with nine degrees:
- Name
- Type
- Convention
- Algorithm
- Position
- Execution Order
- Timing
- Value
- Identity
and 3+1 rules:
- Strength
- Locality
- Degree
- Stableness
With these elements we can evaluate a design, and also get direct suggestions how we can improve it with refactoring (RoD and RoS are missing in the figure, because they appear later in the prio):
In addition to the refactoring suggestions, Connascence also gives a team a common language to talk about design – and I think that is the even greater value. The fewer decisions we make purely based on experience, the faster we can have fruitful discussions as a team and learn together.
I don’t think that with Connascence Code Smells and other practices have become obsolete. In the same way, even rocket scientists like to rely on Newton’s equations for simple questions, instead of trying to kill everything with relativity or quantum theory. That applies here as well: The check for code smells is much faster to do than to look at the connascence for the elements involved.
I actively pay attention to connascence especially in the following two cases:
- If I design an API Is the execution order relevant for the caller, does he have to interpret which value he got back or can timing problems arise in case of multiple calls? Can we change the API so that the connascence is lowered?
- If I want to evaluate designs
I therefore come back to the code snippet from the beginning and invite all readers to ask themselves if this is a good or bad design. What are the criteria that make this code good or bad? What forms of connascence are there and how can you improve this code by applying the rules?