Introduction
In the previous posts we learned about connascence and explained why it’s so important to understand for software developers and architects, we looked at all 5 levels of static connascence and different approaches to improve code quality. In this post we will focus on 4 levels of dynamic connascence and will take practical approach to reduce the degree of dynamic connascence.
Connascence of Execution
Connascence of execution is the first dynamic level connascence. It appears when order of operation is important. For example, when brewing coffee, it’s important to follow basic preparation steps: grind coffee, prepare, turn on a coffee machine, wait and then serve freshly brewed coffee.
Example: We have a Barista
class that can brew given a coffee recipe. Each coffee recipe has sequence of steps that is critically important to follow in certain order. The steps may vary from one recipe to another, but basic steps and their order remain the same. Problem with this design is that Coffee
class needs to follow the conventional sequence of recipe steps in order to prepare each coffee. When we add new class of coffee we will have to replicate the sequence of basic steps of each recipe. The need to duplicate steps creates an opportunity to make a mistake and mess the order of recipe steps.
public class Cappuccino { public void Grind() { } public void PressCoffee() { } public void AddWater() { } public void AddMilk() { } public void TurnOnMachine() { } public void Wait() { } public void Serve() { } } public class Americano { public void Grind() { } public void AddCoffee() { } public void AddWater() { } public void TurnOnMachine() { } public void Wait() { } public void Serve() { } } public class Barista { public void BrewAmericano(Americano coffee) { // Sequence of steps is important and // is duplicated in every coffee class. coffee.Grind(); coffee.AddCoffee(); coffee.AddWater(); coffee.TurnOnMachine(); coffee.Wait(); coffee.Serve(); } public void Brew(Cappuccino coffee) { coffee.Grind(); coffee.PressCoffee(); coffee.AddWater(); coffee.AddMilk(); coffee.TurnOnMachine(); coffee.Wait(); coffee.Serve(); } }
How to fix connascence of Execution
This type of connascence is hard to avoid completely as most of algorithms require certain order of steps. However, we can improve our design by increasing locality of connascence. Template pattern can help us to extract the order of steps into a separate function or a class. Having algorithm implemented in one place also decreases degree of connascence by reducing number of components that duplicate the order of recipe steps.
public interface ICoffee { void Prepare(); void AddExtras(); } public class Cappuccino : ICoffee { public void Prepare() { PressCoffee(); AddWater(); AddMilk(); } public void AddExtras() { MakeCinnamonPrint(); } private void PressCoffee() { } private void AddWater() { } private void AddMilk() { } private void MakeCinnamonPrint() { } } public class Americano : ICoffee { public void Prepare() { AddCoffee(); AddWater(); } public void AddExtras() { } private void AddCoffee() { } private void AddWater() { } } public class Barista { // Sequence of basic steps is implemented as template method. public void Brew(ICoffee coffee) { Grind(); coffee.Prepare(); TurnOnMachine(); Wait(); coffee.AddExtras(); Serve(); } private void Grind() { } private void TurnOnMachine() { } private void Wait() { } private void Serve() { } }
Another approach is to make order of steps hard to mess up. For example, if a class requires certain initialization steps in a particular order, we can employ Factory pattern and lock the order withing factory method.
Connascence of Timing
Connascence of Timing appears when timing of operations becomes important. This level of connascence may appear when communication between components is asynchronous. For example, one component begins an asynchronous operation, while the other component needs the result of previous operation to continue execution. If second component does not wait for the result it may start execution before the result is ready which may cause sporadic exceptions or undefined behavior on runtime.
public class User { public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string PhoneNumber { get; set; } } public abstract class UserRepository { public async Task<User> SaveUser(string firstName, string lastName, string phoneNumber) { var user = new User(); var saveUserTask = SaveUserAsync(user); // We need to await saveUserTask completeion. // If we do not, then saveUserTask may not be completed // before we attempt to load user. This may cause // undefined behavior or exception. var savedUserId = await saveUserTask; // User must be saved before we attempt to load. var loadedUserTask = LoadUserAsync(savedUserId); return await loadedUserTask; } public abstract Task<int> SaveUserAsync(User user); public abstract Task<User> LoadUserAsync(int id); }
Connascence of timing may be found on higher design levels as well, for example when strong consistency models need to communicate with eventual consistency model. With strong consistency model a result is guaranteed to be available when operation returns. Eventual consistency model guarantees operation to be completed, but has no guarantees on when this is going to happen. Therefore, strong consistency model has timing dependency on eventual consistency model.
How to fix connascence of Timing
In general, whenever we have asynchronous code, we have connascence of timing. The way we can fix connascence of timing is to keep it as much local as possible (high locality) with few components involved (low degree). Luckily there is powerful pattern at our disposal – Promises. Multiple languages have support for promises: JavaScript Promises, C# Task Parallel Library and async / await
, Python asyncio and many other. Promises as asynchronous pattern allows to chain asynchronous operations declaratively in one place, hence achieving high locality, and also allows code reuse helping to lower the degree of connascence.
Connascence of Value
Connascence of value appears when multiple components depend on a changing value to be the same on runtime. For example, initial value of User.Id
property is -1. If User.Id == -1
, then UserRepository.SaveOrUpdateUser()
method assumes that user is new and needs to be inserted, otherwise existing user needs to be updated.
public class User { public User() { // If user is new, Id property has // vlaue -1 Id = -1; } public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string PhoneNumber { get; set; } } public abstract class UserRepository { public int SaveOrUpdateUser(User user) { // We make an assumption if user.Id property is -1 // then user is new and needs to be inserted. // Othereise we update existing user. if (user.Id == -1) { return InserttUser(user); } else { return UpdateUser(user); } } public abstract int InserttUser(User user); public abstract int UpdateUser(User user); }
Another example is error handling through error messages and string comparison. For example, one component returns an error message and the other component checks whether error massage contains certain sub-string to identify success or failure of the operation. This type of code can be found when components communicate over network, e.g. user interface communicates with a service, or microservices communicate with each other. Dependency on error message is dangerous because if the error message changes, it can break other components. In the worst case the introduced bug can make its way to production and stay undetected for some time, in the best case it can be detected by integration tests.
public class User { ... } public class UserServiceProxy { public string SaveUser(User user) { // Returns result of the operation as string. } } public class UserUiController { public void SaveUserData(User user) { var userService = new UserServiceProxy(); var response = userService.SaveUser(user); if (!response.Contains("error", StringComparison.OrdinalIgnoreCase)) { return; } if (response.Contains("database", StringComparison.OrdinalIgnoreCase)) { throw new DatabaseException("Can't save user"); } else { throw new ApplicationException("Fialed to save user, unknown reason"); } } }
How to fix connascence of Value
In many cases a fix is pretty obvious and boils down to spelling out unobvious assumptions clearly. First example can be fixed by adding IsNew
property to User
class which removes dependency on -1 to be initial value of User.Id
.
public class User { // 1. Give name to default value of User.Id private const int NewUserId = -1; public User() { Id = NewUserId; } public int Id { get; set; } public string FirstName { get; set; } public string LastName { get; set; } public string PhoneNumber { get; set; } // 2. Add property to check if user is new public bool IsNew => Id == NewUserId; } public abstract class UserRepository { public int SaveOrUpdateUser(User user) { // 3. Use User.IsNew if (user.IsNew) { return InserttUser(user); } else { return UpdateUser(user); } } public abstract int InserttUser(User user); public abstract int UpdateUser(User user); }
Error handling example can be fixed by introducing standard error codes that are part of UserService interface and can only change in backward compatible way.
public class User { ... } // 1. Define error codes explicitly public enum UserServiceResponseCode { Success = 0, DatabaseError = 2 } public class UserServiceResponse { public const int DatabaseError = 2; public UserServiceResponseCode ErrorCode { get; set; } public int ErrorMessage { get; set; } public bool IsSuccess => ErrorCode == UserServiceResponseCode.Success; } public class UserServiceProxy { public UserServiceResponse SaveUser(User user) { ... } } public class UserUiController { public void SaveUserData(User user) { var userService = new UserServiceProxy(); var response = userService.SaveUser(user); // 2. Now check for success result is easy if (response.IsSuccess) { return; } // 3. Check for the type of error is easy too if (response.ErrorCode == UserServiceResponseCode.DatabaseError) { throw new DatabaseException("Can't save user"); } else { throw new ApplicationException("Fialed to save user, unknown reason"); } } }
Connascence of Identity
Connascence of identity appears when two or more components require to have a reference to the same instance of an entity (object). The first example that comes in mind is a singleton, however with singleton we know by fact that we are dealing with shared object by design. This fact is explicit and well understood. Singleton is responsible for maintaining only one instance of its class. Therefore, when we reference a singleton this creates static connascence very similar to referencing a variable by name. So Singleton is not a good example.
Well, when can we have connascence of identity then? The most common case that I’ve encountered is when one object (A) creates an instance (I), the other object (B) gets access to the instance (I) through public interface of object A.
Example: we have a publisher-subscriber model. Publisher creates a queue and exposes it as public property. Publisher pushes messages to the queue. Subscriber accesses the queue through Publishers public property and pulls messages from the queue.
public class Publisher { public Queue<string> Queue { get; private set; } = new Queue<string>(); public void Publish(string message) { Queue.Enqueue(message); } } public class Subscriber { public void Consume(Publisher publisher) { Console.WriteLine(publisher.Queue.Dequeue()); } }
How to fix connascence of Identity
To fix connascence of identity we can make shared reference explicit. In our example we can move responsibility of creating and managing queue to a broker or to a Dependency Injection container. This approach converts dynamic connascence into static.
public class Publisher { private readonly Queue<string> _queue; public Publisher(Queue<string> queue) { _queue = queue; } public void Publish(string message) { _queue.Enqueue(message); } } public class Subscriber { private readonly Queue<string> _queue; public Subscriber(Queue<string> queue) { _queue = queue; } public void Consume() { Console.WriteLine(_queue.Dequeue()); } }
Summary
This post concludes the connascence series. We covered essentials of connascence as software quality metric and walked though multiple approaches to improve code quality for all levels of connascence. Now it’s time to apply this knowledge in the real world!