65.9K
CodeProject 正在变化。 阅读更多。
Home

动态原型设计

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.15/5 (10投票s)

2010年11月3日

CPOL

7分钟阅读

viewsIcon

46685

了解什么是动态原型设计以及如何进行

目录

引言

在这篇文章中,我想谈谈一种叫做“动态原型设计”的东西。这种软件开发方法的想法出现在我的脑海中,因为我厌倦了所有开发者都面临的永无止境的运行-重新编译-运行循环。所以在这篇文章中,我将解释动态原型设计的概念,讨论其实现,并演示其用法。

问题

软件开发任务有大有小——有些庞大,有些微小。根据您尝试实现的功能,一个任务可能需要一分钟或一个月。一分钟长度的任务非常烦人,因为它们的验证(我们暂时忘记单元测试)比修正本身花费的时间更长。它们更烦人,因为它们会打断我的注意力。

由于这种不满,我开始探索摆脱与重新编译周期相关的往返的想法。本质上,只有两种处理方法。

第一个选项是依赖 DLR 来处理需要修改的代码段。然而,这种方法会导致代码速度变慢,并且还会让我们使用我们最初可能不愿意使用的奇怪语言。

第二个选项,也是我偏爱的选项,是进程内编译。由于每个 .NET 程序都能够从内部编译其他 .NET 程序,因此能够修改正在运行的程序并且——更重要的是——能够立即看到结果是现实可行的。

基本基础结构问题

由于勤奋的开发人员可能会立即发现这种方法的几个问题,我将事先解决一些显而易见的担忧。

速度

要解决的第一个问题是:程序会因此从根本上变慢吗?传统观点认为会,很简单,因为我的许多读者已经在考虑跨域封送处理之类的事情。事实上,您可能已经注意到我从未建议卸载程序集。实际上,这对于使事情运行起来并不是必需的。

为什么不呢?嗯,主要是因为没有什么能阻止您加载同一程序集的多个副本。当然,您会为此付出代价,并且需要一些额外的调整,但最终结果是

  • 您更改的程序集在您完成更改后会立即编译并加载。新加载的程序集会立即取代以前的版本投入使用。

当然,需要谨慎的规划和深思熟虑的开发实践,以确保在加载和使用新程序集时不会出现问题。我们稍后会讨论这些。

往返和发布

所以下一个问题有点多方面。即,以下问题浮现在脑海中

  1. 您如何同时打包源代码并将其编译到程序中?
  2. 您如何确保内存中的更改实际“返回”到程序中?
  3. 您如何确保这些黑客行为都不会进入已部署的程序?

让我逐一解决这些问题

首先,源代码是同时编译并嵌入在应用程序中的。因此,您已经有了可用的二进制表示,以及您也可以使用的文本表示。

其次,内存中的更改会返回到程序中,因为实际上没有什么能阻止您将一些文本序列化到..\..\SomeClass.cs。您的断点(提示!)当然会断开,但除此之外,一切都会正常运行。

第三,提议的基础设施旨在(并不是说这是你应该做的)只在调试模式下使用。然而,实际上它不必如此——你可以将程序的源代码持久化到某个地方。当然,你也可以对其进行加密,使其不可读。

为了进一步阐述最后一点,其理念是源代码通常可在解决方案文件夹中找到,并且不会以任何方式打包。相反,它只是保存在它需要的地方——项目目录中!现在,为什么没有真正的往返问题应该(希望)很明显了——因为我们正在使用原始程序文件进行实现!

架构变更

好的,您可能会想象,加载一个包含与之前相同类型的新程序集将需要重新连接 IoC 容器中每个必要的类型,您当然是对的——这就是它的完成方式。然而,这个要求无论如何都符合最佳实践,反对它就像说我们不应该刷牙一样。

实现

所以,是时候写一些代码了。为了本文的目的,我将介绍一个用于 HTML 编码的程序的最小实现。这个想法是,程序的 HTML 编码部分是一个可编辑的服务,我们将为此创建规定。

默认实现

好的,假设我有一个名为 IConverter 的接口

public interface IConverter
{
  string Convert(string source);
}

随后我创建了我的(最小)HTML 编码实现

public class HtmlConverter : IConverter
{
  public string Convert(string source)
  {
    var sb = new StringBuilder(source);
    sb.Replace("<", "&lt;");
    return sb.ToString();
  }
}

现在,我是一个好孩子,我将 IConverter 注入到我的主窗口中,并用它来执行转换

