动态加载与应用程序扩展

在软件开发中,动态加载是一种允许在运行时加载和使用程序集的技术,而无需在设计时直接引用它们。这使得扩展或自定义应用程序变得简单,无需为了一个简单或复杂的更改而重新编译整个项目。这种概念有助于设计和创建可定制的现成解决方案(COTS),同时也使应用程序更加可扩展和稳定。

例如,假设正在设计一个电子商务应用程序,并希望提供隔夜和第二日送达的运费计算器功能。最简单的方法是创建一组内部函数来处理这些计算,并根据用户的选择调用相应的函数。这在一开始是可行的,但如果需要添加新选项会发生什么呢?可以想象,这种方法的扩展性并不好,最终可能会导致一些大问题。

为了解决这个问题,可以将所有的运费逻辑打包成自包含的程序集,并在需要时在运行时加载它们。当有新需求出现时,只需创建一个满足该需求的新项目,将其编译成新的程序集,将其放置在项目的“/bin”文件夹中,然后在web.config文件中引用它,或者通过向数据库添加新记录来引用它。这个新程序集将通过反射加载,从而在应用程序中可用。

设计考虑

实现这种方法时,需要考虑一些设计因素,但这并不像听起来那么困难。首先,需要确定对象所需的所有方法和属性。在上面的运输示例中,将创建一个IShipping接口,它将定义所有运输对象将派生的合同。为了简单起见,这个接口将定义一个接受OrderHeader对象作为单个参数的CalculateShipping方法,以及将用于描述每个单独对象的一些只读属性。接下来,将在自己的类库项目中创建一个新类,然后实现IShipping接口。这个类将包含一个根据给定的OrderHeader对象执行所需自定义逻辑的CalculateShipping方法。

使用代码

听起来很简单,让看看代码。首先,让回顾一下OrderHeader对象。如所见,它是一个简单的CLR对象,包含所有属性,没有方法。反过来,它引用了一个OrderDetail对象的列表,这些对象反过来包含一个正在订购的产品列表。

namespace Dynamic.Model { public class OrderHeader { public int OrderHeaderId { get; set; } public DateTime OrderDate { get; set; } public int OrderNumber { get; set; } public string ShipToName { get; set; } public string ShipToStreet { get; set; } public string ShipToCity { get; set; } public string ShipToState { get; set; } public string ShipToZip { get; set; } public double OrderTotal { get; set; } public List OrderDetails { get; set; } public OrderHeader() { this.OrderDetails = new List(); } } public class OrderDetail { public int OrderDetailId { get; set; } public Product Product { get; set; } public int Quantity { get; set; } } public class Product { public int ProductId { get; set; } public string Sku { get; set; } public string ProductName { get; set; } public string ProductDescription { get; set; } public double Weight { get; set; } public double Price { get; set; } } }

接下来是IShipping接口。注意这里没有代码描述,因为它严格用于定义一个合同,所有其他对象都将实现它。

namespace Dynamic.Model { public interface IShipping { double CalculateShipping(OrderHeader orderHeader); string ShippingType { get; } string Description { get; } } }

最后,有一个执行简单计算并返回值的运输对象。这个对象可以位于它自己的单独程序集中,但需要引用包含IShipping接口的程序集,以便它可以被库正确实现和使用。

namespace Overnight { public class Shipping : Dynamic.Model.IShipping { public double CalculateShipping(OrderHeader orderHeader) { // 运费是当前订单的5% return orderHeader.OrderTotal * 0.05; } public string ShippingType { get { return "隔夜运费"; } } public string Description { get { return "这个类计算运费为订单总额的5%"; } } } }

由于定义了一个接口,并且在运行时动态加载运输程序集,并不局限于单一的运输解决方案。下面是一个稍微复杂一点的计算器示例,它根据传入订单中所有产品的总重量来确定运费。

namespace SecondDayShipping { public class Shipping : Dynamic.Model.IShipping { public double CalculateShipping(OrderHeader orderHeader) { double totalWeight = 0; double shippingRate = 0; // 在这里做一些额外的逻辑来找出订单有多重 foreach (OrderDetail detail in orderHeader.OrderDetails) { totalWeight += detail.Product.Weight * detail.Quantity; } // 不同的重量有不同的费率 if (totalWeight > 100) shippingRate = 20; else if (totalWeight > 50) shippingRate = 10; else shippingRate = 5; return shippingRate; } public string ShippingType { get { return "第二日运费"; } } public string Description { get { return "这个类计算第二日运费率"; } } } }

现在已经设置并定义了接口和运输对象,必须编写应用程序来实际使用它们。一个不太动态的方法是使用早期绑定方法创建一个运输对象的实例,如下例所示,但这正是试图避免的。

namespace DynamicLoadingExample { public class Program { public static void Main() { // 这个示例展示了如何使用早期绑定创建一个隔夜运输对象 // 这种方法是可行的,但对于未来的更新来说并不是很灵活 Overnight.Shipping shipping = new Overnight.Shipping(); double overNightRate = shipping.CalculateShipping(orderHeader); } } }

理想情况下,将使用反射来加载一个程序集,给IShipping合同,然后实例化该对象,以便可以调用其CalculateShipping方法。这种方法的工作原理是,首先必须能够访问编译后的程序集,要么将其放置在GAC中,要么放置在应用程序的/bin文件夹中。其次,然后必须使用以下格式传入要实例化的资源的完全限定名:“Namespace.Classname, AssemblyName”。注意这是区分大小写的,如果不确定程序集名称是什么,点击项目 - 属性 - 应用程序,名称将列在程序集名称下。

namespace DynamicLoadingExample { public class Program { public static void Main() { // 这个示例展示了如何通过反射创建一个SecondDay运输对象 // 首先,必须获得正确的程序集的引用 // 为此,传入完全限定的类名 IShipping secondDay = CreateShippingInstance("SecondDayShipping.Shipping,SecondDayShipping"); // 一旦有了这个实例,就可以调用IShipping接口定义的方法来返回计算出的价格 double secondDayRate = secondDay.CalculateShipping(orderHeader); } public static IShipping CreateShippingInstance(string assemblyInfo) { Type assemblyType = Type.GetType(assemblyInfo); if (assemblyType != null) { Type[] argTypes = new Type[] { }; ConstructorInfo cInfo = assemblyType.GetConstructor(argTypes); IShipping shippingClass = (IShipping)cInfo.Invoke(null); return shippingClass; } else { // 需要错误检查来帮助捕获实例 throw new NotImplementedException(); } } } }

感兴趣的点

包含的项目包含所有代码,可以看到这一点,但示例在调用CreateShippingInstance并传入硬编码字符串时采取了一点捷径。理想情况下,这个方法将从数据库或其他配置文件中提取程序集信息,以便它们可以被添加、删除或更新,而无需重新编译整个项目。如何做到这一点取决于自己的独特需求和要求。

沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485