实践中的模式

通过配置的约定

Jeremy Miller

内容

语言创新的影响
一次并只一旦说它
明智的默认值
通过配置的约定
接下来的步骤

具有您曾经认为有关您的项目多长时间花费的时间花费 wrestling 与纯技术问题与核心问题?一些可能会说所有新的软件技术和技术的最重要目的是减少软件开发人员的意图距离在代码中该目的的实现。并在行业已连续引发允许开发人员需要提供的功能和较少时写入底层基础结构的更多时间的抽象级别,但仍对转。

考虑这一分钟。如果要向您的业务代表显示您的代码假定它们实际上愿意与您阅读该代码,多少该代码将他们关心?这些业务代表将可能只关注有关表示系统的业务功能的代码。该代码是"实质"系统。而在另一方面,它们可能不具有代码如类型声明,配置设置、 Try/Catch 块和泛型的约束少许兴趣。该代码是基础结构或"仪式,"您开发人员,必须经过只是为了提供代码。

在本专栏的以前付款我已经很大程度上探讨基本设计概念和原则,最的已设计 Canon 的一部分非常一些时间。这次我想看一些较新技术可以采用以减少在代码中的仪式。这些概念的一些可能熟悉您,但是我认为所有这些将是主流几年内.NET 的一部分。

语言创新的影响

我要考虑的第一个系数是的编程语言和如何使用编程语言的选择。若要阐明的编程语言的代码中的仪式量影响,让我们看到历史记录的 musty 的页很少的 detour。

这十年中我已构建在 Visual Basic 6.0 中的大型系统。每个方法将类似在 图 1 中看到。每一位的代码是仪式。

图 1 仪式上的位置

Sub FileOperations()
    On Error Goto ErrorHandler

    Dim A as AType
    Dim B as AnotherType

    ' Put some code here

    Set A = Nothing
    Set B = Nothing
    ErrorHandler:
        Err.Raise Err.Number, _
            "FileOperations" & vbNewLine & Err.Source, _
            Err.Description, _
            Err.HelpFile, _
            Err.HelpContext
Exit Sub

我对每个方法使用大量样板化创建更容易调试一个堆栈跟踪的等效。 我还必须取消对方法内的变量 (设置 A = Nothing) 来释放这些对象。 我有一个严格的标准,只是为了验证的错误处理和对象清理已编码正确对每个方法的代码审阅。 所有的仪式的中间,实际的代码在的系统的实质的浮动位置。

闪烁向前今天和现代型的编程语言,如 C# 或 Visual Basic.NET。 目前,垃圾回收消除大部分显式内存用来清理 Visual Basic 6.0 编程中的图标。 在异常的堆栈跟踪已内置到本身,Microsoft.NET Framework,所以不再需要为自己做。 如果您认为有关您移动到.NET Framework 时已消除了所有机械的样板代码,您可以假设.NET 兼容语言因仪式代码中减少中是更加高效和 Visual Basic 6.0 可读。

.NET Framework 是一个很大的跳,但在语言演变不尚未完成。 让我们设想一个 C# 中的简单属性实现传统的方法:

public class ClassWithProperty {
  // Higher Ceremony
  private string _name;
  public string Name {
    get { return _name; }
    set { _name = value; }
  }
}

此代码的实质是只是类 ClassWithProperty 具有名为名称的字符串属性。 让我们快放到 C# 3.0,并执行相同的操作使用的自动属性操作:

public class ClassWithProperty {
  // Lower Ceremony
  public string Name { get; set; }
}

此代码具有与在经典样式属性完全相同的目的,但它需要明显较少的"编译器干扰"代码。

通常情况下,通过,软件开发人员没有编程语言的绝对控制。 尽管我确实认为我们应利用新的编程语言创新或甚至其他语言的讨论您可以使用今天主流 C# 和 Visual Basic 的设计思想的时候。

中心域的验证

