Skip to content

Latest commit

 

History

History
1592 lines (1112 loc) · 72.1 KB

File metadata and controls

1592 lines (1112 loc) · 72.1 KB

二、编写可修改可读代码

在第一章中,我们讨论了软件架构的各个方面,并涵盖了相关术语的一些定义。我们研究了架构师应该关注的软件架构的不同方面。在本章末尾,我们讨论了架构师在构建系统时应该关注的各种架构质量属性。我们详细讨论了这些属性中的每一个,查看了一些定义,以及在为实现这些属性而构建系统时应牢记的各种问题。

从本章开始,我们将逐一关注这些质量属性,并在每章中详细讨论它们。我们将深入研究一个属性,例如它的各种因素、实现它的技术、为它编程时要记住的方面,等等。由于本书的重点是 Python 及其生态系统,因此我们还将研究 Python 为实现和维护这些质量属性而提供的各种代码示例和第三方软件支持。

本章的重点是可修改性的质量属性。

什么是可修改性?

可修改性的建筑质量属性可以定义为:

可修改性是指对系统进行更改的容易程度,以及系统适应此类更改的灵活性。

我们在第一章中讨论了可修改性的各个方面,如内聚耦合等。在本章中,我们将通过一些示例对这些方面进行更深入的挖掘。然而,在我们深入挖掘之前,最好先看看可修改性如何与其他与之相关的质量属性相适应。

与可修改性相关的方面

在前一章中,我们已经看到了可修改性的一些方面。让我们进一步讨论这一点,并查看与可修改性密切相关的一些相关质量属性:

  • 可读性:可读性可以定义为程序逻辑易于遵循和理解。可读软件是以特定风格编写的代码,遵循所用编程语言通常采用的准则,其逻辑以简洁、清晰的方式使用该语言提供的功能。
  • 模块化:模块化意味着软件系统是在封装良好的模块中编写的,这些模块具有非常具体、文档化的功能。换句话说,模块化代码为系统的其余部分提供了程序员友好的 API。可修改性与可重用性密切相关。
  • 可重用性:这衡量软件系统的部分数量,包括代码、工具、设计和其他,可以在零修改或很少修改的情况下在系统的其他部分中重用。一个好的设计从一开始就强调可重用性。可重用性体现在软件开发的干式原则中。
  • 可维护性:软件的可维护性是指系统可以通过其预期利益相关者进行更新并保持在有用状态下工作的简易性和效率。可维护性是一种度量标准,它包括可修改性、可读性、模块性和可测试性。

在本章中,我们将深入探讨可读性和可重用性/模块化方面。我们将从 Python 编程语言的上下文中逐一介绍这些。我们将首先从可读性开始。

理解可读性

软件系统的可读性与其可修改性密切相关。编写良好、文档丰富的代码,与编程语言的标准或采用的实践保持一致,倾向于生成易于阅读和修改的简单、简洁的代码。

可读性不仅与遵循良好的编码准则有关,还与逻辑的清晰程度、代码使用语言的标准功能的程度、函数的模块化程度等有关。

事实上,我们可以将可读性的不同方面总结如下:

  • 写得好:如果一段代码使用简单的语法,使用该语言众所周知的特性和习惯用法,逻辑清晰简洁,并且有意义地使用变量、函数和类/模块名称,那么它就是写得好的代码;也就是说,他们表达他们所做的事情。
  • 文档化:文档化通常指代码中的内联注释。一段记录良好的代码详细说明了它的功能、输入参数(如果有)、返回值(如果有)以及逻辑或算法(如果有)。它还记录了内联或单独文件中运行代码所需的任何外部库或 API 使用和配置。
  • 格式良好的:大多数编程语言,尤其是通过分布式但紧密结合的编程社区在互联网上开发的开放源代码语言,都倾向于有文档化的风格指南。在缩进和格式化等方面符合这些准则的代码,往往比不符合这些准则的代码更具可读性。

不符合这些指导原则的代码在可读性方面通常是缺乏的。

可读性的缺乏会影响代码的可修改性,从而影响代码的可维护性,从而导致组织在资源(主要是人员和时间)方面的成本不断增加,从而使系统保持在可用状态。

Python 与可读性

Python 是一种从头开始为可读性而设计的语言。借用著名的 Python 禅宗的一句话。

可读性计数

提示

Python 的 Zen 是一组影响 Python 编程语言设计的 20 条原则,其中 19 条已经写下来。通过打开 Python 解释器提示符并键入以下内容,您可以看到 Python 的 Zen:

>>>import this

Python 作为一种语言,强调可读性。它通过清晰、简洁的关键字来实现这一点,这些关键字模仿其英语对应词,使用最少的运算符,并遵循以下原则:

应该有一种——最好只有一种——显而易见的方法来做到这一点。

例如,这里有一种方法可以在 Python 中迭代序列,同时打印其索引:

for idx in range(len(seq)):
    item = seq[idx]
    print(idx, '=>', item)

