内聚和耦合
什么是内聚性
内聚是指一个模块的内部功能相互关联的紧密程度,执行某个特定的任务或相关任务组的模块是具有高内聚性的,而没有核心功能只是将大量功能凑到一起的模块具有低内聚性。
什么是耦合性
耦合是指模块 A 和模块 B 功能上的相关程度。如果两个模块的功能在代码层面上高度重叠,即模块之间有方法的大量互相调用,那么这两个模块就是高耦合的。在模块 A 上的任何变动都会使得 B 变化。
强耦合性不利于代码的可修改性,因为它增加了维护代码的成本,要提高代码的可修改性就应该做到代码的高内聚和低耦合。
测量内聚性和耦合性
举一个小例子来说明如何判断内聚性和耦合性。下面是模块 A 的代码:
""" 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(array, 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)
然后是模块 B 的代码:
""" Module B(b.py) - Implement functions provide 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 """
return pow(variance(array), 0.5)
现在对模块 A 和 B 中的函数进行分析:
模块 | 核心功能数 | 无关功能数 | 功能依赖数 |
---|---|---|---|
B | 4 | 0 | 3 * 1 = 3 |
A | 3 | 1 | 0 |
1),模块 B 有 4 个函数,它们都是围绕核心功能的。该模块中没有与核心功能无关的功能,所有模块 B 有 100% 的内聚性。
2),模块 A 有 4 个函数,其中 3 个是与核心功能相关的,但是最后一个 frequency 函数与核心功能无关,所以说 A 只有 75% 的内聚性。
3),模块 B 中有 3 个函数依赖着模块 A 中的 square 函数的,这使得模块 B 与 模块 A 有强烈的耦合性,这里的模块 B 对于模块 A 在函数层面上的耦合性是 75%。
4),模块 A 不依赖模块 B 中的任何函数,因此模块 A 是独立于模块 B 工作的,模块 A 对于模块 B 的耦合性是 0。
如何改善
想要提高模块 A 的内聚性可以很简单的将最后一个无关函数去掉,然后移植到其他模块中。
现在分析从模块 B 到模块 A 的耦合性:
1),B 中的 3 个函数只依赖 A 中的一个函数。
2),A 中被依赖的函数为 square,其功能是接受一个数组并返回每个数组的平方。
3),该函数的 API 很简单,因此在未来修改此函数 API 的可能性很小。
4),系统中不存在两种耦合方式,依赖方向只有从 B 到 A。
综上,尽管从 B 到 A 存在强耦合,但是这是一个 “好” 的耦合,而且丝毫不会影响系统的可修改性。
另一个例子
下面是模块 A 的代码:
""" Mudule 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 word across text1 and text2 """
# A text is a collection of strings split using newlines
strings1 = text1.split('\n')
strings2 = text2.split('\n')
common = []
for string1 in strings1:
for string2 in strings2:
common += b.common(string1, string2)
# drop duplicates
return list(set(common))
下面是模块 B 的代码:
""" Module B(b.py) - Provides text processing functions to user """
import a
def common(string1, string2):
""" Return common words across string1 and string2 """
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)
现在对模块 A 和 B 中的函数进行分析:
模块 | 核心功能数 | 无关功能数 | 功能依赖数 |
---|---|---|---|
B | 2 | 0 | 1 * 1 = 1 |
A | 2 | 0 | 1 * 1 = 1 |
1),模块 A 和模块 B 分别有两个函数,各个函数都是处理各自模块的核心功能,所以模块 A 和模块 B 都有 100% 的内聚性。
2),模块 A 中有一个函数依赖于模块 B 中的一个函数,同时,模块 B 中有 一个函数也依赖于模块 A 中的一个函数。因此从模块 A 到模块 B 有强耦合性,从模块 B 到模块 A 也具有强耦合性。也就是说,耦合性是双向的。
两个模块之间的双向耦合性就把每个模块的可修改性与对方紧密联系了起来,模块 A 中的任何修改将会迅速影响模块 B 的行为,反之亦然。因此这是一个 “坏” 的耦合。
提高可修改性的策略
提供显式接口
一个模块应该为外部代码提供一组函数、类或方法作为接口。接口可以认为是模块的 API,实际上 API 就是从接口发展而来的。任何使用此模块 API 的外部代码都被称为此莫夸的客户。对于模块来说是内部的方法或函数,或者不是 API 的构成方法或函数,也应该明确加上私有标识,或被记录在案。
减少双向依赖
从上面第二个例子可以看出,如果耦合方向是单方面的,两个模块之间的耦合是可管理的,也就是说,它不会造成大的软件质量缺陷。然而,双向耦合使得两个模块之间紧密相连,进而使得模块难以被使用以及维护成本提高。
某些语言(比如 python) 采用的是基于引用的垃圾回收机制,如果存在双向耦合,可能会导致变量与对象之间的隐式引用循环链,进而导致垃圾回收运行结果不理想。
双向依赖可以用这样一种方式打破:第一个模块总是使用第二个模块之外的函数,第二个模块也可以这样做。也就是说,可以将各模块*同使用的函数统一封装在一个模块中。
上面举出的第二个例子可以简单的将模块 B 中的 common 函数移植到模块 A 中。
抽象出公共服务
使用抽象常用功能和方法的辅助模块可以减少两个模块之间的耦合,并增加他们的内聚性。在上面的第二个例子中,模块 A 相当于模块 B 的辅助模块。
辅助模块可以认为是中间件或媒介,它抽象出其他模块的公共服务,使得被依赖的代码都被放在同一个地方,而不用在各模块中重复出现。另外,还可以把一个模块中与核心功能关联不大的函数移植到另一个辅助模块中,从而提高该模块的内聚性。
使用继承技术
当各个类中有相似的代码或函数时,可以使用类继承。使用继承方法使得各个类的通用代码通过继承共享。
下面举一个例子,根据某个词的出现频率对文本文件进行排序:
""" 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:
with open(fpath, 'r') as f:
data = f.read()
words = map(lambda x: x.lower().strip(), data.split())
# filter empty words
count = words.count(self.word)
occurs.appen((fpath, count))
# return in sorted order
return sorted(occurs, key=operator.itemgetter(1), reverse=True)
下面是另一个模块: urlrank。该模块对多个 URL 用同样的方法进行排序:
""" Mudule urlrank - Rank URLs in order of degree of a specific word frequency """
import operator
import requests
class URLRank(object):
""" Accept URLs as inputs and rank them in trems 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 occurenecs """
occurs = []
for url in urls:
data = requests.get(url).text
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)
上面两个模块都是对多个输入的数据集进行排序,依据都是某个词在数据集中的出现频率,因此这两个模块中会有很多相似的代码,从而降低了可修改性,下面使用继承重构这两个模块。
基类 RankBase:
""" Mudule 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.lower().strip()
def rank(self, *texts):
""" Rank input data. A tuple is returnes with (idx, #occur) in decreasing
order of occurences ""'
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)
textrank 模块
""" Module textrank - Rank text files in order of degree of specific word frequency """
import operator
from rankbase import RankBase
class TextRank(RankBase):
""" Accept text files as inputs and rank them in terms of how much a word occurs
in them """
def __init__(self, word, *filenames):
super().__init__(word)
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, 'r').read(), self.filenames)
occurs = super().rank(*texts)
occurs = [(self.filenames[x], y) for x, y in occurs.items()]
return self.sort(occurs)
urlrank 模块:
""" Mudule urlrank - Rank URLs in order of defree of a specific word frequency """
import operator
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):
super().__init__(word)
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).text, self.urls)
occurs = super().rank(*texts)
occurs = [(self.urls[x], y) for x, y in occurs.items]
return self.sort(occurs)