简化代码与遵守规则的平衡

在软件开发的世界里,经常面临着一个挑战:如何在遵守既定编程规则的同时,简化代码。规则的存在是为了确保代码的可读性、可维护性和可扩展性,但有时候,它们也会让代码变得冗长和重复。本文将探讨一种方法,它在简化代码的同时,也让对规则有了更深的理解。

代码的重复与简化

在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类型的实体。

如果不想使用默认值,可以提供自己的,但这种情况很少发生。

如果请求中找不到ID怎么办?

有一个额外的布尔参数叫做"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)
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485