在软件开发的世界里,经常面临着一个挑战:如何在遵守既定编程规则的同时,简化代码。规则的存在是为了确保代码的可读性、可维护性和可扩展性,但有时候,它们也会让代码变得冗长和重复。本文将探讨一种方法,它在简化代码的同时,也让对规则有了更深的理解。
在Web开发中,经常需要处理来自Web请求的简单值,并将它们转换为对象。这个过程可能会非常繁琐,尤其是当需要从数据库或缓存中检索对象时。曾经在几乎每个动作方法的开始处进行这样的操作,有时甚至需要两三次。这种重复性的工作让感到疲惫,以至于不得不推迟编写方法的其余部分,直到午餐休息后。
几年前,Scott Hanselman写了一篇关于IPrincipal模型绑定器的文章,这让意识到,模型绑定器不仅仅是用来将表单值组合在一起,它们可以做更多的事情。受到启发,编写了一个名为EntityBinder的自定义模型绑定器。
这个绑定器可能会被一些尊重规则的开发者所不齿,因为绑定器应该知道自己的位置,它们应该位于MVC模式中的M、V和C之间。数据库和业务层应该是它们的禁地。但是,决定不再让控制器充满无聊的重复代码。
这个绑定器的一个缺点是,它做了两件事情而不是一件(违反了单一职责原则)。它既是一个自定义绑定器属性,也是一个Binder。之所以这样做,是为了节省键入[EntityBinder("projectId")]而不是[EntityModelBinder(typeof(EntityBinder), "projectId")]的键入次数。虽然有人认为这样做会降低代码的可维护性,但使用它的代码的可维护性却提高了两倍,这是一个巨大的收获。
然而,不能在这个绑定器中使用依赖注入(在编写时,也不能使用它,因为那是ASP.NETMVC的第一个版本),所以不得不求助于服务定位,而且从未因此遇到任何问题。
将要展示的这个绑定器会查看参数的名称和类型,并尝试猜测包含ID的字段名称和实体的类型。例如,给定如下声明:
public ActionResult AnketaDefinition([EntityBinder] Project project)
它会首先寻找名为"projectId"的请求值,如果找不到,它会寻找名为"Id"的值。然后,它会请求ORM获取具有该ID的Project类型的实体。
如果不想使用默认值,可以提供自己的,但这种情况很少发生。
有一个额外的布尔参数叫做"relaxed",可以用它来决定默认行为。建议抛出异常,以防万一。
代码和示例应用程序可以在GitHub上找到。以下是主要部分:
public object BindModel(ControllerContext controllerContext, ModelBindingContext bindingContext) {
var fieldName = bindingContext.ModelName + "Id";
var result = bindingContext.ValueProvider.GetValue(fieldName);
if (FieldNotFoundOrValueIsEmpty(result)) {
fieldName = _idName;
result = bindingContext.ValueProvider.GetValue(fieldName);
if (FieldNotFoundOrValueIsEmpty(result)) {
if (_relaxed)
return null;
throw new MissingFieldException("Could not find the request parameter: " + fieldName);
}
}
var entityType = _entityType ?? bindingContext.ModelType;
var session = ObjectFactory.GetInstance();
var id = GetId(result, fieldName);
object instance = session.Get(entityType, id);
if (instance == null)
bindingContext.ModelState.AddModelError("null", new HttpException(404, string.Format("Could not find {0} ({1}: {2}", entityType, fieldName, id)));
return instance;
}
首先,使用上述规则查找字段值。接下来,确定实体类型。如果没有在属性中明确设置,类型应该是正在绑定的参数的类型。接下来,使用StructureMap.ObjectFactory获取NHibernate.ISession的实例。可以使用任何喜欢的容器和ORM。剩下的就很简单了。已经省略了处理数组值参数的部分,可以在原始源代码中看到。
像往常一样,更喜欢编写集成测试,因为它可以实际执行ASP.NET请求,这让展示了Ivonna,ASP.NET测试工具的强大。这次,添加了一点模拟(所以它不是100%的集成测试)。因为不想设置NHibernate的所有映射、引导等,只是使用新的Ivonna/CThru Stub语法来模拟数据库访问:
session.Stub("Get").Return(entity);
在这里,有一些粗暴的力量模拟,不需要太多的灵活性,也不想让任何东西"强迫"进入一个所谓的好设计(在编写集成测试时几乎是不可能的)。让任何ISession实例的Get方法返回这个对象,不管参数如何(严格来说,应该验证参数是否如预期,但让不要过度复杂化测试)。以下是完整的测试:
var entity = new Entity();
// 不想设置ORM,
// 所以将伪造ISession
var session = new TestSession();
session.Stub("Get").Return(entity);
// 现在让执行一个Web请求
var response = session.Get("/Sample/Get?entityId=1");
// 检查结果
Assert.AreEqual(entity, response.ActionMethodParameters["entity"]);
public ActionResult Get([EntityBinder()] Entity entity)