我们在一个Web应用程序中,需要对来自于用户输入或数据库查询的数据进行验证,然后在物理资源上执行某些操作。其设计可用下图简单表示:
用户输入 <=> 模型对象 <=> 数据库存储
当数据来源于用户输入时,需要进行验证;但当数据来源于数据库查询结果时,则不必验证,因为如果数据库中存在该记录,那么这些被验证字段的值一定已经存在。
我们试图重构代码,以便将验证操作放在对象的构造函数中进行,而不是使用传统的单独的验证例程。
我们想知道哪种方法更好?(方法1(传统)和方法2的基本区别在于,方法1中的验证不是强制的,并且与实例化对象无关;而方法2则将验证与实例化对象绑定在一起,并强制要求所有请求进行验证。)
下面是设计1和设计2的两个代码示例:
方法1:
# 处理单个请求。
# 步骤:1. 验证所有传入的数据。2. 实例化对象。
ValidateAttributes(request) # raise Exceptions if failed
resource = Resource(**request)
方法2:
# 将其提取出来,因为它与对象无关。
# raise Exceptions if some required params missing.
# steps: 1. Check whether its a batching request. 2. instantiate the object.
# (validations are performed inside the constructor)
CheckIfBatchRequest(request)
resource = Resource(**request) # raise Exceptions when validations failed
对于批处理请求:
方法1:
# 步骤:1. 验证每个请求,如果发现任何错误,则返回错误给客户端。
# 2. 执行对象实例化和创建过程。异常会被捕获。
# 3. 完成所有操作后,通过电子邮件发送任何错误。
for request in batch_requests:
try:
ValidateAttribute(request)
except SomeException, e:
return ErrorPage(e)
errors = []
for request in batch_requests:
try:
CreatResource(Resource(**request), request)
except CreationError, e:
errors.append('failed to create with error: %s', e)
email(errors)
方法2:
# 步骤:1. 验证请求中的批处理作业相关数据。
# 2. 如果成功,则为每个请求创建对象并进行验证。
# 3. 如果出现异常,则返回发现的错误,否则,
# 返回包含(对象,请求)对的列表
# 4. 执行创建过程并通过电子邮件发送如果遇到的任何错误。
CheckIfBatchRequest(request)
request_objects = []
for request in batch_requests:
try:
resource = Resource(**request)
except SomeException, e:
return ErrorPage(e)
request_objects.append((resource, request))
email(CreateResource(request_objects)) # the CreateResource will also need to be refactored.
方法1的优点和缺点:
更接近业务逻辑。当对象来自数据库查询时,不进行冗余验证检查。验证例程更容易维护和阅读。
方法2的优点和缺点:
对调用者来说简单干净。即使来自数据库查询,也必须进行验证。验证的可维护性和可读性较差。
2、解决方案
在Django中,在构造函数中进行验证并不是一种“Django式”的方法。由于要验证的数据来自客户端,因此使用新表单(可能采用ModelForm)是验证的最佳方法,因为它将所有关注点都包装到一个API中:它提供了明智的验证默认值(并且可以轻松自定义),此外,模型表单将数据输入端(html表单)与数据提交端(model.save())集成在一起。
不过,听起来你的项目可能是混乱的遗留项目,最初可能没有时间来重写所有表单处理程序以使用新表单。因此,以下是一些想法:
首先,将一些验证放在模型本身中并不是“非Django式”的——毕竟,html表单提交可能不是新数据的唯一来源。你可以覆盖save()方法或使用信号来在保存时清理数据或在数据无效时抛出异常。从长远来看,Django将具备模型验证功能,但目前尚未实现; 在此期间,你应该将此视为一种“安全保障”,以确保你不会向数据库中提交无效数据。换句话说,在提交之前,你仍然需要逐字段验证,以便知道在无效输入时向用户显示什么错误。
我建议你这样做。为每个需要验证的项目创建一个新的表单类,即使你最初没有使用它们。 Malcolm Tredinnick概述了一种使用表单系统中提供的钩子进行模型验证的技术。阅读一下(这非常简单而优雅),然后将其引入你的模型中。一旦定义并使用了新的表单类,你就会发现(如果将现有表单模板和相应的验证剔除,并使用表单框架处理表单POST)这并不难——实际上会极大地简化你的代码。有一个小小的学习曲线,但表单API非常周全,你会感谢它让你的代码变得多么简洁。