.NET Framework 使得在用户界面与 ASP.NET 验证程序控件等工具中添加声明性的字段级验证几乎简单。 完全相同,我认为很有利置于实际的域的模型类的验证逻辑或至少关闭的域服务为这些原因中:

  1. 验证逻辑是一个业务逻辑考虑因素,,我喜欢表明所有业务逻辑都包含业务逻辑类中。
  2. 置于域模型的域服务可能断开用户界面的验证逻辑将减少屏幕上的重复项,并允许相同的验证逻辑,以将执行非用户界面服务 (Web 服务,例如) 公开的应用程序 (说它一次仅一次,但再次) 中。
  3. 还会远更便于在模型中编写对验证逻辑的单位和接受测试,比测试作为用户界面的一部分实现的相同逻辑。

一次并只一旦说它

如果您发现中途岛通过有关的单个数据字段定义的内容需要更改的项目,会发生什么? 在得太许多的情况下,对数据库的小更改将 ripple 通过应用程序,在中间层数据访问的代码的各个部分进行了类似的更改并甚至到用户界面层为了适应一个逻辑更改。

在一个以前的雇主,我们调用 rippling 小的数据域中的效果将更改旋涡式星体反模式。 若要离开小的更改需要获取在被 teleported 都通过层的一个位置。 您可以通过减少不必要将,降低下旋涡式星体效果,并这是第一步。 在更难,但多回报的步骤是查找可以说一次和仅一次。

之后,并仅一旦原则指出有应该只,比如是单一的权威源,事实或策略系统中作为一个整体。 让我们例构建 Web 应用程序的创建、 读取,、 更新,和删除 (CRUD) 功能。 系统的这种是主要关注编辑和存储数据字段。 这些字段需要在屏幕中编辑,验证在服务器上,存储在数据库,并希望验证客户端也以获得更好的用户体验上,但您想要指定的"此字段是必需和/或此字段必须不超过 50 个字符"在代码中一次。

