Exploring strategies for modifiability

Now that we have seen some examples of good and bad coupling and cohesion, let's get to the strategies and approaches that a software architect can adopt to improve the modifiability of the software system.

Providing explicit interfaces

A module should mark a set of functions, classes, or methods as the interface it provides to external code. This can be thought of as the API of this module. Any external code that uses this API would become a client to the module.

Methods or functions that the module considers internal to its function, and which do not make up its API, should either be explicitly made private to the module or should be documented as such.

In Python, which doesn't provide variable access scope for functions or class methods, this can be done by conventions such as prefixing the function name with a single or double underscore, thereby signaling to potential clients that these functions are internal and shouldn't be referred to from outside.

Reducing two-way dependencies

As seen in the examples earlier, coupling between two software modules is manageable if the coupling direction is one-way. However, bidirectional coupling creates very strong linkages between modules, which can complicate the usage of the modules and increase their maintenance costs.

In Python, which uses reference-based garbage collection, this may also create cryptic referential loops for variables and objects, thereby making garbage collection difficult.

Bidirectional dependencies can be broken by refactoring the code in such a way that a module always uses the other one and not vice versa. In other words, encapsulate all related functions in the same module.

Here are our modules A and B of the earlier example, rewritten to break their bidirectional dependency:

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

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

        return string.count(char)

    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 text1 and text2"""

        # A text is a collection of strings split using newlines
        strings1 = text1.split("
")
        strings2 = text2.split("
")

        common_w = []
        for string1 in strings1:
            for string2 in strings2:
                common_w += common(string1, string2)

        return list(set(common_w))

Next is the listing of module B:

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

  import a

  def common_words(filename1, filename2):
    """ Return common words across two input files """

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

    return a.common_words(lines1, lines2)

We achieved this by simply moving the common function, which picks common words from two strings from module B to A. This is an example of refactoring to improve modifiability.

Abstract common services

Usage of helper modules that abstract common functions and methods can reduce coupling between two modules and increase their cohesion. For example, in the first example, module A acts as a helper module for module B.

Helper modules can be thought of as intermediaries or mediators, which abstract common services for other modules so that the dependent code is all available in one place without duplication. They can also help modules to increase their cohesion by moving out unwanted or unrelated functions.

Using inheritance techniques

When we find similar code or functionality occurring in classes, it might be a good time to refactor them so as to create class hierarchies so that common code is shared by virtue of inheritance.

Let's take a look at the following example:

""" Module textrank - Rank text files in order of degree of a specific word frequency. """

import operator

class TextRank(object):
    """ Accept text files as inputs and rank them in
    terms of how much a word occurs in them """

    def __init__(self, word, *filenames):
        self.word = word.strip().lower()
        self.filenames = filenames

    def rank(self):
        """ Rank the files. A tuple is returned with
        (filename, #occur) in decreasing order of
        occurences """

        occurs = []

        for fpath in self.filenames:
            data = open(fpath).read()
            words = map(lambda x: x.lower().strip(), data.split())
            # Filter empty words
            count = words.count(self.word)
            occurs.append((fpath, count))

        # Return in sorted order
        return sorted(occurs, key=operator.itemgetter(1), reverse=True)

Here is another module, urlrank, which performs the same function on URLs:

    """ Module urlrank - Rank URLs in order of degree of a specific word frequency """
    import operator
import operator
import requests

class UrlRank(object):
    """ Accept URLs as inputs and rank them in
    terms of how much a word occurs in them """

    def __init__(self, word, *urls):
        self.word = word.strip().lower()
        self.urls = urls

    def rank(self):
        """ Rank the URLs. A tuple is returned with
        (url, #occur) in decreasing order of
        occurences """

        occurs = []

        for url in self.urls:
            data = requests.get(url).content
            words = map(lambda x: x.lower().strip(), data.split())
            # Filter empty words
            count = words.count(self.word)
            occurs.append((url, count))

        # Return in sorted order
        return sorted(occurs, key=operator.itemgetter(1), reverse=True)

Both these modules perform similar functions of ranking a set of input data in terms of how much a given keyword appears in them. Over time, these classes could develop a lot of similar functionality, and the organization could end up with a lot of duplicate code, reducing modifiability.

We can use inheritance to help us here to abstract away the common logic in a parent class. Here is the parent class named RankBase, which accomplishes this by abstracting all common code as part of its API:

""" Module rankbase - Logic for ranking text using degree of word frequency """

import operator

class RankBase(object):
    """ Accept text data as inputs and rank them in
    terms of how much a word occurs in them """

    def __init__(self, word):
        self.word = word.strip().lower()

    def rank(self, *texts):
        """ Rank input data. A tuple is returned with
        (idx, #occur) in decreasing order of
        occurences """

        occurs = {}
        
        for idx,text in enumerate(texts):
            words = map(lambda x: x.lower().strip(), text.split())
            count = words.count(self.word)
            occurs[idx] = count

        # Return dictionary
        return occurs

    def sort(self, occurs):
        """ Return the ranking data in sorted order """

        return sorted(occurs, key=operator.itemgetter(1), reverse=True)

We now have the textrank and urlrank modules rewritten to take advantage of the logic in the parent class:

""" Module textrank - Rank text files in order of degree of a specific word frequency. """

import operator
from rankbase import RankBase

class TextRank(object):
    """ Accept text files as inputs and rank them in
    terms of how much a word occurs in them """

    def __init__(self, word, *filenames):
        self.word = word.strip().lower()
        self.filenames = filenames

    def rank(self):
        """ Rank the files. A tuple is returned with
        (filename, #occur) in decreasing order of
        occurences """

        texts = map(lambda x: open(x).read(), self.filenames)
        occurs = super(TextRank, self).rank(*texts)
        # Convert to filename list
        occurs = [(self.filenames[x],y) for x,y in occurs.items()]
            
        return self.sort(occurs)

Here is the modified listing for the urlrank module:

""" Module urlrank - Rank URLs in order of degree of a specific word frequency """

import requests
from rankbase import RankBase

class UrlRank(RankBase):
    """ Accept URLs as inputs and rank them in
    terms of how much a word occurs in them """

def __init__(self, word, *urls):
    self.word = word.strip().lower()
    self.urls = urls

def rank(self):
    """ Rank the URLs. A tuple is returned with
    (url, #occur) in decreasing order of
    occurences"""

    texts = map(lambda x: requests.get(x).content, self.urls)
    # Rank using a call to parent class's 'rank' method
    occurs = super(UrlRank, self).rank(*texts)
    # Convert to URLs list
    occurs = [(self.urls[x],y) for x,y in occurs.items()]

    return self.sort(occurs)

Not only has refactoring reduced the size of the code in each module, but it has also resulted in improved modifiability of the classes by abstracting the common code to a parent class which can be developed independently.

Using late binding techniques

Late binding refers to the practice of postponing the binding of values to parameters as late as possible in the order of execution of a code. Late binding allows the programmer to defer the factors that influence code execution, and hence the results of execution and performance of the code, to a later time by making use of multiple techniques.

Some late-binding techniques that can be used are as follows:

  • Plugin mechanisms: Rather than statically binding modules together, which increases coupling, this technique uses values resolved at runtime to load plugins that execute a specific dependent code. Plugins can be Python modules whose names are fetched during computations done at runtime or via IDs or variable names loaded from database queries or from configuration files.
  • Brokers/registry lookup services: Some services can be completely deferred to brokers, which look up the service names from a registry on demand, and call them dynamically and return results. An example may be a currency exchange service, which accepts a specific currency transformation as input (say USDINR), and looks up and configures a service for it dynamically at runtime, thereby requiring only the same code to execute on the system at all times. Since there is no dependent code on the system that varies with the input, the system remains immune from any changes required if the logic for the transformation changes, as it is deferred to an external service.
  • Notification services: Publish/subscribe mechanisms, which notify subscribers when the value of an object changes or when an event is published, can be useful to decouple systems from a volatile parameter and its value. Rather than tracking changes to such variables/objects internally, which may need a lot of dependent code and structures, such systems keep their clients immune to the changes in the system that affect and trigger the objects' internal behavior, but bind them only to an external API, which simply notifies the clients of the changed value.
  • Deployment time binding: By keeping the variable values associated to names or IDs in configuration files, we can defer object/variable binding to deployment time. The values are bound at startup by the software system once it loads its configuration files, which can then invoke specific paths in the code that creates appropriate objects.

    This approach can be combined with object-oriented patterns such as factories, which create the required object at runtime given the name or ID, hence keeping the clients that are dependent on these objects immune from any internal changes, increasing their modifiability.

  • Using creational patterns: Creational design patterns such as factory or builder, which abstract the task of creating of an object from the details of creating it, are ideal for separation of concerns for client modules that don't want their code to be modified when the code for creation of a dependent object changes.

    These approaches, when combined with deployment/configuration time or dynamic binding (using lookup services), can greatly increase the flexibility of a system and aid its modifiability.

We will look at examples of Python patterns in a later chapter in this book.

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

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