但是,Python 中使用的一个更常见的习惯用法是使用迭代器的enumerate()助手,它为序列中的每个项返回两个元组(idxitem

for idx, item in enumerate(seq):
    print(idx, '=>', item)

在许多编程语言中,如 C++、java 或 Ruby,第一个版本将被认为与第二个版本一样好。然而,在 Python 中,有一些编写代码的习惯用法,它们比其他一些更符合语言的原则。

在本例中,第二个版本更接近 Python 程序员编写代码来解决问题的方式。第一种方式被认为比第二种方式不那么像蟒蛇。

术语“Pythonic”是您在与 Python 社区交互时经常遇到的东西。这意味着代码不仅解决了问题,而且遵循了 Python 社区通常遵循的约定和习惯用法,并以预期的方式使用该语言。

Pythonic 的定义是主观的,但您可以将其视为与 Python 的禅保持一致的 Python 代码,或者一般来说,遵循社区采用的著名惯用编程实践。

Python 的设计原则和简洁的语法使编写可读代码变得容易。然而,程序员从其他的迂回的、不太习惯的语言迁移到 Python 是一个常见的陷阱,比如说 C++或 java,用 Python 的方式编写 Python 代码。例如,循环的第一个版本更有可能是由从这些语言之一迁移到 Python 的人编写的,而不是已经用 Python 编写了一段时间的人编写的。

对于 Python 程序员来说,尽早理解这一点很重要,这样当您越来越习惯这种语言时,您就可以编写更多的惯用或 Python 代码。如果您熟悉 Python 的编码原则和习惯用法,那么从长远来看,您可以更高效地使用 Python。

可读性-反模式

一般来说,Python 鼓励并促进编写可读代码。然而,说任何用 Python 编写的代码都具有高度可读性当然是非常不现实的。尽管 Python 具有所有可读性 DNA,但它也有一些难以阅读、写得不好或不可读的代码,这一点可以通过花一些时间浏览一些在 Web 上用 Python 编写的公开开源代码来证明。

在编程语言中,有一些实践倾向于产生难以阅读或不可读的代码。这些可以被视为反模式,这是一个祸根,不仅在 Python 编程中,在任何编程语言中都是如此:

  • Code with little or no comments: Lack of code comments is often the primary reason for producing code that is unreadable. More often than not, programmers don't do a very good job of documenting their thoughts, which led to a particular implementation, in code. When the same code is read by another programmer or by the same programmer a few months later (this happens quite a lot!), it is not easy to figure out why the specific implementation approach was followed. This makes it difficult to reason about the pros and cons of an alternate approach.

    这也使得做出修改代码的决定变得困难,可能是为了客户修复,并且通常会长期影响代码的可修改性。对代码的注释通常是编写代码的程序员的纪律性和严谨性的指标,也是执行此类实践的组织的指标。

  • Code which breaks best practices of the language: Best practices of a programming language typically evolves from years of experience in using the language by a community of developers, and the efficient feedback that it generates. They capture the best way of putting the programming language to good use to solve problems, and typically, capture the idioms and common patterns for using the language.

    例如,在 Python 中,Zen 可以被视为其最佳实践和社区采用的一组通用编程习惯用法的闪光火炬。

    通常,经验不足或从其他编程语言或环境迁移的程序员倾向于生成与这些实践不一致的代码,因此,最终编写的代码可读性较低。

  • 编程反模式:存在大量的编码或编程反模式,它们往往产生难以读取的代码,因此难以维护代码。以下是一些著名的例子:

    • 意大利面代码:一段没有可识别结构或控制流的代码。它通常是由以下复杂逻辑生成的:大量无条件跳转、非结构化异常处理、设计糟糕的并发结构等等。

    • 大泥球:一个没有显示整体结构或目标的代码片段系统。“大泥球”通常由许多意大利面代码组成,通常是由多人编写的代码的标志,这些代码经过多次修补,几乎没有文档。

    • Copy-Paste programming: Often produced in organizations where expediency of delivery is favored over thoughtful design, copy/paste coding produces long, repetitive chunks of code, which essentially do the same thing again and again with minor modifications. This leads to code-bloat, and in the long term, the code becomes unmaintainable.

      一个类似的反模式是cargo cult programming,程序员反复遵循相同的设计或编程模式,而不考虑它是否适合它试图解决的特定场景或问题。

    • 自我编程:自我编程是一个程序员,通常是一个经验丰富的程序员,他喜欢自己的个人风格,而不是记录在案的最佳实践或编码的组织风格。这有时会为其他人(通常是年轻或经验不足的程序员)创建晦涩难懂的代码。一个例子是在 Python 中使用函数式编程结构将所有内容编写为一行程序的趋势。

通过在您的组织中采用结构化编程的实践,并强制使用编码准则和最佳实践,可以规避反模式编码。

以下是一些特定于 Python 的反模式:

  • Mixed Indentation: Python uses indentation to separate blocks of code, as it lacks braces or other syntactical constructs of languages like C/C++ or Java which separate code blocks. However, one needs to be careful when indenting code in Python. A common antipattern is where people mix both tabs (the \t character) and spaces in their Python code. This can be fixed by using editors which always use either tabs or spaces to indent code.

    Python 自带了内置模块,如tabnanny,可用于检查代码的缩进问题。

  • Mixing string literal types: Python provides three different ways to create string literals: either by using the single quote ('), the double quote ("), or Python's own special triple quote (''' or """). Code which mixes these three types of literals in the same block of code or functional unit becomes more difficult to read.

    与此相关的字符串滥用是程序员在 Python 代码中使用三引号字符串作为内联注释,而不是使用#字符作为注释前缀。

  • 过度使用函数构造:Python 作为一种混合范式语言,通过其 lambda 关键字和其map()reduce()filter()函数为函数式编程提供支持。然而,有时,有经验的程序员或来自 Python 函数式编程背景的程序员会过度使用这些结构,生成的代码过于神秘,因此其他程序员无法阅读。

可读性技巧

现在我们已经对什么有助于代码的可读性有了很好的了解,让我们来看看我们可以采用哪些方法来提高 Python 代码的可读性。

记录您的代码

提高代码可读性的一个简单而有效的方法是记录代码的功能。文档对于代码的可读性和长期可修改性很重要。

代码文档可分为以下几类:

  • 内联文档:程序员通过使用代码注释、函数文档、模块文档等作为代码本身的一部分来编写代码。这是最有效和有用的代码文档类型。
  • 外部文档:这些是在单独的文件中捕获的附加文档,通常记录代码的使用、代码更改、安装步骤、部署等方面。例如,READMEINSTALLCHANGELOG文件,这些文件通常是在符合 GNU 构建原则的开源项目中找到的。
  • 用户手册:这些是正式文件,通常由专门的人员或团队使用图片和文本,通常针对系统用户。此类文档通常是在软件项目接近尾声时准备和交付的,此时产品是稳定的,并且可以发货。在这里的讨论中,我们不关心这种类型的文档。

Python 是一种从一开始就为智能内联代码文档设计的语言。在 Python 中,内联文档可以在以下级别完成:

  • Code comments: This is the text inline with code, prefixed by the hash (#) character. They can be used liberally inside your code explaining what each step of the code does.

    以下是一个示例:

    # This loop performs a network fetch of the URL, retrying upto 3
    # times in case of errors. In case the URL cant be fetched, 
    # an error is returned.
    
    # Initialize all state
    count, ntries, result, error = 0, 3, None, None
    while count < ntries:
        try:
            # NOTE: We are using an explicit   timeout of 30s here
            result = requests.get(url, timeout=30)
        except Exception as error:
            print('Caught exception', error, 'trying again after a while')
          # increment count
          count += 1
          # sleep 1 second every time
          time.sleep(1)
    
      if result == None:
        print("Error, could not fetch URL",url)
        # Return a tuple of (<return code>, <lasterror>)
        return (2, error)
    
          # Return data of URL
        return result.content

    即使在某些地方,自由使用评论也可能被认为是多余的。稍后我们将在评论代码时了解一些一般的经验法则。

  • Function doc-strings: Python provides a simple way to document what a function does by using a string literal just below the function definition. This can be done by using any of the three styles of string literals.

    以下是一个例子:

    def fetch_url(url, ntries=3, timeout=30):
             " Fetch a given url and return its contents "
    
            # This loop performs a network fetch of the URL, retrying 
            # upto
            # 3 times in case of errors. In case the URL cant be 
            # fetched,       
            # an error is returned.
    
            # Initialize all state
            count, result, error = 0, None, None
            while count < ntries:
                try:
                    result = requests.get(url, timeout=timeout)
                except Exception as error:
                    print('Caught exception', error, 'trying again after a while')
                    # increment count
                    count += 1
                    # sleep 1 second every time
                    time.sleep(1)
    
            if result == None:
                print("Error, could not fetch URL",url)
                # Return a tuple of (<return code>, <lasterror>)
                return (2, error)
    
            # Return data of URL
            return result.content

    函数 docstring 表示获取给定 URL 并返回其内容。然而,尽管它很有用,但它的使用是有限的,因为它只说明函数的作用,而没有解释函数的参数。以下是一个改进的版本:

    def fetch_url(url, ntries=3, timeout=30):
            """ Fetch a given url and return its contents. 
    
            @params
                url - The URL to be fetched.
                ntries - The maximum number of retries.
                timeout - Timout per call in seconds.
    
            @returns
                On success - Contents of URL.
                On failure - (error_code, last_error)
            """
    
            # This loop performs a network fetch of the URL, 
            # retrying upto      
            # 'ntries' times in case of errors. In case the URL 
            # cant be
            # fetched, an error is returned.
    
            # Initialize all state
            count, result, error = 0, None, None
            while count < ntries:
                try:
                    result = requests.get(url, timeout=timeout)
                except Exception as error:
                    print('Caught exception', error, 'trying again after a while')
                    # increment count
                    count += 1
                    # sleep 1 second every time
                    time.sleep(1)
    
            if result == None:
                print("Error, could not fetch URL",url)
                # Return a tuple of (<return code>, <lasterror>)
                return (2, error)
    
            # Return data of the URL
            return result.content

    在前面的代码中,函数的用法对于程序员来说变得更加清楚,他们可能计划导入函数的定义并在代码中使用它。请注意,这种扩展文档通常会跨越多行,因此,在函数 docstrings 中始终使用三重引号是一个好主意。

  • Class docstrings: These work just like a function docstring except that they provide documentation for a class directly. This is provided just below the class keyword defining the class.

    以下是一个例子:

    class UrlFetcher(object):
             """ Implements the steps of fetching a URL.
    
            Main methods:
                fetch - Fetches the URL.
                get - Return the URLs data.
            """
    
            def __init__(self, url, timeout=30, ntries=3, headers={}):
                """ Initializer. 
                @params
                    url - URL to fetch.
                    timeout - Timeout per connection (seconds).
                    ntries - Max number of retries.
                    headers - Optional request headers.
                """
                self.url = url
                self.timeout = timeout
                self.ntries = retries
                self.headers = headers
                # Enapsulated result object
                self.result = result 
    
            def fetch(self):
                """ Fetch the URL and save the result """
    
                # This loop performs a network fetch of the URL, 
                # retrying 
                # upto 'ntries' times in case of errors. 
    
                count, result, error = 0, None, None
                while count < self.ntries:
                    try:
                        result = requests.get(self.url,
                                              timeout=self.timeout,
                                              headers = self.headers)
                    except Exception as error:
                        print('Caught exception', error, 'trying again after a while')
                        # increment count
                        count += 1
                        # sleep 1 second every time
                        time.sleep(1)
    
                if result != None:
                    # Save result
                    self.result = result
    
            def get(self):
                """ Return the data for the URL """
    
                if self.result != None:
                    return self.result.content

    请参见类 docstring 如何定义类的一些主要方法。这是一个非常有用的实践,因为它在顶层为程序员提供了有用的信息,而无需单独检查每个函数的文档。

  • Module docstrings: Module docstring capture information at the module level, usually about the functionality of the module and some detail about what each member of the module (function, class, and others) does. The syntax is the same as the class or function docstring. The information is usually captured at the beginning of the module code.

    模块文档还可以捕获模块的任何特定外部依赖项,如果它们不是很明显,例如,导入一个不常用的第三方软件包:

    """
        urlhelper - Utility classes and functions to work with URLs.
    
        Members:
    
            # UrlFetcher - A class which encapsulates action of 
            # fetching
            content of a URL.
            # get_web_url - Converts URLs so they can be used on the 
            # web.
            # get_domain - Returns the domain (site) of the URL.
    """
    
    import urllib
    
    def get_domain(url):
        """ Return the domain name (site) for the URL"""
    
        urlp = urllib.parse.urlparse(url)
        return urlp.netloc
    
    def get_web_url(url, default='http'):
        """ Make a URL useful for fetch requests
        -  Prefix network scheme in front of it if not present already
        """ 
    
        urlp = urllib.parse.urlparse(url)
        if urlp.scheme == '' and urlp.netloc == '':
                  # No scheme, prefix default
          return default + '://' + url
    
        return url
    
    class UrlFetcher(object):
         """ Implements the steps of fetching a URL.
    
        Main methods:
            fetch - Fetches the URL.
            get - Return the URLs data.
        """
    
        def __init__(self, url, timeout=30, ntries=3, headers={}):
            """ Initializer. 
            @params
                url - URL to fetch.
                timeout - Timeout per connection (seconds).
                ntries - Max number of retries.
                headers - Optional request headers.
            """
            self.url = url
            self.timeout = timeout
            self.ntries = retries
            self.headers = headers
            # Enapsulated result object
            self.result = result 
    
        def fetch(self):
            """ Fetch the URL and save the result """
    
            # This loop performs a network fetch of the URL, retrying 
            # upto 'ntries' times in case of errors. 
    
            count, result, error = 0, None, None
            while count < self.ntries:
                try:
                    result = requests.get(self.url,
                                          timeout=self.timeout,
                                          headers = self.headers)
                except Exception as error:
                    print('Caught exception', error, 'trying again after a while')
                    # increment count
                    count += 1
                    # sleep 1 second every time
                    time.sleep(1)
    
            if result != None:
                # Save result
                self.result = result
    
        def get(self):
            """ Return the data for the URL """
    
            if self.result != None:
                return self.result.content

遵循编码和样式指南

大多数编程语言都有一套相对知名的编码和/或风格指南。它们要么是作为一种约定在多年的使用过程中开发出来的,要么是该编程语言在线社区讨论的结果。C/C++是前者的一个很好的例子,Python 是后者的一个很好的例子。

对于公司来说,通过采用现有的标准指南,并根据公司自己的特定开发环境和需求定制它们来指定自己的指南也是一种常见的做法。

对于 Python,Python 编程社区发布了一套明确的编码风格指南。本指南称为 PEP-8,作为Python 增强方案PEP文件集)的一部分在线提供。

您可以在以下 URL 找到 PEP-8:

https://www.python.org/dev/peps/pep-0008/

PEP-8 最早创建于 2001 年,自那时以来经历了多次修订。主要作者是 Python 的创建者,Guido Van Rossum,来自 Barry Warsaw 和 Nick Coghlan。

PEP-8 是通过改编 Guido 最初的Python 风格指南文章并加入 Barry 的风格指南而创建的。

在本书中,我们将不深入探讨 PEP-8,因为本节的目标不是教你 PEP-8。然而,我们将讨论 PEP-8 的基本原则,并列出一些主要建议。

PEP-8 的基本理念可概括如下:

  • 代码读的比写的多。因此,提供一个指南将使代码更具可读性,并使其在整个 Python 代码中保持一致。
  • 项目内的一致性很重要。然而,模块或包内的一致性更为重要。类或函数等代码单元内的一致性是最重要的。
  • 知道什么时候应该忽略一条指导方针。例如,如果采用该准则会降低代码的可读性、破坏周围的代码或破坏代码的向后兼容性,则可能会发生这种情况。学习例子,选择最好的。
  • 如果指南不直接适用于您的组织或对您的组织没有帮助,请对其进行自定义。如果您对指导方针有任何疑问,请询问有帮助的 Python 社区以获得澄清。

我们在此不详细介绍 PEP-8 指南。感兴趣的读者可以使用此处提供的 URL 在线参考 Web 上的文档。

检查和重构代码

代码需要维护。生产中使用的未维护代码如果不定期更新,可能会成为问题和噩梦。

定期定期检查代码对于保持代码可读性和良好的可修改性和可维护性非常有用。对于生产中的系统或应用至关重要的代码,随着时间的推移往往会得到很多快速修复,因为它是针对不同的用例进行定制或增强的,或者是针对问题进行修补的。据观察,程序员通常不会记录此类快速修复(称为“补丁”或“热修复”),因为时间需求通常会比良好的工程实践(如记录和遵循指南)加快立即测试和部署!

随着时间的推移,这些补丁可能会积累,从而导致代码膨胀,并为团队创造巨大的未来工程债务,这可能会成为一件昂贵的事情。解决办法是定期审查。

评审应与熟悉应用的工程师一起进行,但不必使用相同的代码。这为代码提供了一套全新的视角,这在检测原始作者可能忽略的 bug 时非常有用。由几位经验丰富的开发人员来审查大的更改是一个好主意。

这可以与代码的一般重构相结合,以改进实现、减少耦合或增加内聚。

对代码进行注释

我们即将结束关于代码可读性的讨论,现在是介绍编写代码注释时要遵循的一些一般经验规则的好时机。这些可列示如下:

  • Comments should be descriptive, and should explain the code. A comment which simply repeats what is obvious from the function name is not very useful.

    这里有一个例子。以下两个代码显示了均方根RMS)速度计算的相同实现,但第二个版本的docstring比第一个版本有用得多:

    def rms(varray=[]):
        """ RMS velocity """
    
        squares = map(lambda x: x*x, varray)
        return pow(sum(squares), 0.5) 
    
    def rms(varray=[]):
        """ Root mean squared velocity. Returns
        square root of sum of squares of velocities """
    
        squares = map(lambda x: x*x, varray)
        return pow(sum(squares), 0.5)
  • Code comments should be written in the block we are commenting on, rather than as follows:

    # This code calculates the sum of squares of velocities 
    squares = map(lambda x: x*x, varray)

    前一个版本比后一个版本更清晰,后一个版本在代码下面使用注释,因为它符合从上到下阅读的自然顺序。

    squares = map(lambda x: x*x, varray)
    # The above code calculates the sum of squares of velocities 
  • 应该尽可能少地使用内联注释。这是因为很容易将这些内容作为代码本身的一部分混淆,特别是如果分隔注释字符被意外删除,从而导致错误:

    # Not good !
    squares = map(lambda x: x*x, varray)   # Calculate squares of velocities
  • 尽量避免多余的评论,增加很少的价值:

    # The following code iterates through odd numbers
    for num in nums:
        # Skip if number is odd
        if num % 2 == 0: continue

最后一段代码中的第二条注释没有什么价值,可以省略。

可修改性基础——内聚&耦合

现在让我们回到可修改性的主题,讨论影响代码可修改性的两个基本方面,即内聚和耦合。

我们已经在第一章简要讨论了这些概念。让我们在这里快速回顾一下。

内聚性是指模块的职责之间的紧密联系。执行特定任务或一组相关任务的模块具有很高的内聚性。如果一个模块中转储了大量功能,而没有考虑核心功能,那么该模块的内聚性就会很低。

耦合是两个模块 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(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)

接下来是模块 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 = [(xavg) 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)

让我们对模块 A 和模块 B 中的功能进行分析。以下是报告:

|

单元

|

核心功能

|

无关函数

|

函数依赖关系

| | --- | --- | --- | --- | | B | 4. | 0 | 3x1=3 | | A. | 3. | 1. | 0 |

这有四个功能,可以解释如下:

  • 模块 B 有四个功能,它们都涉及核心功能。此模块中没有与其核心功能无关的功能。模块 B 具有 100%的内聚性。
  • 模块 A 有四个功能,其中三个功能与其核心功能相关,但最后一个功能(频率)不相关。这使模块具有大约75%的内聚力。
  • 模块 B 中的三个函数依赖于模块 A 中的一个函数,即 square。这使得模块 B 与模块 A 强耦合。功能级别的耦合是模块 B 的75%?A.
  • 模块 A 不依赖于模块 B 的任何功能。模块 A 将独立于模块 B 工作。与模块 A 耦合?B 是零。

现在让我们看看如何提高模块 A 的内聚性。在这种情况下,只需删除不属于模块 A 的最后一个函数即可。它可以完全退出或移动到另一个模块。

这是重写的模块 A 代码,现在其职责具有 100%的内聚性:

""" 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]

现在让我们分析模块 B 的耦合质量?A、 并查看 B 代码相对于 A 代码的可修改性风险因素,如下所示:

  • B 中的三个功能仅依赖于模块 A 中的一个功能。
  • 函数名为 squares,它接受一个数组并返回它的每个成员 squares。
  • 函数签名(API)很简单,因此将来更改函数签名的机会较少。
  • 系统中没有双向耦合。依赖关系仅来自方向 B?A.

换言之,即使从 B 到 A 存在强耦合,但它是良好耦合,并且根本不会以任何方式影响系统的可修改性。

现在让我们看另一个例子。

测量衔接和耦合-字符串和文本处理

现在让我们考虑一个不同的用例,一个具有很多字符串和文本处理功能的例子:

""" 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("\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 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)

让我们看一下下表中给出的这些模块的耦合和内聚分析:

|

单元

|

核心功能

|

无关函数

|

函数依赖关系

| | --- | --- | --- | --- | | B | 2. | 0 | 1 x 1=1 | | A. | 2. | 0 | 1 x 1=1 |

以下是表中这些数字的说明:

  • 模块 A 和 B 各有两个功能,每个功能处理核心功能。模块 A 和模块 B 都具有100%的内聚性。
  • 模块 A 的一个功能依赖于模块 B 的一个功能。类似地,模块 B 的一个功能依赖于模块 A 的一个功能。模块 A 的强耦合?B、 也是从 B 来的?A.换句话说,耦合是双向的。

两个模块之间的双向耦合将它们的可修改性紧密地联系在一起。模块 A 中的任何更改都将快速级联到模块 B 和反之亦然。换句话说,这是不好的耦合。

探索可修改性策略

现在我们已经看到了一些好耦合和坏耦合以及内聚的例子,让我们来看看软件设计师或架构师可以使用哪些策略和方法来减少这些方面对可修改性的影响,从而提高软件系统的可修改性。

提供显式接口

模块应该将一组函数、类或方法标记为它提供给外部代码的接口。可以将其视为该模块的 API,并从中导出。任何使用此 API 的外部代码都将成为模块的客户端。

模块认为属于其函数内部且不构成其 API 的方法或函数,应明确为模块私有,或应作为此类文件记录。

在 Python 中,它不为函数或类方法提供变量访问范围,这可以通过约定来实现,比如在函数名前面加上一个或两个下划线,从而向潜在的客户端发出信号,表明这些函数是内部的,不应该从外部引用。

减少双向依赖

如前面的示例所示,如果耦合方向为单向,则两个软件模块之间的耦合是可管理的。然而,双向耦合在模块之间创建了非常强的链接,这会使模块的使用变得复杂,并增加其维护成本。

在 Python 等使用基于引用的垃圾收集的语言中,这也可能为变量和对象创建神秘的引用循环,从而使垃圾收集变得困难。

通过重构代码,使模块始终使用另一个模块,而不是相反,可以打破双向依赖关系。换句话说,将所有相关函数封装在同一个模块中。

下面是我们前面的示例中的模块 A 和 B,它们被重写以打破它们的双向依赖关系:

    """ 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))

接下来是模块 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)

我们通过简单地移动函数common来实现这一点,该函数从模块 B 到模块 A 的两个字符串中选择常用词。这是一个重构以提高可修改性的示例。

抽象共同事务

使用 helper 模块抽象出常用的函数和方法,可以减少两个模块之间的耦合,也可以增加它们的内聚性。例如,在第一个示例中,模块 A 充当模块 B 的辅助模块。在第二个示例中,在重构步骤之后,模块 A 充当模块 B 的辅助模块。

Helper 模块可以被看作是中介体或中介体,它们为其他模块抽象公共服务,从而使依赖代码都在一个地方抽象,而不会重复。它们还可以通过将不需要的或不相关的函数转移到合适的辅助模块来帮助模块增加其内聚性。

使用遗传技术

当我们发现类中出现类似的代码或功能时,这可能是重构它们以创建类层次结构的好时机,以便通过继承共享公共代码。

让我们看一看下面的例子:

""" 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)

下面是另一个模块urlrank,它对 URL 执行相同的功能:

    """ 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)

这两个模块都执行类似的功能,根据给定关键字在其中出现的程度对一组输入数据进行排序。随着时间的推移,这些类可能会开发出许多类似的功能,组织最终可能会产生大量重复的代码,从而降低可修改性。

我们可以使用继承来帮助我们抽象出父类中的公共逻辑。下面是名为RankBase的父类,它通过将所有公共代码移到自身来实现这一点:

""" 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):
            # print text
            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)

我们现在重写了textrankurlrank模块,以利用父类中的逻辑:

""" 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)

以下是对urlrank模块的修改清单:

""" 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)

重构不仅减少了每个模块中代码的大小,而且还通过将公共代码抽象为可以独立开发的父模块/类,改善了类的可修改性。

使用后期绑定技术

延迟绑定是指按照代码执行顺序尽可能延迟将值绑定到参数的做法。后期绑定允许程序员通过使用多种技术将影响代码执行的因素,以及因此影响代码执行结果和性能的因素推迟到以后。

可以使用的一些后期绑定技术如下:

  • 插件机制:这种技术不是静态地将模块绑定在一起,从而增加耦合,而是使用在运行时解析的值来加载插件,插件执行特定的依赖代码。插件可以是 Python 模块,其名称在运行时完成的计算期间获取,或者通过从数据库查询或配置文件加载的 ID 或变量名获取。

  • 代理/注册表查找服务:一些服务可以完全延迟给代理,代理根据需要从注册表中查找服务名称,并动态调用并返回结果。例如货币兑换服务,它接受特定的货币转换作为输入(比如 USDINR),并在运行时为其动态查找和配置服务,因此始终只需要在系统上执行相同的代码。由于系统上没有随输入而变化的依赖代码,因此如果转换逻辑发生变化,系统将不受任何所需更改的影响,因为它被延迟到外部服务。

  • 通知服务:当对象的值发生变化或发布事件时通知订阅者的发布/订阅机制可用于将系统与易失性参数及其值解耦。这些系统不需要在内部跟踪这些变量/对象的变化,因为这些变量/对象可能需要大量依赖的代码和结构,而是让它们的客户端不受系统中影响和触发对象内部行为的变化的影响,而只将它们绑定到外部 API,它只是将更改的值通知客户机。

  • 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.

    这种方法可以与面向对象模式(如工厂)相结合,工厂在运行时根据名称或 ID 创建所需的对象,从而使依赖这些对象的客户端免受任何内部更改的影响,从而提高其可修改性。

  • 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 which don't want their code to be modified when the code for creation of a dependent object changes.

    当这些方法与部署/配置时间或动态绑定(使用查找服务)结合使用时,可以极大地提高系统的灵活性并帮助其可修改性。

我们将在本书后面的一章中介绍 Python 模式的示例。

指标——静态分析工具

静态代码分析工具可以提供关于代码静态属性的丰富信息摘要,可以深入了解代码的复杂性和可修改性/可读性等方面。

Python 有很多第三方工具支持,这有助于度量 Python 代码的静态方面,例如:

  • 符合 PEP-8 等编码标准
  • 代码复杂性度量,如 McCabe 度量
  • 代码中的错误,如语法错误、缩进问题、缺少导入、变量覆盖等
  • 代码中的逻辑问题
  • 代码气味

以下是 Python 生态系统中一些最流行的工具,它们可以执行此类静态分析:

  • Pylint:Pylint 是 Python 代码的静态检查器,可以检测一系列编码错误、代码气味和样式错误。Pylint 使用接近 PEP-8 的样式。Pylint 的较新版本还提供有关代码复杂性的统计信息,并可以打印报告。Pylint 要求在检查代码之前先执行代码。您可以参考http://pylint.org 链接。
  • Pyflakes:Pyflakes 是一个比 Pylint 更新的项目。它与 Pylint 的不同之处在于,它不需要在检查错误之前执行代码。Pyflakes 不检查编码样式错误,只在代码中执行逻辑检查。您可以参考https://launchpad.net/pyflakes 链接。
  • McCabe:这是一个脚本,用于检查并打印关于代码 McCabe 复杂性的报告。您可以参考https://pypi.python.org/pypi/mccabe 链接。
  • Pycodestyle:Pycodestyle 是一种根据 PEP-8 指南检查 Python 代码的工具。这个工具早些时候被称为 PEP-8。参考https://github.com/PyCQA/pycodestyle 链接。
  • Flake8:Flake8 是 Pyflakes、McCabe 和 pycodestyle 工具的包装器,可以执行许多检查,包括这些工具提供的检查。参考https://gitlab.com/pycqa/flake8/ 链接。

什么是代码气味?

Code 气味是代码深层问题的表面症状。它们通常表示设计中的问题,这些问题可能会在将来导致 bug,或者对特定代码段的开发产生负面影响。

代码气味本身不是 bug,但它们是一种模式,表明代码中采用的解决问题的方法是不正确的,应该通过重构来修复。

一些常见的代码气味如下所示:

在班级一级:

  • 上帝对象:一个试图做太多事情的类。简言之,这个类缺乏任何类型的内聚性。
  • 常量类:一个只不过是常量集合的类,在其他地方使用,因此理想情况下不应该属于这里。
  • 拒绝遗赠:一个不遵守基类契约的类,因此违反了继承的替代原则。
  • Freeloader:一个函数太少的类,几乎什么都不做,附加值也很少。
  • 特征嫉妒:过度依赖另一类方法的类,表示耦合度高。

在方法/功能层面:

  • 长方法:一种方法或功能变得太大、太复杂。
  • 参数蠕变:函数或方法的参数太多。这使得函数的调用能力和可测试性变得困难。
  • 圈复杂度:一种具有太多分支或循环的函数或方法,它会产生难以遵循的复杂逻辑,并可能导致微妙的错误。这样的函数应该重构并分解为多个函数,或者重写逻辑以避免太多分支。
  • 超长或超短标识符:一种使用超长或超短变量名的函数,其目的不清楚。这同样适用于函数名。

与代码气味相关的反模式是设计气味,它是系统设计中的表面症状,指示架构中潜在的更深层次的问题。

圈复杂度——McCabe 度量

圈复杂度是计算机程序复杂度的度量。它被计算为从开始到结束通过程序源代码的线性独立路径的数量。

对于一段根本没有分支的代码,比如下一个给定的代码,圈复杂度将是1,因为代码中只有一条路径。

""" Module power.py """

def power(x, y):
    """ Return power of x to y """
    return x^y

一段具有一个分支的代码(如以下分支)的复杂性为 2:

""" Module factorial.py """

def factorial(n):
    """ Return factorial of n """
    if n == 0:
        return 1
    else:
        return n*factorial(n-1)

圈复杂度的使用是由 Thomas J.McCabe 在 1976 年开发的,它使用代码的控制图作为度量。因此,它也被称为 McCabe 复杂性或 McCabe 指数。

为了测量度量,可以将控制图描绘为定向图,其中节点表示程序的块,边表示从一个块到另一个块的控制流。

关于程序的控制图,McCabe 复杂性可表示为:

M=E-N+2P

哪里

E=>图中的边数,

N=>图中节点数

P=>图中连接组件的数量

在 Python 中,Ned Batcheldor 编写的包mccabe可用于测量程序的圈复杂度。它可以作为独立模块使用,也可以作为 Flake8 或 Pylint 等程序的插件使用。

例如,下面是我们如何度量前面给出的两个代码段的圈复杂度:

Cyclomatic complexity – the McCabe metric

一些示例 Python 程序的 McCabe 度量

参数–min告诉mccabe模块从给定的 McCabe 指数开始测量和报告。

量度测试

现在让我们尝试前面提到的几种工具,并在一个示例模块上使用它们,以了解这些工具报告的信息类型。

以下各节的目的不是教您如何使用这些工具或其命令行选项,读者可以通过工具文档了解这些工具或命令行选项。相反,其目的是探索这些工具提供的关于代码样式、逻辑和其他问题的信息的深度和丰富性。

为了进行该测试,使用了以下人为模块示例。它是有目的地编写的,有很多编码错误、样式错误和编码气味。

由于我们正在使用的工具按行号列出错误,因此代码中显示了带编号的行,因此很容易按照工具的输出返回代码:

     1  """
     2  Module metrictest.py
     3  
     4  Metric example - Module which is used as a testbed for static checkers.
     5  This is a mix of different functions and classes doing different things.
     6  
     7  """
     8  import random
     9  
    10  def fn(x, y):
    11      """ A function which performs a sum """
    12      return x + y
    13  
    14  def find_optimal_route_to_my_office_from_home(start_time,
    15                                      expected_time,
    16                                      favorite_route='SBS1K',
    17                                      favorite_option='bus'):
    18  
    19      # If I am very late, always drive.
    20      d = (expected_timestart_time).total_seconds()/60.0
    21  
    22      if d<=30:
    23          return 'car'
    24  
    25      # If d>30 but <45, first drive then take metro
    26      if d>30 and d<45:
    27          return ('car', 'metro')
    28  
    29      # If d>45 there are a combination of options
    30      if d>45:
    31          if d<60:
    32              # First volvo,then connecting bus
    33              return ('bus:335E','bus:connector')
    34          elif d>80:
    35              # Might as well go by normal bus
    36              return random.choice(('bus:330','bus:331',':'.join((favorite_option,
    37                           favorite_route))))
    38          elif d>90:
    39              # Relax and choose favorite route
    40              return ':'.join((favorite_option,
    41                               favorite_route))
    42  
    43      
    44  class C(object):
    45      """ A class which does almost nothing """
    46  
    47      def __init__(self, x,y):
    48          self.x = x
    49          self.y = y
    50          
    51      def f(self):
    52          pass
    53  
    54      def g(self, x, y):
    55  
    56          if self.x>x:
    57              return self.x+self.y
    58          elif x>self.x:
    59              return x+ self.y
    60  
    61  class D(C):
    62      """ D class """
    63  
    64      def __init__(self, x):
    65          self.x = x
    66  
    67      def f(self, x,y):
    68          if x>y:
    69              return x-y
    70          else:
    71              return x+y
    72  
    73      def g(self, y):
    74  
    75          if self.x>y:
    76              return self.x+y
    77          else:
    78              return y-self.x

运行静态检查程序

让我们看看派林对我们这段看起来相当可怕的测试代码有什么看法。

Pylint 打印了很多样式错误,但是本例的目的是关注逻辑问题和代码气味,日志仅从这些报告开始显示。

$ pylintreports=n metrictest.py

以下是两个屏幕截图中捕获的详细输出:

Running Static Checkers

图 2。公制测试程序的 Pylint 输出(第 1 页)

请看另一个屏幕截图:

Running Static Checkers

公制测试程序的 Pylint 输出(第 2 页)

让我们关注 Pylint 报告中非常有趣的最后 10-20 行,跳过前面的样式和惯例警告。

以下是错误,分类到表中。我们跳过了类似的事件以保持表的简短:

|

错误

|

事件

|

解释

|

代码类型气味

| | --- | --- | --- | --- | | 无效的函数名 | 功能fn | 名称fn太短,无法使用解释函数的作用 | 标识符太短 | | 无效的变量名 | fn函数f的变量xy | 名称xy太短,无法表示变量所代表的内容 | 标识符太短 | | 无效的函数名 | 功能名称find_optimal_route_to_my_office_from_home | 函数名太长 | 标识符太长 | | 无效的变量名 | 函数find_optimal的变量d。。。 | 名称d太短,无法表示变量所代表的内容 | 标识符太短 | | 无效的类名 | 类别C | 名字C没有告诉任何关于这个班级的事情 | 标识符太短 | | 无效的方法名 | 类别C:方法f | 名称f太短,无法解释其作用 | 标识符太短 | | 无效的__init__方法 | 类别D:方法__init__ | 不调用基类的__init__ | 打破与基类的契约 | | f 的参数在D类和C类中不同 | 类别D:方法f | 方法签名打破了与基类签名的契约 | 拒绝遗产 | | g的参数在D类和C类中不同 | 类别D:方法g | 方法签名打破了与基类签名的契约 | 拒绝遗产 |

如您所见,Pylint 检测到了许多代码气味,我们在上一节中讨论了这些气味。其中最有趣的是它如何检测到长得离谱的函数名,以及子类 D 如何在其__init__和其他方法中破坏与基类C的约定。

让我们看看flake8对我们的代码有什么启示。我们将运行它以报告错误计数的统计和摘要:

$  flake8 --statistics --count metrictest.py

Running Static Checkers

图 4。第 8 页公制测试程序的静态检查输出

正如所期望的,对于一个主要遵循 PEP-8 约定编写的工具,报告的错误都是样式和约定错误。这些错误有助于提高代码的可读性,并使其更接近 PEP-8 的风格指南。

通过将选项–show-pep8传递到第 8 页,您可以获得有关 PEP-8 测试的更多信息。

现在是检查代码复杂性的好时机。首先我们直接使用mccabe,然后通过 Flake8 调用:

Running Static Checkers

度量测试程序的 mccabe 复杂性

正如预期的那样,办公路线功能的复杂性太高,因为它有太多的分支机构和支行。

由于flake8打印的样式错误太多,我们将专门针对复杂性报告进行 grep:

Running Static Checkers

如第 8 条所述,公制测试程序的 mccabe 复杂性

正如所料,Flake8 报告函数 find_optimal_route_to_my_office_from_home太复杂。

还有一种方法可以将mccabe作为来自 Pylint 的插件运行,但由于它涉及一些配置步骤,我们将不在这里介绍。

最后一步,让我们在代码上运行pyflakes

Running Static Checkers

公制测试代码上 pyflakes 的静态分析输出

没有输出!因此 Pyflakes 没有发现代码的问题。原因是 PyFlakes 是一个基本的检查器,它只报告明显的语法和逻辑错误、未使用的导入、缺少变量名等。

让我们在代码中添加一些错误,然后重新运行 Pyflakes。以下是带有行号的调整代码:

     1  """
     2  Module metrictest.py
     3  
     4  Metric example - Module which is used as a testbed for static checkers.
     5  This is a mix of different functions and classes doing different things.
     6  
     7  """
     8  import sys
     9  
    10  def fn(x, y):
    11      """ A function which performs a sum """
    12      return x + y
    13  
    14  def find_optimal_route_to_my_office_from_home(start_time,
    15                                      expected_time,
    16                                      favorite_route='SBS1K',
    17                                      favorite_option='bus'):
    18  
    19      # If I am very late, always drive.
    20      d = (expected_timestart_time).total_seconds()/60.0
    21  
    22      if d<=30:
    23          return 'car'
    24  
    25      # If d>30 but <45, first drive then take metro
    26      if d>30 and d<45:
    27          return ('car', 'metro')
    28  
    29      # If d>45 there are a combination of options
    30      if d>45:
    31          if d<60:
    32              # First volvo,then connecting bus
    33              return ('bus:335E','bus:connector')
    34          elif d>80:
    35              # Might as well go by normal bus
    36              return random.choice(('bus:330','bus:331',':'.join((favorite_option,
    37                           favorite_route))))
    38          elif d>90:
    39              # Relax and choose favorite route
    40              return ':'.join((favorite_option,
    41                               favorite_route))
    42  
    43      
    44  class C(object):
    45      """ A class which does almost nothing """
    46  
    47      def __init__(self, x,y):
    48          self.x = x
    49          self.y = y
    50          
    51      def f(self):
    52          pass
    53  
    54      def g(self, x, y):
    55  
    56          if self.x>x:
    57              return self.x+self.y
    58          elif x>self.x:
    59              return x+ self.y
    60  
    61  class D(C):
    62      """ D class """
    63  
    64      def __init__(self, x):
    65          self.x = x
    66  
    67      def f(self, x,y):
    68          if x>y:
    69              return x-y
    70          else:
    71              return x+y
    72  
    73      def g(self, y):
    74  
    75          if self.x>y:
    76              return self.x+y
    77          else:
    78              return y-self.x
    79  
    80  def myfunc(a, b):
    81      if a>b:
    82          return c
    83      else:
    84          return a

请看以下输出:

Running Static Checkers

图 8。修改后,公制测试代码上 pyflakes 的静态分析输出

Pyflakes 现在返回一些有用的信息,包括缺少的名称(随机)、未使用的导入(sys)和未定义的名称(新引入的函数myfunc中的变量c)。因此,它确实对代码执行了一些有用的静态分析。例如,关于缺少的和未定义的名称的信息有助于修复前面代码中的明显错误。

提示

最好在代码上运行 Pylint 和/或 Pyflakes,以便在编写代码后报告并找出逻辑和语法错误。要运行 Pylint 仅报告错误,请使用-E 选项。要运行 Pyflakes,只需遵循前面的示例。

重构代码

现在我们已经了解了如何使用静态工具来报告 Python 代码中的各种错误和问题,让我们做一个重构代码的简单练习。我们将把编写糟糕的度量测试模块作为用例(它的第一个版本),并执行一些重构步骤。

以下是我们在进行重构时将遵循的大致指导原则:

  1. 首先修复复杂的代码:这会让很多代码不受干扰,通常情况下,当一段复杂的代码被重构时,我们最终会减少代码行数。这总体上提高了代码质量,并减少了代码气味。您可能正在这里创建新的函数或类,因此首先执行此步骤总是有帮助的。
  2. 现在就对代码进行分析:最好在这一步运行复杂性检查器,看看代码类/模块或函数的总体复杂性是如何降低的。如果没有,请再次迭代。
  3. 下一步修复代码气味:下一步修复代码气味类、函数或模块的任何问题。这将使您的代码具有更好的形状,并且还改进了总体语义。
  4. 运行检查程序:现在在代码上运行 Pylint 之类的检查程序,并获取代码气味报告。理想情况下,它们应该接近于零,或者比原始值减少很多。
  5. 修复低挂果实:修复低挂果实,如代码样式和惯例错误,最后一个。这是因为,在重构过程中,为了降低复杂性和代码气味,通常会引入或删除大量代码。因此,在早期阶段尝试和改进编码约定是没有意义的。
  6. 使用工具执行最终检查:您可以运行 Pylint 来检查代码气味,运行 Flake8 来检查 PEP-8 约定,运行 Pyflakes 来捕获逻辑、语法和缺少的变量问题。

在下一节中,我们将逐步演示如何使用这种方法修复编写糟糕的度量测试模块。

重构代码——修复复杂性

大部分复杂性都存在于 office route 函数中,所以让我们尝试修复它。以下是重写版本(此处仅显示该函数):

def find_optimal_route_to_my_office_from_home(start_time,
                                              expected_time,
                                              favorite_route='SBS1K',
                                              favorite_option='bus'):

        # If I am very late, always drive.
        d = (expected_time - start_time).total_seconds()/60.0

        if d<=30:
            return 'car'
        elif d<45:
            return ('car', 'metro')
        elif d<60:
            # First volvo,then connecting bus
            return ('bus:335E','bus:connector')
        elif d>80:
            # Might as well go by normal bus
            return random.choice(('bus:330','bus:331',':'.join((favorite_option,
                                         favorite_route))))
        # Relax and choose favorite route
        return ':'.join((favorite_option, favorite_route))

在前面的重写中,我们去掉了多余的 if。。其他条件。现在让我们检查一下复杂性:

Refactoring code – fixing complexity

重构步骤#1 后度量测试程序的 mccabe 度量

我们能够将复杂性从7降低到5。我们能做得更好吗?

在下面的代码中,代码被重写为使用值的范围作为键,使用相应的返回值作为值。这大大简化了我们的代码。此外,最后的早期默认返回值永远不会被拾取,因此现在将其删除,从而去掉一个分支,并将复杂性降低一倍。代码变得简单得多:

deffind_optimal_route_to_my_office_from_home(start_time,
    expected_time,
    favorite_route='SBS1K',
    favorite_option='bus'):

    # If I am very late, always drive.
    d = (expected_timestart_time).total_seconds()/60.0
    options = { range(0,30): 'car',
    range(30, 45): ('car','metro'),
    range(45, 60): ('bus:335E','bus:connector') }

if d<80:
# Pick the range it falls into
for drange in options:
    if d in drange:
    return drange[d]

    # Might as well go by normal bus
    return random.choice(('bus:330','bus:331',':'.join((favorite_option, favorite_route))))

Refactoring code – fixing complexity

重构步骤 2 后度量测试程序的 mccabe 度量

函数的复杂度现在降低到4,这是可以管理的。

重构代码-修复代码气味

下一步是修复代码气味。谢天谢地,我们从前面的分析中得到了一个非常好的列表,所以这并不太困难。大多数情况下,我们需要更改函数名、变量名,还需要修复从子类到基类的契约问题。

以下是包含所有修复程序的代码:

""" Module metrictest.py - testing static quality metrics of Python code """

import random

def sum_fn(xnum, ynum):
    """ A function which performs a sum """

    return xnum + ynum

def find_optimal_route(start_time,
                       expected_time,
                       favorite_route='SBS1K',
                       favorite_option='bus'):
    """ Find optimal route for me to go from home to office """

    # Time difference in minutes - inputs must be datetime instances
    tdiff = (expected_time - start_time).total_seconds()/60.0

    options = {range(0, 30): 'car',
               range(30, 45): ('car', 'metro'),
               range(45, 60): ('bus:335E', 'bus:connector')}

    if tdiff < 80:
        # Pick the range it falls into
        for drange in options:
            if tdiff in drange:
                return drange[tdiff]

    # Might as well go by normal bus
    return random.choice(('bus:330', 'bus:331', ':'.join((favorite_option,
                                    favorite_route))))

class MiscClassC(object):
    """ A miscellaneous class with some utility methods """

    def __init__(self, xnum, ynum):
        self.xnum = xnum
        self.ynum = ynum

    def compare_and_sum(self, xnum=0, ynum=0):
        """ Compare local and argument variables
        and perform some sums """

        if self.xnum > xnum:
            return self.xnum + self.ynum
        else:
            return xnum + self.ynum

class MiscClassD(MiscClassC):
    """ Sub-class of MiscClassC overriding some methods """

    def __init__(self, xnum, ynum=0):
        super(MiscClassD, self).__init__(xnum, ynum)

    def some_func(self, xnum, ynum):
        """ A function which does summing """

        if xnum > ynum:
            return xnum - ynum
        else:
            return xnum + ynum

    def compare_and_sum(self, xnum=0, ynum=0):
        """ Compare local and argument variables
        and perform some sums """

        if self.xnum > ynum:
            return self.xnum + ynum
        else: 
            return ynum - self.xnum

让我们在这段代码上运行 Pylint,看看它这次输出了什么:

Refactoring code - fixing code smells

重构度量测试程序的 Pylint 输出

您可以看到,除了抱怨缺少public方法之外,代码气味的数量已经降到了接近零,并且洞察到类MiscClassD的方法some_func可以是一个函数,因为它不使用类的任何属性。

我们使用选项–reports=n调用了 Pylint,以避免 Pylint 打印其摘要报告,因为这会使整个输出过长,无法在此处显示。无需任何参数即可通过调用 Pylint 来启用这些报告。

重构代码-修复样式和编码问题

既然我们已经修复了主要的代码问题,下一步就是修复代码样式和约定错误。但是,为了缩短本书中本练习的步骤数和要打印的代码量,这已经与最后一步合并,正如您可能从 Pylint 的输出中猜到的那样。

除了一些空白警告,所有问题都已修复。

这就完成了我们的重构练习。

总结

在本章中,我们研究了可修改性的建筑质量属性及其各个方面。我们详细讨论了可读性,包括可读性反模式以及一些编码反模式。在讨论中,我们了解到 Python 是一种为可读性而编写的语言。

我们研究了提高代码可读性的各种技术,花了一些时间研究了代码注释的各个方面,并研究了 Python 中函数、类和模块级别的文档字符串。我们还研究了 Python 的官方编码惯例指南 PEP-8,并了解到持续重构代码对于保持其可修改性和降低长期维护成本非常重要。

然后,我们研究了代码注释的一些经验法则,并继续讨论了可修改性的基本原理,即代码的耦合和内聚。我们通过几个例子研究了耦合和内聚的不同情况。然后,我们继续讨论改进代码可修改性的策略,如提供显式接口或 API、避免双向依赖、将公共服务抽象到帮助器模块以及使用继承技术。我们看了一个例子,在这个例子中,我们通过继承重构了一个类层次结构,以抽象出公共代码并改进系统的可修改性。

最后,我们列出了在 Python 中提供静态代码度量的不同工具,如 PyLint、Flake8、PyFlakes 等。我们通过几个例子了解了 McCabe 圈复杂度。我们还了解了代码的气味,并执行了重构练习,以分阶段提高代码的质量。

在下一章中,我们将讨论软件架构的另一个重要质量属性,即可测试性。