重构一词涵盖了广泛的动作和定义。尽管该术语本身通常会导致一个共同的目标,即建立一个更干净,更好的代码库,但仍有许多动作可以视为“重构”,即将依赖项升级到较新版本、重命名您的代码功能,类,模块等、重新组织代码库,将功能从文件移动到另一个文件、改进功能的实现以提高性能、重新格式化代码以使其符合标准。
重构之前
尽管从一种编程语言到另一种编程语言有所区别,但重构时通常仍要保留一些步骤和原则。
第1步:了解代码质量。
运行静态分析工具。静态分析工具为您提供了带有定量统计信息的摘要报告,可以在不时重构时进行比较。
在Python中,我个人使用Prospector,一个静态分析套件包含其他工具,例如pep8,Pylint等。我对Prospector的最喜欢的东西是它能够检测并适应我使用的框架。
请注意,并非所有找到的消息都是有效的,因此有可能出现误报。
步骤2:准备和验证测试用例
未经测试的代码在设计上是不好的代码。您有测试用例吗?如果是这样,它们的完整性如何?这些测试用例是否最新?
为什么在重构之前需要测试用例?您可以辩称,即使进行了简单的更改,也不需要任何东西。好吧,相信我,您会被自己修饰的杰作所回应的意外行为所吸引。
我不会说服您一般而言具有测试代码的重要性。重构之前测试代码是为了确保重构后系统的行为保持一致。
即使有测试用例,这为您提供了开绿灯,您仍然应该验证测试代码。让我来告诉你为什么。
想象一下,在尝试进行重构之前,您将运行可用的任何测试,并且得到以下结果。
但是,当您进一步研究时,您发现了这个测试用例。
“”“
软件包:utils.tests
错误的测试用例
”“”
导入单元测试
UtilsTestCase(unittest.TestCase)类:
def setUp(self):
通过
def test_is_empty(self):
从utils.common.helpers导入is_empty
#问题:没有断言
is_empty('')
is_empty(None)
is_empty('',object_type ='json')
is_empty('{}',object_type ='json')
def test_is_ok(self):
#问题:这是空的。
通过
def test_is_number(self):
#问题:这将通过。
如果不是is_empty(''):
print('Fail')
那么,您看到验证的重点了吗?
此外,测试用例通常是软件的最佳文档。在代码内导航时,它们也是您最好的GPS导航器。
开始重构—重组/重组
在本节中,我将config.py通过重组结构,合并重复的方法,分解和编写测试代码以确保向后兼容性,向您展示一个示例。
config.py 看起来像这样:
“”“
软件包:
重组
”“” 之前的utils.config
CONFIG_NAME = {
“ ENABLE_LOGGING”:“ enable_logging”,
“ LOGGING_LEVEL”:“ logging_level”,
}
def get_logging_level():
通过
ConfigHelper类:
def get(self,config_name,default = None):
通过
def set(self,config_name,value):
通过
def _get_settings_helper(self):
通过
def get_logging_level():
通过
def is_logging_enabled():
通过
类LOGGING_LEVEL:
VERBOSE =“详细”
STANDARD =“标准”
步骤1:编写向后兼容代码
此步骤至关重要。在重构我们的代码之前,必须有测试用例。在这种情况下,我们编写向后兼容的代码,以确保对类/函数/常量的所有引用仍然有效。
在中__init__.py,我们将重新定义类/方法签名:
“”“
在__init__.py
这是向后兼容的代码的生命。
这是为了确保重构的包支持
进口的旧路。
这是不完整的,我们将在后面再讲__init__.py
‘’”
CONFIG_NAME = {}
def get_logging_level(* args,** kwargs):
通过
ConfigHelper类:
def get(self,* args,** kwargs):
pass
def set(自我,* args,** kwargs):
通过
def _get_settings_helper(self):
通过
def get_logging_level(self):
通过
def is_logging_enabled(self):
通过
LOGGING_LEVEL类:
通过
目前__init__.py尚不完整。稍后我们将重新访问该文件。
接下来,我们编写一个测试用例,以确保我们仍然可以像导入旧包一样导入该包。
tests.py中的“””
简单的向后兼容性测试用例
“””
类ConfigHelperCompatibilityTestCase(unittest.TestCase):
def test_backward_compatibility(self):
试试:
从.config导入CONFIG_NAME,
从.config导入LOGGING_LEVEL
从.config导入get_logging_level 从.config导入ConfigHelper,
但ImportError除外,例如e:
self.fail(e.message)
这是一个简单的测试用例,您可能会注意到在该测试用例中未捕获到某些向后兼容性问题。
步骤2:重组套件结构
本节为您提供有关如何重新组织Python软件包的想法。让我们回顾一下config.py我们拥有的:
“”“
软件包:
重组
”“” 之前的utils.config
CONFIG_NAME = {
“ ENABLE_LOGGING”:“ enable_logging”,
“ LOGGING_LEVEL”:“ logging_level”,
}
def get_logging_level():
通过
ConfigHelper类:
def get(self,config_name,default = None):
通过
def set(self,config_name,value):
通过
def _get_settings_helper(self):
通过
def get_logging_level():
通过
def is_logging_enabled():
通过
类LOGGING_LEVEL:
VERBOSE =“详细”
STANDARD =“标准”
你能发现这里有什么问题吗?这很杂乱,在一个文件中有常量,助手,重复的代码。当代码config.py变大时,将难以导航。通过这种凌乱的结构,您可以为循环依赖,隐藏的耦合和优化最美味的意大利面条代码的配方提供一个地方。
您如何重组config.py?对我而言,关注的分离是我的脑海。以下结构通常被认为是构建Python包的一种好习惯(在Django中也使用了该结构)。
config/
├── abstracts.py # All the abstract classes should live here
├── constants.py # All the constants should live here
├── exceptions.py # All custom exceptions should live here
├── helpers.py # All helpers should live here
├── __init__.py # All backward compatible code in here
├── mixins.py # All mixins goes to here
├── serializers.py # All common serializers goes to here
└── tests.py # All `config` related tests should live here
让我们config.py在重构之前重新访问一下,并确定各个代码段应位于何处。
“”“
软件包:
重组
”“” 之前的utils.config
#这看起来像属于utils.config.constants
CONFIG_NAME = {
“” ENABLE_LOGGING“:” enable_logging“,
” LOGGING_LEVEL“:” logging_level“,
}
#看起来像一个辅助函数,转到utils.config.helpers
def get_logging_level():
#看起来像是重复的方法
传递
#这看起来像一个帮助程序类,转到utils.config.helpers
类ConfigHelper:
def get((自我,config_name,默认=无):
通过
def set(self,config_name,value):
通过
def _get_settings_helper(self):
通过
def get_logging_level():
#这看起来像是重复的方法
传递
def is_logging_enabled():
通过
#这看起来像另一个常量,转到utils.config.constants
类LOGGING_LEVEL:
VERBOSE =“详细”
STANDARD =“标准”
在重构之后,config.py应该成为一个Python包config用__init__.py它。
实用程序/
├──config.py # To be removed
└──config/
├── constants.py
├── helpers.py
├── __init__.py
└── tests.py
在utils.config.constants :
“”“
软件包:
重组
”“” 之后的utils.config.constants
#不一致的编程构造
CONFIG_NAME = {
“” ENABLE_LOGGING“:” enable_logging“,
” LOGGING_LEVEL“:” logging_level“,
}
#不一致的编程构造
类LOGGING_LEVEL:
VERBOSE =“详细”
STANDARD =“标准”
在utils.config.helpers :
“”“
软件包:
重组
”“” 之后的utils.config.constants
def get_logging_level():
#这是重复的,删除了此
通行证
ConfigHelper类:
def get(self,config_name,default = None):
通过
def set(self,config_name,value):
通过
def _get_settings_helper(self):
通过
def get_logging_level():
通过
def is_logging_enabled():
通过
步骤3:消除和合并重复项
在中utils.config.helpers ,有2个相似的方法/功能get_logging_level()和ConfigHelper()._get_logging_level() 。假设两个实现都相同,则意味着我们必须找到一个最佳的位置来托管该功能。
在这种情况下,我将删除独立服务器get_logging_level()并将其保留在中ConfigHelper。
“”“
软件包:
删除重复项后的utils.config.constants
”“”
ConfigHelper类:
def get(self,config_name,default = None):
通过
def set(self,config_name,value):
通过
def _get_settings_helper(self):
通过
def get_logging_level():
通过
def is_logging_enabled():
通过
步骤4:分解
我个人是分解爱好者。除了拥有一个类之外ConfigHelper,我们还可以进一步分解ConfigHelper为类和mixin的层次结构。
我们托管AbstractBaseConfigHelper在abstracts.py:
“”“
在abstracts.py
‘’”
从ABC进口ABCMeta
class AbstractBaseConfigHelper:
__metaclass__ = ABCMeta
def get(self,config_name):
通过
def set(self,config_name,value):
通过
def _get_settings_helper(self):
通过
在mixins.py :
“”“
在mixins.py
‘’”
类LoggingConfigMixin:
def is_logging_enabled():
通过
def get_logging_level():
通过
在helpers.py :
“”
在helpers.py中的
“””
类ConfigHelper(
AbstractBaseConfigHelper,
LoggingConfigMixin
):
通过
ConfigHelper 现在分解为多个类和混合。
步骤5:填写我们的向后兼容代码
在步骤1中,我们在中添加了一些代码。__init__.py. 但是,它基本上是不完整的。让我们重新访问该文件:
“”“
在__init__.py
这是向后兼容的代码的生命。
这是为了确保重构的包支持
进口的旧路。
这是不完整的,我们将在后面再讲__init__.py
‘’”
CONFIG_NAME = {}
def get_logging_level(* args,** kwargs):
通过
ConfigHelper类:
def get(self,* args,** kwargs):
pass
def set(自我,* args,** kwargs):
通过
def _get_settings_helper(self):
通过
def get_logging_level(self):
通过
def is_logging_enabled(self):
通过
LOGGING_LEVEL类:
通过
请注意,上面的代码与我们新组织的config软件包之间的桥梁仍然缺失。要建立桥梁,我们将其编辑__init__.py为:
__init__.py中的““”
这是向后兼容代码所在的地方。
这是为了确保重构的程序包支持
旧的导入方式。
“ 。”
从.constants导入CONFIG_NAME,
从.helpers导入LOGGING_LEVEL 从ConfigHelper导入为_ConfigHelper
def get_logging_level(* args,** kwargs):
返回_ConfigHelper()。get_logging_level()
类ConfigHelper(_ConfigHelper):
通过
步骤6:通知开发人员
直到第5步,我们config的重构都正确了。但是,我们需要及时通知开发人员有关更改的信息。有什么简单的方法吗?是。每当开发人员尝试导入过时的函数/类/方法时,我们都可以发出警告消息。例如,我们用装饰器注释旧的函数/类/方法:
“”“
decorators.py
”“”
def refactored_class(消息):
def cls_wrapper(cls):
类包装(cls,对象):
def __init __(self,* args,** kwargs):
warnings.warn(message,FutureWarning)
super(Wrapped,self).__ init __( * args,** kwargs)
return包装的
return cls_wrapper
def重构(消息):
def装饰器(func):
def glow_warning(* args,** kwargs):
warnings.warn(消息,FutureWarning)
返回func(* args,** kwargs)
返回return_warning
返回装饰器
在我们的中__init__.py ,我们添加装饰器,如下所示:
__init__.py中的““”
这是向后兼容代码所在的地方。
这是为了确保重构的程序包支持
旧的导入方式。
“ 。”
从.constants导入CONFIG_NAME,
从.helpers导入LOGGING_LEVEL 从ConfigHelper导入为_ConfigHelper
@refactored('get_logging_level()被重构和弃用。')
def get_logging_level(* args,** kwargs):
返回_ConfigHelper()。get_logging_level()
@refactored_class( 'config.ConfigHelper被重构和弃用请使用config.helpers.ConfigHelper。')
类ConfigHelper(_ConfigHelper):
通过
重组后
重组我们的Python包之后,我们运行测试用例并确保已通过所有测试。
结论
到现在为止,您应该能够了解代码库的质量,了解重构的概念,确定重构的需求,并了解如何重构/重组Python包。想了解更多关于Python的信息,请继续关注。