有几个可能需要不同的方法。 您可以使数据库架构主机的定义,并从架构生成中间层和用户演示文稿的代码。 您还可以定义某种外部元数据存储 (如一个 XML 文件中的在数据字段,然后使用代码生成生成数据库架构、 任何中间层对象和用户界面屏幕。 个人,我不大规模的代码生成的风扇使我的团队选择另一个方向。

我们通常域模型类首先设计,我们认为验证逻辑,为域模型的实体类的责任。 如所需的字段的简单的验证规则和最大字符串长度规则,我们修饰验证属性 (如 图 2 所示的地址类的属性。

图 2 使用验证属性

public class Address : DomainEntity {
  [Required, MaximumStringLength(250)]
  public string Address1 { get; set; }

  [Required, MaximumStringLength(250)]
  public string City { get; set; }

  [Required]
  public string StateOrProvince { get; set; }

  [Required, MaximumStringLength(100)]
  public string Country { get; set; }

  [Required, MaximumStringLength(50)]
  public string PostalCode { get; set; }

  public string TimeZone { get; set; }
}

使用属性才能指定验证规则是一种简单且很常见的技术。 您将也能够说因为验证规则会表示以声明方式而不是使用强制性的代码实现,已传递本质与仪式测试。

现在,通过,您需要将必填的字段和最大字符串长度的规则复制到该数据库。 在我的工作组案例,使用 NHibernate 为我们持久性的机制。 NHibernate 的强大功能之一是从即可使用创建数据库架构,并使其与域模型 (该策略显然适合在新项目中) 不同步的 NHibernate 映射生成数据定义语言 (DDL) 代码。 为了使域模型生成数据库的此策略会很有用,这是需要我们可以添加到标记非空字段,并指定字符串长度的 NHibernate 映射的其他信息。

我们可以使用新的 Fluent NHibernate 机制来定义我们对象映射。 在为 Fluent NHibernate 安装代码中, 我们自动约定映射中由建立讲授 Fluent NHibernate 如何与 图 3 所示的代码处理该状态 [必需] 和我们模型类中的 [MaximumStringLength] 属性。

图 3 处理 NHibernate 中的属性

public class MyPersistenceModel : PersistenceModel {
  public MyPersistenceModel() {
    // If a property is marked with the [Required]
    // attribute, make the corresponding column in
    // the database "NOT NULL"
    Conventions.ForAttribute<RequiredAttribute>((att, prop) => {
      if (prop.ParentIsRequired) {
        prop.SetAttribute("not-null", "true");
      }
    });

    // Uses the value from the [MaximumStringLength]
    // attribute on a property to set the length of 
    // a string column in the database
    Conventions.ForAttribute<MaximumStringLengthAttribute>((att, prop) => {
      prop.SetAttribute("length", att.Length.ToString());
    });
  }
}

现在,这些规则将应用到项目中的所有映射。 在地址类我只是告诉 Fluent NHibernate 则保留的属性:

public class AddressMap : DomainMap<Address> {
  public AddressMap() {
    Map(a => a.Address1);
    Map(a => a.City);
    Map(a => a.TimeZone);
    Map(a => a.StateOrProvince);
    Map(a => a.Country);
    Map(a => a.PostalCode);
  }
}

现在,我已告诉 Fluent NHibernate 有关验证属性,可生成地址表的 DDL (请参见 图 4 )。 请注意在 图 4 中的 SQL 字符串长度匹配从 [MaximumStringLength] 属性地址类上定义的。 同样,有 NULL / NOT NULL 值从 [必需] 属性按地址类。

图 4 生成 DDL 代码

CREATE TABLE [dbo].[Address](
  [id] [bigint] IDENTITY(1,1) NOT NULL,
  [StateOrProvince] [nvarchar](100) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
  [Country] [nvarchar](100) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
  [PostalCode] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
  [TimeZone] [nvarchar](100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
  [Address1] [nvarchar](250) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
  [Address2] [nvarchar](100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL,
  [City] [nvarchar](250) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
PRIMARY KEY CLUSTERED 
(
  [id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, 
ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]

我们已投入派生所域模型对象验证属性的数据库结构的某些基础结构中,但我们仍有客户端验证和客户端外观问题。 我们希望执行简单的输入的验证,在浏览器,并标记某些方法更好的用户体验中的必填的字段元素。

我的工作组到达的解决方案是让 input 元素的 HTML 呈现所有验证属性了解。 (上下文,我们使用 ASP.NET 模型视图控制器,或 MVC,Framework Beta 1 与 Web 窗体引擎为我们视图引擎)。 图 5 显示了该标记可能外观的地址视图我们系统中。

图 5 地址查看标记

<div class="formlayout_body">
  <p><%= this.TextBoxFor(m => m.Address1).Width(300)%></p>
  <p><%= this.TextBoxFor(m => m.Address2).Width(300) %></p>
  <p>
    <%= this.TextBoxFor(m => m.City).Width(140) %>
    <%= this.DropDownListFor(m => 
        m.StateOrProvince).FillWith(m => m.StateOrProvinceList) %>
  </p>
  <p><%= this.TextBoxFor(m => m.PostalCode).Width(80) %></p>        
  <p><%= this.DropDownListFor(m => 
         m.Country).FillWith(m => m.CountryList) %></p>
  <p><%= this.DropDownListFor(m => 
         m.TimeZone).FillWith(m => m.DovetailTimeZoneList) %></p>
</div>

TextBoxFor 和 DropDownListFor 是小 HTML 帮助公共基视图中用于我们 MVC 体系结构中的所有视图。 TextBoxFor 的签名所示:

public static TextBoxExpression<TModel> TextBoxFor< TModel >(
  this IViewWithModel< TModel > viewPage, 
  Expression<Func< TModel, object>> expression)

  where TModel : class {
    return new TextBoxExpression< TModel >(
    viewPage.Model, expression);
  }

在此代码需要注意重要的是输入的参数是表达式 (表达式 < Func < TModel、 对象 > > 要精确)。 构造文本框的实际 HTML,时 TextBoxExpression 类将:

  1. 分析表达式,并绑定属性找到精确的 PropertyInfo 对象。
  2. 询问该 PropertyInfo 验证属性存在。
  3. 相应地呈现在文本框的 HTML。

我们只需添加一个名为需要绑定到属性的所有 HTML 元素的类标记为 [必需] 的属性。 同样,如果我们绑定的属性中找到 [MaximumStringAttribute],我们会设置 HTML 文本框,以匹配该属性,并限制用户输入以允许长度的 maxlength 属性。 生成 HTML 类似:

<p><label>Address:</label>
<input type="text" name="Address1" value="" 
       maxlength="250" style="width: 300px;" 
       class="required textinput" /></p>

必选字段的外观很容易控件只是通过编辑 CSS 类所需的外观 (我们将设置一个浅蓝色屏幕上必选字段的颜色)。 我们实际的客户端验证,jQuery 验证插件,这方便地足够,只是寻找所需的类,input 元素上存在。 只是由 maxlength 属性设置输入元素上实施文本元素的最大长度。

通过任何方式是完整的实现。 生成实际的实现随时间没有的困难。 困难的部分考虑通过消除重复的元数据和多个层中编码的方法。 我确信许多团队将不会 keen 方式,我的团队从该对象模型生成数据库,但的会是确定,因为我实际目标就是为您提供有关如何在可能使用,例如一次和一次主体来简化您自己的开发工作的一些想法。

明智的默认值

在上一的节,我显示 (通过一个 AddressMap 类) 一个小型示例表示使用 Fluent NHibernate 映射 (ORM) 的对象关系。 以下是表示从站点类对地址对象引用一个稍微复杂一些的示例:

public SiteMap() {
  // Map the simple properties
  // The Site object has an Address property called PrimaryAddress
  // The code below sets up the mapping for the reference between
  // Site and the Address class
  References(s => s.PrimaryAddress).Cascade.All();
  References(s => s.BillToAddress).Cascade.All();
  References(s => s.ShipToAddress).Cascade.All();
}

当您配置 ORM 工具时,您通常需要:

  1. 指定的实体类映射到表名称。
  2. 指定主键字段的实体,通常还指定策略指派主键的值的某种。 (是一个自动编号 / 数据库序列? 或不会在系统,它指派了主键的值? 或执行您使用 GUID 主键?)
  3. 将对象属性或字段映射到表中的列。
  4. 建立"到"关系从一个对象之间 (如到地址映射从站点) 时, 需要指定 ORM 工具可以使用加入父记录和子记录的外键列。

就是通常乏味的工作。 就必须执行获取保持对象 ORM 的仪式。 幸运的是,您可以通过在代码 (注意的大写字母) 中嵌入一些明智的默认设置排除某些在烦闷。

您可能会注意到在映射的示例中我未指定表名称、 主要的密钥策略或任何外键字段名称。 在我的工作组 Fluent NHibernate 映射超类别,我们设置某些默认值为我们的映射:

public abstract class DomainMap<T> : ClassMap<T>, 
  IDomainMap where T : DomainEntity {

  protected DomainMap() {
    // For every DomainEntity class, use the Id property
    // as the Primary Key / Object Identifier
    // and use an Identity column in SQL Server,
    // or an Oracle Sequence
    UseIdentityForKey(x => x.Id, "id");
    WithTable(typeof(T).Name);
  }
}

opinionated 软件

您可能注意到 我描述采用约定为"限制。 通过配置的约定后面理念的一部分是使"opinionated 软件"创建人工约束设计的。

一个 opinionated 框架需要执行某种特定方式几乎到消除灵活性的点的操作的开发人员。 opinionated 软件的 proponents 认为这些限制提高开发效率决定删除开发人员和提升的一致性。

使用我的团队的一个看来是所有的域模型类完全由一个单个的长属性,调用 ID 标识:

public virtual long Id { get; set; }

它并简单的规则,但它具有大量渊博影响设计。 因为所有的实体类在相同的方式中标识的您已经可以使用单一存储库类而不是编写专用于每个顶级实体的存储库类。 在相同的 vein 中 Web 应用程序中处理的 URL 而无需注册为每个实体的特殊路由规则在实体类是一致。

执行此看来减少了添加一个新的实体类型的基础结构的成本。 此方法的缺点是它将是很难以容纳一个自然的键或甚至复合键或 GUID 的对象标识符。 这不我的团队的问题,但它可以轻松地阻止从采用我们看来的另一个工作组。

现在,如何实施这些意见? 第一个步骤只创建公共了解和有关这些观点工作组中的协议。 了解开发人员突出显示有效地使用到他们利用这些 opinionated 的设计选择的最佳机会。 同样,如果开发人员不熟悉现有规则或者约定是令人困惑约定通过配置可以是近似的灾难。

可以还考虑使用作为您连续的集成版本的一部分的一个静态代码分析工具自动强制执行项目的约定。

在我们正在设置此代码中标识策略分配的 Id 属性将标识所有类的子类 domain­entity 一个策略。 表名称被假定为类的名称相同。 现在,我们总是可以覆盖基于每个类,这些选项,但我们已经很少了为此 (名为用户的类必须被映射到一个名为用户只是为了避免与 SQL Server 中保留字冲突的表)。 相同的方式 Fluent NHibernate 假定根据属性名称引用另一个类的一个外部项名称。

授予,这不保存多行代码,每个映射类的但它可以通过减少整体的干扰代码,映射中读取变得更容易执行不同的映射部分。

通过配置的约定

软件开发人员必须查找获得更多的工作效率,并使系统更多动态移动出的强制性的代码和到声明性 XML 配置行为。 许多开发人员觉得 XML 配置的普及是太多了,已成为一有害的操作。 通过显式配置的默认设置的策略是也称为约定 over 配置。

通过配置的约定是设计理念和设法应用所暗示代码而不需要显式的代码的结构中的默认值的方法。 其目的是简化开发过程,从而开发人员可以只考虑应用程序和体系结构的非常规部分。

现在右,许多人积极使用 ASP.NET MVC 框架,试验使用它的不同方法。 Web 开发 MVC 模型中有几个可能要通过配置应用规则的好机会的重复代码的源。

MVC 模型中的单个请求的基本流程如下五个步骤:

  1. 接收来自客户端的 URL。 在路由子系统将分析该 URL,并确定处理此 URL 的控制器的名称。
  2. 从控制器名称由路由子系统生成或找到正确的控制器对象。
  3. 调用正确的控制器方法。
  4. 选择正确的视图,并封送处理模型数据从控制器方法生成此视图。
  5. 呈现视图。

Out of 该的 Box 某些重复仪式中没有生成 ASP.NET MVC 框架可降低通过采用某些严格的规则的网页。

第一个任务是连接到 Web 站点与适当的控制器类的一个传入的 URL。 MVC Framework 中的路由库可以询问一个 URL,并确定该控制器的名称。 MVC 框架,然后会为与该控制器控制器名称由传入的 URL 相匹配的控制器对象要求注册的 IControllerFactory 对象。

许多团队只是委派来控制 (IOC) 工具的一个反转的控制器构造。 在我的工作组的情况下,我们使用开源 StructureMap 工具通过名称解析控制器实例:

public class StructureMapControllerFactory 
  : IControllerFactory {

  public IController CreateController(
    RequestContext requestContext, string controllerName) {

    // Requests the named Controller from the 
    // StructureMap container
    return ObjectFactory.GetNamedInstance<IController>(
      controllerName.ToLowerInvariant());
  }
}

请求该控制器相当简单,但第一个要注册所有控制器类按与 IOC 容器的名称。 等待 ! 不,可以添加到体系结构的某些仪式? 一年或前两将具有的控制器类的显式 IOC 配置提前 plunged 我示:

public static class ExplicitRegistration {
  public static void BootstrapContainer() {
    ObjectFactory.Initialize(x => {
      x.ForRequestedType<IController>().AddInstances(y => {
        y.OfConcreteType<AddressController>().WithName("address");
        y.OfConcreteType<ContactController>().WithName("contact");

        // and so on for every possible type of Controller
      });
    });
  }
}

此代码表示纯烦闷和存在于到源 IOC 工具的其他原因的仪式。 如果您查看在注册代码的详细,您会注意到它的执行一致的模式。 AddressController 注册为地址并且 contact­controller 被注册为联系人。 而不是显式配置每个控制器,您可以只是创建一个约定用于自动确定每个控制器类的路由的名称。

幸运的是,还有对约定基于注册,直接支持 StructureMap 因此您可以创建一个新 ControllerConvention 自动注册 IController 任何具体类型的:

public class ControllerConvention : TypeRules, ITypeScanner {
  public void Process(Type type, PluginGraph graph) {
    if (CanBeCast(typeof (IController), type)) {
      string name = type.Name.Replace("Controller", "").ToLower();
      graph.AddType(typeof(IController), type, name);
    }
  }
}

接下来,需要一些代码,如 图 6 所示引导 StructureMap 容器与新的约定。 位置和引导 IOC 容器的一部分中新的 ControllerConvention 后,添加到应用程序的任何新控制器类将自动添加于无需任何显式配置的开发人员 IOC 登记。 因此有更多错误和 Bug 因为开发人员忘记添加新的配置元素的新屏幕。

图 6 的新规则的 StructureMap

/// <summary>
/// This code would be in the same assembly as 
/// the controller classes and would be executed
/// in the Application_Start() method of your
/// Web application
/// </summary>
public static class SampleBootstrapper {
  public static void BootstrapContainer() {
    ObjectFactory.Initialize(x => {
      // Directs StructureMap to perform auto registration
      // on all the Types in this assembly
      // with the ControllerConvention
      x.Scan(scanner => {
        scanner.TheCallingAssembly();
        scanner.With<ControllerConvention>();
        scanner.WithDefaultConventions();
      });
    });
  }
}

为便笺,我想说明此策略自动注册的所有只要 IOC 容器提供程序的注册 API 我了解.NET Framework 的 IOC 容器中是可能。

接下来的步骤

最后的有关减少您的需要和发生这种情况的目的的代码之间的距离和冲突的所有信息。 我显示此列中的这些技术大量将真正让代码"只是认为它出"而不是显式的代码中使用命名约定或查找如何避免重复系统中的信息。 我还用于一些反射方法重复使用埋减少机械工作的属性中的信息。

所有这些设计概念可以减少在开发工作的重复性的仪式,但涉及代价。 通过配置的约定的 detractors 抱怨在固有"每逢"的方法。 将意见嵌入到您的代码或 Framework 会损害这些观点不是为有利差异的新方案中的潜在重用。

没有很多我无法介绍这一次,我可能涵盖更高版本的列中的其他主题。 我明确希望了解如何面向语言的编程,; 其他语言 (如 F #、 IronRuby,和 IronPython ; 和内部的特定于域的语言使用影响的软件设计过程。

将您的问题和提出的意见发送至 mmpatt@Microsoft.com.

Jeremy Miller C# 的 Microsoft MVP 还是打开的源的作者 StructureMap 使用.NET 和在 forthcoming 依赖性注入的工具 storyTeller 工具为 supercharged 适应测试.NET。 访问他的博客 带阴影显示树开发人员CodeBetter 网站的一部分。