В отличие от того, что было сказано в других ответах, я не думаю, что это вопрос личного вкуса.
Как вы написали, DataManipulator2
кажется на первый взгляд легче тестировать.(Но, как сказал @AliFaizan, не так просто модульный тест функция, для которой требуется соединение с базой данных.) И, кажется, проще тестировать, потому что это без состояний .Класс без состояния автоматически не проще для тестирования, но его легче понять: для одного входа вы всегда получаете один и тот же вывод.
Но это не единственная точка: с DataManipulator2
, порядокдействия в 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()
Это неплохо, а 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))
Есть еще один момент: имя файла.Попытайтесь предпочесть файловый объект именам файлов в качестве аргументов, потому что вы можете протестировать свой код с помощью 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))
с макетами объектов ,теперь очень легко протестировать поведение классов.
Давайте подведем итог преимуществам этого кода:
- порядок действий гарантирован (как в
DataManipulator2
) - метод
run
настолько чист, как и должно быть (как в DataManipulator2
) - код является модульным: вы можете создать новый
Transformer
или новый DataSample
(загрузить из БД и сохранить, например, в CSV-файл) - код тестируемый: каждый метод общедоступен (в смысле Python), но API остаются простыми.
КонечноЭто действительно (старый стиль) Java-подобный.В python вы можете просто передать функцию transform
вместо экземпляра класса Transformer
.Но как только ваш transform
начинает становиться сложным, класс - хорошее решение.