- Software Architecture with Python
- Anand Balachandran Pillai
- 1774字
- 2021-07-02 23:29:56
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("\n") strings2 = text2.split("\n") 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.
- Xcode 7 Essentials(Second Edition)
- INSTANT Weka How-to
- HTML5 and CSS3 Transition,Transformation,and Animation
- Linux操作系統基礎案例教程
- 零基礎輕松學SQL Server 2016
- 零基礎學Python網絡爬蟲案例實戰全流程詳解(入門與提高篇)
- C#應用程序設計教程
- jQuery炫酷應用實例集錦
- JQuery風暴:完美用戶體驗
- Data Science Algorithms in a Week
- Learning Bootstrap 4(Second Edition)
- UI設計基礎培訓教程(全彩版)
- OpenCV 3.0 Computer Vision with Java
- Python數據可視化之matplotlib實踐
- Python編程:從入門到實踐(第2版)