tvg*_*iek 9 python design-patterns
我经常发现自己遇到了同样的问题。一个常见的模式是我创建一个执行某些操作的类。例如。加载数据,转换/清除数据,保存数据。然后出现问题,如何传递/保存中间数据。查看以下2个选项:
import read_csv_as_string, store_data_to_database
class DataManipulator:
''' Intermediate data states are saved in self.results'''
def __init__(self):
self.results = None
def load_data(self):
'''do stuff to load data, set self.results'''
self.results = read_csv_as_string('some_file.csv')
def transform(self):
''' transforms data, eg get first 10 chars'''
transformed = self.results[:10]
self.results = transformed
def save_data(self):
''' stores string to database'''
store_data_to_database(self.results)
def run(self):
self.load_data()
self.transform()
self.save_data()
DataManipulator().run()
class DataManipulator2:
''' Intermediate data states are not saved but passed along'''
def load_data(self):
''' do stuff to load data, return results'''
return read_csv_as_string('some_file.csv')
def transform(self, results):
''' transforms data, eg get first 10 chars'''
return results[:10]
def save_data(self, data):
''' stores string to database'''
store_data_to_database(data)
def run(self):
results = self.load_data()
trasformed_results = self.transform(results)
self.save_data(trasformed_results)
DataManipulator2().run()
Run Code Online (Sandbox Code Playgroud)
现在,对于编写测试,我发现DataManipulator2更好,因为可以更轻松地独立测试功能。同时,我也喜欢DataManipulator的干净运行功能。什么是最pythonic的方式?
与其他答案中所说的不同,我认为这不是个人品味的问题。
正如你所写的,DataManipulator2乍一看,似乎更容易测试。(但正如@AliFaizan 所说,对需要数据库连接的函数进行单元测试并不容易。)而且测试似乎更容易,因为它是无状态的。无状态类并不自动更容易测试,但更容易理解:对于一个输入,你总是得到相同的输出。
但这还不是唯一的一点:withDataManipulator2中的动作顺序run不能错,因为每个函数都会将一些数据传递给下一个函数,而没有这些数据,下一个函数就无法继续。对于静态(和强)类型语言,这会更明显,因为您甚至无法编译错误的run函数。
相反,DataManipulator不容易测试,有状态并且不能确保操作的顺序。这就是方法DataManipulator.run如此干净的原因。它的事件太干净了,因为它的实现隐藏了一些非常重要的东西:函数调用是有序的。
因此,我的回答是:更喜欢DataManipulator2实现而不是DataManipulator实现。
但DataManipulator2完美吗?是和否。对于快速而肮脏的实现,这就是要走的路。但让我们尝试走得更远。
您需要该函数run是公开的,但是load_data,save_data并且transform没有理由公开(“公开”我的意思是:没有用下划线标记为实现细节)。如果您用下划线标记它们,则它们不再是合同的一部分,并且您对测试它们感到不舒服。为什么?因为尽管可能存在测试失败,但实现可能会在不破坏类契约的情况下发生变化。这是一个残酷的困境:要么您的类DataManipulator2具有正确的 API,要么无法完全测试。
尽管如此,这些函数应该是可测试的,但作为另一个类的 API 的一部分。考虑一个三层架构:
load_data并且save_data在数据层transform 是在业务层。run通话中表示层让我们尝试实现这一点:
class DataManipulator3:
def __init__(self, data_store, transformer):
self._data_store = data_store
self._transformer = transformer
def run(self):
results = self._data_store.load()
trasformed_results = self._transformer.transform(results)
self._data_store.save(transformed_results)
class DataStore:
def load(self):
''' do stuff to load data, return results'''
return read_csv_as_string('some_file.csv')
def save(self, data):
''' stores string to database'''
store_data_to_database(data)
class Transformer:
def transform(self, results):
''' transforms data, eg get first 10 chars'''
return results[:10]
DataManipulator3(DataStore(), Transformer()).run()
Run Code Online (Sandbox Code Playgroud)
这还不错,而且Transformer很容易测试。但:
DataStore不得心应手:读取文件被掩埋在代码和数据库了。DataManipulator应该能够运行Transformer在多个数据样本。因此,另一个版本解决了这些问题:
class DataManipulator4:
def __init__(self, transformer):
self._transformer = transformer
def run(self, data_sample):
data = data_sample.load()
results = self._transformer.transform(data)
self.data_sample.save(results)
class DataSample:
def __init__(self, filename, connection)
self._filename = filename
self._connection = connection
def load(self):
''' do stuff to load data, return results'''
return read_csv_as_string(self._filename)
def save(self, data):
''' stores string to database'''
store_data_to_database(self._connection, data)
with get_db_connection() as conn:
DataManipulator4(Transformer()).run(DataSample('some_file.csv', conn))
Run Code Online (Sandbox Code Playgroud)
还有一点:文件名。尝试更喜欢文件类对象而不是文件名作为参数,因为您可以使用io模块测试您的代码:
class DataSample2:
def __init__(self, file, connection)
self._file = file
self._connection = connection
...
dm = DataManipulator4(Transformer())
with get_db_connection() as conn, open('some_file.csv') as f:
dm.run(DataSample2(f, conn))
Run Code Online (Sandbox Code Playgroud)
使用模拟对象,现在可以很容易地测试类的行为。
我们总结一下这段代码的优点:
DataManipulator2)run方法应该是干净的(如DataManipulator2)Transformer或新的DataSample(例如从数据库加载并保存到 csv 文件)当然,这真的是(旧式)Java 式的。在python中,您可以简单地传递函数transform而不是Transformer类的实例。但是一旦您transform开始变得复杂,类就是一个很好的解决方案。
最Pythonic的方式是什么?
Python 支持多种范式。第二种形式更接近功能性,第一种形式更具命令性。这完全是一个偏好问题,没有上下文。
不过,我有第三个建议,因为我喜欢当对象没有状态时可以避免。这很容易测试,并且避免了复杂错误方法中的各种问题run()(例如加载之前转换、调用两次转换、不转换而保存等)。
class DataTransformer:
@classmethod
def from_csv(cls, some_file):
'''Because I don't like __init__ to do logic, it's harmful for testability,
but at the same time this is needed data for proper initialization
'''
return cls(read_csv_as_string(some_file))
def __init__(self, raw_data):
''' Feel free to init with bogus test data '''
self.raw_data = raw_data
def transform(self):
''' Returning the data instead of a ContentSaver is a less coupled design (suppose you add more exporters)'''
return self.raw_data[:10]
class ContentSaver:
'''Having a different class makes sense now the data is transformed:
it's a different type of data, from a logical standpoint.'''
def __init__(self, some_content):
self.content = some_content
def save_data(self):
store_data_to_database(self.content)
def run():
'''Note this code part isn't easily testable, so it's better if possible mistakes are made fewer.'''
transformer = DataTransformer.from_csv('some_file')
writer = ContentSaver(transformer.transform())
# Possible further uses of transformer and writer without care of order
writer.save_data()
Run Code Online (Sandbox Code Playgroud)
在我的对象生命周期中的任何时候,它们都保存一致类型的初始化数据。run()这使得它们可测试、不易出错并且更可能在不同的实现中(不仅仅是)有用。
由于列出的所有好处,我认为值得在管道的每个结构化步骤(DataCleaner 等)编写一个类,因为随着代码的增长,它会更容易维护。
| 归档时间: |
|
| 查看次数: |
436 次 |
| 最近记录: |