public partial class MainFrame : Form
{
  private IConverter converter;
  public MainFrame(IConverter converter)
  {
    this.converter = converter;
    InitializeComponent();
  }
  private void btnConvert_Click(object sender, EventArgs e)
  {
    tbConverted.Text = converter.Convert(tbSource.Text);
  }
}

最后,我配置容器……

static void Main()
{
  var uc = new UnityContainer();
  uc.RegisterType<IConverter, HtmlConverter>();
  
  Application.EnableVisualStyles();
  Application.SetCompatibleTextRenderingDefault(false);
  Application.Run(uc.Resolve<MainFrame>());
}

然后应用程序就可以使用了

显示编辑器

好的,为了本次练习的目的,我将假设(这意味着我现在不会实现它)所有与动态原型设计相关的东西要么被包裹在 #if DEBUG 中,要么前面有 [Conditional("DEBUG")]。考虑到这一点,我们可以显示编辑器了。

如果您想知道我们如何找出要打开的文件,答案很简单:通常,您需要编写调试专用代码来实际显示此窗口,因此您不妨硬编码文件名。如果您想要一个更具工业强度的解决方案,请随时创建您自己的基础设施,其中包含漂亮的小菜单,让您指定要编辑的类型/文件。

重建类型

好的,我们有一个编辑器,它允许我们直接从解决方案中编辑文件,并且自然地,允许我们将其持久化回原来的位置。接下来呢?哦,我们还得编译和加载它。让我们先处理编译。

编译实际上很容易,唯一的注意事项是您需要引用您的原始可执行文件和任何相关的 DLL。这是一个例子

public class InProcessCompiler
{
  public object CompileAndInstantiate(string sourceCode)
  {
    var ps = new CompilerParameters {GenerateInMemory = true, GenerateExecutable = false};
    ps.ReferencedAssemblies.Add("System.Drawing.dll");
    ps.ReferencedAssemblies.Add("System.Core.dll");
    ps.ReferencedAssemblies.Add("DynamicPrototypingSample.exe");
    var po = new Dictionary<string, string> {{"CompilerVersion", "v4.0"} };
    var p = new CSharpCodeProvider(po);
    var results = p.CompileAssemblyFromSource(ps, new[] {sourceCode});
    if (results.Errors.HasErrors)
    {
      var sb = new StringBuilder();
      foreach (var e in results.Errors)
        sb.AppendLine(e.ToString());
      throw new Exception(sb.ToString());
    }
    var ass = results.CompiledAssembly;
    var mainType = ass.GetTypes()[0];
    return Activator.CreateInstance(mainType);
  }
}

上面有很多黑客技术——指定要添加为引用的显式 DLL 和我们的 EXE,在 4.0 下编译源代码,最后但并非最不重要的一点是——创建所有可用类型中的第一个。实际上,最后一点是审慎的,因为在这种情况下,我只对编译一种类型感兴趣,那就是在生成的程序集中首先找到的类型。

最后润色

创建了生成类型的实例后,我们做一件简单的事情:将其分配给变量。因此,整个过程如下所示

  • 抛出编辑 UI
  • 尝试编译类型
  • 如果成功,保存新实例
  • 用新源覆盖 .cs 文件

这是我们编辑按钮的事件处理程序

private void btnEditProgram_Click(object sender, EventArgs e)
{
  string path = @"..\..\HtmlConverter.cs";
  var ce = new CodeEditor(path);
  if (ce.ShowDialog() == DialogResult.OK)
  {
    try
    {
      converter = (IConverter) Compiler.CompileAndInstantiate(ce.SourceCode);
      File.WriteAllText(path, ce.SourceCode, Encoding.UTF8);
    } 
    catch (Exception ex)
    {
      MessageBox.Show(ex.ToString());
    }
  }
}

就这样了!

结论

好的,如您所见,实现并不太复杂。当然,它可以“修剪”,使得这种进程内编辑的检测仅在调试代码中可用。而且,通过在代码的编译和“吸收”方式上投入更多时间,您可以使您的原型开发更容易一些。

我做的一件有用的事情(但无法在本文中展示)是使用一个商业控件进行 C# 编辑,而不是仅仅将其作为纯文本编辑。除此之外,我使用的设置与我这里描述的非常相似。

感谢阅读!欢迎评论!

历史

  • 2010年11月3日:初次发布
© . All rights reserved.