Fundamentals of modifiability – cohesion and coupling

Let's now get back to the main topic of modifiability and discuss the two fundamental aspects that affect modifiability of code—namely, cohesion and coupling.

We've already discussed these concepts briefly in the first chapter. Let's do a quick review here.

Cohesion refers to how tightly the responsibilities of a module are related to each other. A module that performs a specific task or group of related tasks has high cohesion. A module in which a lot of functionality is dumped without a thought as to the core functionality would have low cohesion.

Coupling is the degree to which the functionality of two modules, A and B, are related. Two modules are strongly coupled if their functionality overlaps strongly at the code level—in terms of function or method calls. Any changes in module A would probably require changes in module B.

Strong coupling is always prohibitory for modifiability, as it increases the cost of maintaining the code base. Code which aims to increase modifiability should aim for high cohesion and low coupling.

We will analyze cohesion and coupling in the following subsections with some examples.

Measuring cohesion and coupling

Let's look at a simple example of two modules to figure out how we can measure coupling and cohesion quantitatively. The following is the code for module A, which purportedly implements functions that operate with a series (array) of numbers:

"" Module A (a.py) – Provides functions that operate on series of numbers """

def squares(narray):
    """ Return array of squares of numbers """
    return pow_n(array, 2)

def cubes(narray):
    """ Return array of cubes of numbers """
    return pow_n(narray, 3)

def pow_n(narray, n):
    """ Return array of numbers raised to arbitrary power n each """
    return [pow(x, n) for x in narray]
    
def frequency(string, word):
    """ Find the frequency of occurrences of word in string
    as percentage """

    word_l = word.lower()
    string_l = string.lower()

    # Words in string
    words = string_l.split()
    count = w.count(word_l)

    # Return frequency as percentage
    return 100.0*count/len(words)

Next is the listing of module B:

""" Module B (b.py) – Provides functions implementing some statistical methods """

import a

def rms(narray):
    """ Return root mean square of array of numbers"""
    return pow(sum(a.squares(narray)), 0.5)

def mean(array):
    """ Return mean of an array of numbers """
    return 1.0*sum(array)/len(array)

def variance(array):
    """ Return variance of an array of numbers """
    
    # Square of variation from mean
    avg = mean(array)
    array_d = [(x – avg) for x in array]
    variance = sum(a.squares(array_d))
    return variance

def standard_deviation(array):
    """ Return standard deviation of an array of numbers """
    
    # S.D is square root of variance
    return pow(variance(array), 0.5)

Let's do an analysis of the functions in module A and B. Here is the report:

Module

Core functions

Unrelated functions

Function dependencies

B

4

0

3 x 1 = 3

A

3

1

0

This has four functions that can be explained as follows:

  • Module B has four functions, all of them dealing with the core functionality. There are no unrelated functions in this module. Module B has 100% cohesion.
  • Module A has four functions, three of which are related to its core functionality, but the last one (frequency) isn't. This gives module A approximately 75% cohesion.
  • Three of the module B functions depend on one function in module A, namely, squares. This makes module B strongly coupled to module A. Coupling at function level is 75% from module B → A.
  • Module A doesn't depend on any functionality of module B. Module A will work independent of module B. Coupling from module A → B is zero.

Let's now look at how we can improve the cohesion of module A. In this case, it is as simple as dropping the last function, which doesn't really belong there. It could be dropped out entirely or moved to another module.

Here is the rewritten module A code, now with 100% cohesion with respect to its responsibilities:

""" Module A (a.py) – Implement functions that operate on series of numbers """

def squares(narray):
    """ Return array of squares of numbers """
    return pow_n(array, 2)

def cubes(narray):
    """ Return array of cubes of numbers """
    return pow_n(narray, 3)

def pow_n(narray, n):
    """ Return array of numbers raised to arbitrary power n each """
    return [pow(x, n) for x in narray]

Let's now analyze the quality of coupling from module B→ A and look at the risk factors of modifiability of code in B with respect to code in A, which are as follows:

  • The three functions in B depend on just one function in module A.
  • The function is named squares, which accepts an array and returns each of its member squared.
  • The function signature (API) is simple, so chances of changing the function signature in the future is less.
  • There is no two-way coupling in the system. The dependency is only from the direction B → A.

In other words, even though there is strong coupling from B to A, it is good coupling and doesn't affect the modifiability of the system in any way at all.

Let's now look at another example.

Measuring cohesion and coupling – string and text processing

Let's consider a different use case now, an example with modules that do a lot of string and text processing:

""" Module A (a.py) – Provides string processing functions """
import b

def ntimes(string, char):
    """ Return number of times character 'char'
    occurs in string """
    
    return string.count(char)

def common_words(text1, text2):
    """ Return common words across text1 and text2"""
    
    # A text is a collection of strings split using newlines
    strings1 = text1.split("
")
    strings2 = text2.split("
")
    
    common = []
    for string1 in strings1:
        for string2 in strings2:
            common += b.common(string1, string2)
        
    # Drop duplicates
    return list(set(common))

Next is the listing of module B, which is as follows:

""" Module B (b.py) – Provides text processing functions to user """

import a

def common(string1, string2):
    """ Return common words across strings1 1 & 2 """

    s1 = set(string1.lower().split())
    s2 = set(string2.lower().split())
    return s1.intersection(s2)    

def common_words(text1, text2):
    """ Return common words across two input files """

    lines1 = open(filename1).read()
    lines2 = open(filename2).read()

    return a.common_words(lines1, lines2)

Let's go through the coupling and cohesion analysis of these modules, given in following table:

Module

Core functions

Unrelated functions

Function dependencies

B

2

0

1 x 1 = 1

A

2

0

1 x 1 = 1

Here is an explanation of these numbers in the table:

  • Module A and B have two functions each, each of them dealing with the core functionality. Modules A and B both have 100% cohesion.
  • One function of module A is dependent on one function of module B. Similarly, one function of module B is dependent on one function of module A. There is strong coupling from A→ B and from B → A. In other words, the coupling is bidirectional.

Bidirectional coupling between two modules ties their modifiability to each other very strongly. Any changes in module A will quickly cascade to behavior of module B and vice versa. In other words, this is bad coupling.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.151.220