




已阅读5页,还剩13页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
疯狂大白菜 1 18 Inversion of Control Containers and the Dependency Injection pattern 作者 Martin Fowler 翻译 坚强 2002 源文档 轻量级容器在 Java 社 区近来可是风起云涌 这些容器能将来自不同项目的逐渐集结为一 个内聚的应用程序 这些容器都是基于一个共同的模式 这个模式决定了容器如何完成组件 装配 人们统称之为 控制反转 Inversion of Control 本文将深入探讨这个模式的工作 机制 并给它一个具体的名字 依赖注入 Dependency Injection 并与服务定位器 Service Locator 后文将延续使用此译法 模式进行比较 在二者中进行选择不重要 关键是一个二 者都遵循的的原则 应该将应用和配置分离 本文内容 组件 for Iterator it allMovies iterator it hasNext 疯狂大白菜 3 18 Movie movie Movie it next if movie getDirector equals arg it remove return Movie allMovies toArray new Movie allMovies size 这个功能的实现真的是简单至极了 它通过 finder 对象 后面会提及 来返回 finder 所能 找到所有的电影列表 然后遍历列表 并返回特定导演的作品 很简单是吧 我不会去改进 它 在本文它只是用来说明问题的 仅此而已 本文真正关心的是 finder 对象 或者说如何将 lister 对象和 finder 对象联系起来 这个问题 有趣的原因在于我要期望那个漂亮的 movieDirectedBy 要独立于影片的存储方式 所以所有 方法都会引用一个 finder finder 对象可以完成 findeAll 的功能 我们 可以把这个抽取出来 做成接口 public interface MovieFinder List findAll 现在已经完成了很好的对象解耦 但是我需要用一个实体类来完成电影的查找工作时就要 涉及到一个具体类 这里我把代码放在 lister 的构造器中 class MovieLister private MovieFinder finder public MovieLister finder new ColonDelimitedMovieFinder movies1 txt 这个实体类的名字就能表达这样一个事实 我们需要从一个逗号分隔的文本文件列表中获 得影片列表 具体的实现细节我省略掉了 只要知道这是一种实现方式就可以了 如果这个类只要我自己用一点问题都没有 但是如果我的一位朋友也惊叹于这个精彩的功 能并想使用它 那会怎样呢 如果他们也把影片列表存放在文 本文件中并使用逗号分隔 而且把文件名改成 movies1 txt 那么一点问题也没有 如果仅仅是电影列表的名字不同那也 没有问题 我可以从配置文件 中读取 但是如果他们使用完全不同的存储介质呢 比如 SQL 数据库 XML 文 件 Web Service 哪怕只是另外一种格式规则存储的文本文件呢 这样我 们就需要一个新的类来获取数据 由于已经抽取了一个 MovieFinder 接口 我可 以不修改 moviesDirectedBy 方法 我还是希望通过别的途径获得合适的 movieFinder 实现类的实例 疯狂大白菜 4 18 图 1 在 MoiveLister 类中简单创建 MoiveFinder 实例时的依赖关系 上图展现了这种情况下的依赖关系 MovieLister 类同时依赖于 MoiveFinder 接口及其实现 我们当然更期望 MoiveLister 只依赖于接口 但是我们如何得到一个获得一个 MoiveFinder 的实例呢 在我的 企业级应用模式 一书中 我们把这种情况称为插件 Plugin MoiveFinder 不是在 编译时就加入程序的 因为我不知道我的 朋友会怎么用什么样的 finder 我想让我的 MoiveLister类能与任何MoiveFinder实现配合工作 并且允许在运行时加载完全不用我的 控 制 现在的问题就是如何设计这个连接使 MoiveLister 类在不知道实现类具体细节的前提下 与其协作 将这种情况推广到真实系统中 我们或许又数十个服务和组件 每种情况我们都可以把使 用组件的形式加以抽象 抽取接口并通过接口与组件进行交互 如果组件没有提供一个接 口那么可以通过适配暗度陈仓 但是我们希望用不同的方式部署系统 就需要使用插件方 式来处理服务间的交互 这样我们才有可能在 不同的部署方案中使用不同的实现 所以现在核心的问题就是我们如何将这些插件集结在一个应用程序中 这恰恰是新生代轻 量级容器面对的主要障碍 而它们都无一例外的选择了控制反转 控制反转 当这些容器的设计者谈话时会说这些容器是如此的有用 因为他们实现了 控制反转 而 我却深感迷惑 控制反转是框架的共有特征 如果说一个框架以实现了控制反转为特点相当 于说我的汽车有轮子 问题是它们反转了什么 我第一次接触控制反转它关注的是对用户界面的控制 早期的用 户界面全由程序控制 你会设计一系列的命令 类似于 请输 入姓名 请输入地址 你的程 序会显示提示信息并取得用户响应 在图形化 甚至或者是基于触摸屏 用户界面中 用户 界面框架会维护一个主循环 你的应用程 序只需要为不同的区域设计事件和处理函数就可 以了 这里程序的主要控制就发生了反转 从应用程序转移到了框架 疯狂大白菜 5 18 因而对于新生代的容器 它们要反转的就是如何定位插件的具体实现 在我简单的例子中 MovieLister类负责MovieFinder的 定位 它直接就实例化了一个子类 这样依赖 MoiveFinder 也就不是插件了 因为它不是在运行时加载的 这些容器的方法是 只要插件遵守一定转化 规则那么一个独立的程序集模块便可以注入到 lister 结果就是我需要给这个模式一个更具体的名字 控制反转太宽泛了 因而常常让人迷惑 通过和一些 IoC 爱好者商讨之后我们命名为依赖注入 我将开始讨论各式各样的依赖注入 但是要先指出的一点是 要消解应用程序对插件的依 赖 依赖注入绝不是不二法门 你也可以通过使用 Service Locator 模式做到这一点 讨论依 赖注入之后之后我们会谈到 Service Locator 服务定位模式 依赖注入的形式 依赖注入的基本思想是 有一个独立的对象 一个装配器 它获得实现了 Finder 接口合适 的实现类并赋值给 MovieLister 类的一个字段 现在的依赖情况如下图所示 图 2 依赖注入器的依赖关系 依赖注入的形式主要又三种 构造器注入 属性注入 接口注入 如果你关注最近关于依 赖注入的讨论资料你就会发现这就是其中提到的类型 1IoC 类型 2IoC 类型 3IoC 数字往往 比较难记 所以我这里使用了名称命名 使用 PicoContainer 进行构造器注入 首先我通过使用一个轻量级的容器 PicoContainer 来实现构造器注入 之所以要从这里开始 疯狂大白菜 6 18 是因为我在 ThoughtWorks 公司的几个同事在 PicoContainer 的开发社区很活跃 是的 可是 说是一种偏爱 PicoContainer 使用构造器来决定如何把一个 finder 的实现类注入到 lister 类中 要达到这个 目的 MoiveLister 类必须要生命一个构造器而且要包含足够的注入信息 class MovieLister public MovieLister MovieFinder finder this finder finder finder 自身也是由 PicoContainer 管理的所以文本文件的名字也可以通过容器注入来实现 class ColonMovieFinder public ColonMovieFinder String filename this filename filename 下面要做的就是告诉 PicoContainer 哪些接口和哪些类实现关联 哪些字符串注入到 finder 组件 private MutablePicoContainer configureContainer MutablePicoContainer pico new DefaultPicoContainer Parameter finderParams new ConstantParameter movies1 txt pico registerComponentImplementation MovieFinder class ColonMovieFinder class finderParams pico registerComponentImplementation MovieLister class return pico 这段配置代码通常位于另外一个类 我们这个例子 每一个使用我的 lister 类只要在自己的 配置类编写适当的配置代码就可以了 当然通常的做 法是把这些配置信息放在一个单独的 配置文件中 你可以写一个类来读取配置文件来设置容器 尽管 PicoContainer 不包含这个 功能在另一个有紧密关 系的项目 NanoContainer 中提供了一些包装 允许你使用 XML 配置 文件 NanoContainer 解析并对 PicoContainer 进行配置 这个项目的哲学理念是将配置形式 与底层配置机制分离 使用这个容器你的代码大致会像这样 public void testWithPico MutablePicoContainer pico configureContainer MovieLister lister MovieLister pico getComponentInstance MovieLister class Movie movies lister moviesDirectedBy Sergio Leone assertEquals Once Upon a Time in the West movies 0 getTitle 疯狂大白菜 7 18 尽管这里我使用了构造器注入 PicoContainer 也支持属性注入 不过它的开发者推荐构造 器注入 使用 Spring 框架进行属性注入 Spring 框架是一个应用广泛的企业级 Java 开发框架 它包含了对事务 持久化框架 Web 应用开发和 JDBC 的功能抽象 和 PicoContainer 一样它也支持构造器注入和属性注入 但是 它的开发者推荐是使用属性注入 用到这个例子里正合适 为了让我的 Moive Lister 能接收注入 我要给它添加一个赋值方法 class MovieLister private MovieFinder finder public void setFinder MovieFinder finder this finder finder 类似的我们为 filename 也添加这样一个方法 class ColonMovieFinder public void setFilename String filename this filename filename 第三步就是设置配置文件 Spring 支持多种配置方式 你可以通过 XML 文件配置或者通过 代码 XML 配置文件是比较理想的方式 movies1 txt 疯狂大白菜 8 18 测试代码大致是这样的 1 public void testWithSpring throws Exception 2 ApplicationContext ctx new FileSystemXmlApplicationContext spring xml 3 MovieLister lister MovieLister ctx getBean MovieLister 4 Movie movies lister moviesDirectedBy Sergio Leone 5 assertEquals Once Upon a Time in the West movies 0 getTitle 6 7 接口注入 第三种注入技术是在接口中定义注入需要的信息并通过接口完成注入 Avalon 就是使用 了这种技术的框架的典型例子 稍后我会讨论更多相关内容 现在这个例子先用简单的代码 来实现 使用这个技术我先要定义一个接口 并用这个完接口成注入 这个接口的作用就是把 finder 实例注入到实现了该接口的对象中 public interface InjectFinder void injectFinder MovieFinder finder 这个接口由提供 MovieFinder 的类提供 任何要使用 MovieFinder 的实体类都必须实现这个 接口 比如 lister class MovieLister implements InjectFinder public void injectFinder MovieFinder finder this finder finder 我使用类似的方法将文件名注入到 finder 实现中 public interface InjectFinderFilename void injectFilename String filename class ColonMovieFinder implements MovieFinder InjectFinderFilename public void injectFilename String filename this filename filename 接下来我还需要一些配置代码将这些组件的实现包装起来 简单起见我就在代码里完成了 疯狂大白菜 9 18 class Tester private Container container private void configureContainer container new Container registerComponents registerInjectors container start 配置分成了两个阶段 通过定位关键字来注册组件 这和其它的例子一样 class Tester private void registerComponents container registerComponent MovieLister MovieLister class container registerComponent MovieFinder ColonMovieFinder class 下一步就是注册要依赖组件的注入器 每一个注入接口都需要一些代码来注入到依赖的对 象 这里我使用容器来完成注入器的注册 每一个注入器对象都实现了注入接口 class Tester private void registerInjectors container registerInjector InjectFinder class container lookup MovieFinder container registerInjector InjectFinderFilename class new FinderFilenameInjector public interface Injector public void inject Object target 当依赖设计成是一个为容器而写的类那么它对组件自身实现接口注入是有意义的 就像这 里我对 moive finder 做的修改一样 对于普通类 比如 string 我使用内部类来完成配置代 码 class ColonMovieFinder implements Injector public void inject Object target InjectFinder target injectFinder this class Tester public static class FinderFilenameInjector implements Injector public void inject Object target InjectFinderFilename target injectFilename movies1 txt 疯狂大白菜 10 18 The tests then use the container class IfaceTester public void testIface configureContainer MovieLister lister MovieLister container lookup MovieLister Movie movies lister moviesDirectedBy Sergio Leone assertEquals Once Upon a Time in the West movies 0 getTitle 容器使用声明了注入器的接口来指明注入器和实体类之间的依赖关系 具体用什么容器实 现不重要 我也不会做展示这只会让你笑我 使用服务定位器 依赖注入的关键优势是它消除了 lister 类对具体 finder 类实现的依赖 这样就可以让我的朋 友方便的将一个特定的实现插入到他们的应用环境中了 注入绝不是唯一出路 另外一个方 案就是使用 Service Locator 服务定位器模式 服务定位器的基本思想是一个对象知道如何获得应用程序所需要的所有服务 在我们的例 子里服务定位器就应该有一个方法返回所需的 finder 实例 这只是减少了一点负担 从下图 的依赖关系可以看出我们还是需要在 lister 中获得定位器 疯狂大白菜 11 18 图 3 服务定位器的依赖关系 这里我把服务定位器做成一个单件注册表 lister 可以在实例化时通过服务定位器获取一个 finder 的实例 class MovieLister MovieFinder finder ServiceLocator movieFinder class ServiceLocator public static MovieFinder movieFinder return soleInstance movieFinder private static ServiceLocator soleInstance private MovieFinder movieFinder 和注入一样我们也必须配置服务定位器 这里我还是通过代码实现 要是通过读取配置文 件的进行配置也非难事 class Tester private void configure ServiceLocator load new ServiceLocator new ColonMovieFinder movies1 txt class ServiceLocator public static void load ServiceLocator arg soleInstance arg public ServiceLocator MovieFinder movieFinder this movieFinder movieFinder Here s the test code class Tester public void testSimple configure MovieLister lister new MovieLister Movie movies lister moviesDirectedBy Sergio Leone assertEquals Once Upon a Time in the West movies 0 getTitle 我常听到这样的抱怨服务定位器不是什么好东西因为你无法替换服务定位器返回的实例也 疯狂大白菜 12 18 就无法进行测试 当然要是你设计很糟糕遇到了这些麻烦在所难免 在这个例子服务定位器 就是一个简单的数据容器 可以简单修改就可以创建适用于测试的服务 对于更复杂的情况我可以从服务定位器派生子类并将子类传递给注册表类变量 另外我可 以让服务定位器暴露出来一个静态方法而不是直接访问该类实例的变量 我还可以使用特定 的线程存储提供特定线程的服务定位器 所有这些变化都不会修改使用服务定位器的 Client 一种对服务定位器的改进思路是不设计成单件形式 单件的确是实现注册表的简单方法 但这这只是实现决定很容易改变它 为定位器使用分离接口 上面的简单方案有一个问题 lister 类依赖于整个服务定位器哪怕它仅仅使用一个服务 我 们可以通过使用分离的接口来改变这种对服务定位器的依赖 lister 使用的是它想要使用的 那部分接口 相应的 lister 类的提供者也应该提供这样一个定位器接口 使用者可以通过这个接口获得 finder public interface MovieFinderLocator public MovieFinder movieFinder The locator then needs to implement this interface to provide Access to a finder MovieFinderLocator locator ServiceLocator locator MovieFinder finder locator movieFinder public static ServiceLocator locator return soleInstance public MovieFinder movieFinder return movieFinder private static ServiceLocator soleInstance private MovieFinder movieFinder 注意这里由于使用了接口我们就不能再使用静态方法直接访问服务 我们必须在类中获得 定位器实例进而使用各种需要的服务 疯狂大白菜 13 18 动态服务定位器 上面的例子是静态的 服务定位器拥有你想要的各种服务 这还不是唯一的路子 你可以 使用动态服务定位器 它允许你在其中注册需要的任何服务并在运行时按需获取服务 下面的例子中 服务定位器使用 map 来保存服务信息 而不是放在字段中 并提供了一个 通用的方法来获取服务 class ServiceLocator private static ServiceLocator soleInstance public static void load ServiceLocator arg soleInstance arg private Map services new HashMap public static Object getService String key return soleInstance services get key public void loadService String key Object service services put key service 配置的功能就是通过特定的关键字来加载服务 class Tester private void configure ServiceLocator locator new ServiceLocator locator loadService MovieFinder new ColonMovieFinder movies1 txt ServiceLocator load locator 通过同样的关键字使用服务 class MovieLister MovieFinder finder MovieFinder ServiceLocator getService MovieFinder 总体上讲我不喜欢这个方法 尽管它的确灵活但是它不够直观明了 我只有通过字符串关 键字才能定位一个服务 我更倾向比较明了的方法因为通过看接口定义就可以很清楚的知道 如何获得一个特定的服务 Avalon Locator Injection 双管齐下 依赖注入和服务定位器不是水火不容的概念 Avalon 框架就是一个把二者和谐使用的绝好 例证 Avalon 使用服务定位器但是同时使用注入来告诉组件到那里找定位器 疯狂大白菜 14 18 Berin Loritsch 给我发了一个简单的可以运行的例子 就是用 Avalon 实现的 public class MyMovieLister implements MovieLister Serviceable private MovieFinder finder public void service ServiceManager manager throws ServiceException finder MovieFinder manager lookup finder Service 方法就是方法注入的例子 它可以是容器将一个 ServiceManager 对象注入到 Lister 对象中 而 ServiceMananger 则是一个服务定位器的例子 这个例子中 MyMoiveLister 并 不是要把 ServiceManager 保存在字段里 面 而是借助它获得 finder 的实例 并将 finder 保 存起来 选择 到目前为止我已经集中阐述了我对这两个模式及其变化形式的看法 下面就要细数它们的 优缺点了 这样有助于判断什么时候用哪一个 服务定位器 VS 依赖注入 首先是服务定位器和依赖注入的选择 第一点要注意的虽然上面两个过于简单的例子没有 表现出来实际上这两种模式都提供了基本的解耦功能 两种情 况下应用程序代码都不依赖 于具体服务接口的实现 二者的区别在于怎样把具体的实现提供给应用程序类 使用服务定 位器是通过应用程序发送消息明确要哪一个服 务的实现 使用依赖注入不明确发请求 服 务的实现会自然的出现在应用程序的类中 这就是控制反转 控制反转是所有框架的共同特征 但是你要付出一点代价 它会难于理解并在调试时带来 一些问题 所以我整体来讲我尽量避免使用它 这并不是说它不好 只是我更倾向于一个直 观的方案 二者关键的区别在于 使用服务定位器每一个服务的使用者都会依赖于服务定位器 定位 器隐藏了使用者对其它服务具体实现的依赖 而必须依赖服务定位器本身 所以选择注入还 是定位器就在于你是否在意对定位器的依赖 使用依赖注入你可以很清楚的看到组件之间的依赖关系 你只需要观察使用了什么样的依 赖注入机制 比如构造器注入 就可以看清其中的依赖关系 而在服务定位器中你必须在源代码中搜索对服务定位器的调用 具备引用分析功能的 IDE 可以简化这个操作 但还不如直接观察构造器和属性简单 疯狂大白菜 15 18 关于这二者的选择很大程度上取决于我么服务使用者的性质 如果你的应用程序中有不同 的类使用同一个服务 那么应用程序使用服务构造器就不是什 么大的问题 上面影片列表 的例子使用服务定位器就很好我的朋友只需要对定位器做一点配置 配置代码或者配置文件 均可 就可以获得适当的服务实现 而在这种 应用场景中 注入器的反转没有什么引人注 目的地方 要是把 lister 看成一个组件提供给别的应用程序使用情况就不同了 这种情况下我不知道使 用者会使用什么样的服务定位器 API 每一个使用 者都有可能都使用自己的服务定位器 而这些服务定位器彼此之间并不兼容 我可以使用分离接口的方法来解决这个问题 每一个 使用者都可以开发自己的服务定位 器的适配器 但即使是这样我还是希望第一个服务定位 器中寻找特定的接口 而且一旦适配器出现定位器的简单直接就开始削弱了 使用注入器不会产生组件和注入器之间的依赖关系 可是注入器一旦配置确定了组件也无 法获得更多的服务 人们倾向选择依赖注入的原因是进行测试更容易了 关键之处是为了进行测试你可以使用 真实服务或者使用 Stubs 桩或者 mocks 方法等创建的 测试用服务 但是实际上这里使用依 赖注入和服务定位器是没有区别的 两者都很好的支持 桩 我猜想之所以有这么多人又依 赖注入更容易进行测试的印象 是 因为他们并没有努力做到定位器可替换 这就是持续测 试的意义所在 如果你不能很容易的使用桩进行测试 这就意味着你的设计出现了严重问题 当然 如果组件环境又很强的侵入性那么测试的问题会更加严重 例如 Java EJB 框架 我 的观点是这些框架应该尽量减小框架本身对应用程序代码的影响 特别是不要做那些可能会 让编辑 执行过程变慢的操作 用插件来取代重量级的组 件会对测试过程有很大帮助 这也 是 TDD 测试驱动开发实践的关键 所以 主要的问题还是在于作者是否愿意组件脱离自己控制被应用到其它程序中 如果作 者是这样想的 那么它就不能对服务定位器做任何假设 哪怕是最小的假设也会带来麻烦 构造器注入 vs 属性注入 服务整合的时候 你要遵循一定规则来将所有的东西整合在一起 依赖注入的有点在于 只要非常简单的约定 至少对构造器注入和属性注入来说是这样的 你不必在你的组件中做 什么特殊的处理就是直接做一下配置就可以了 接口注入的侵入性要强的多 因为你要按照需要写很多接口 用容器提供一个小型的接口 集合 这个主意不错 Avalon 就是这么做的 整合和依赖方面还有很多工作要做 这就是为什么很多轻量级容器依然致力于研究属性注 入和构造器注入的原因 疯狂大白菜 16 18 属性注入和构造器注入反映了一个有趣的问题 所有面向对象系统都会遇到 应该在那里 填充对象字段 构造器还是属性 一直以来我的实践都是尽量把对象的创建放在构造阶段 也就是构造器中填充对象字段 这个建议可以追溯到 Kent Beck s Smalltalk Best Practice Patterns Constructor Method and Constructor Parameter Method 带参构造器可以明确创建意图 如果不知一种构造方式 就要 提供多个构造函数 构造器的另外一个好处就是允许你隐藏一些不变字段只要没有 setter 就可以了 我认为这 个很重要如果一个字段被认为是不应该被改变的 那么 属性赋值锁定就能很好的表明这一 点 如果你使用属性初始化那就痛苦了 实际上我倾向于避免使用设置方法 而是使用哪个 类似于 initFoo 之类的方法 来 强调这个方法应该在对象创建之初进行调用 不过什么事情都有个例外 如果构造函数的参数过多就显得有点混乱了 特别是对于哪些 不支持关键字参数的语言更是如此 一个冗长的参数列表说明这个类过于繁忙了 应该进行 拆分 确实需要很多参数的情况除外 如果又多种方式构造一个有效的对象 很难通过构造函数的个数和类型来描述构造意图 这就是 Factory Method 模式的用武之地了 工厂方法可以借助多个私有构造函数和 Setter 方法组合来完成自己的任务 经典 Factory Method 模式的问题就在于它往往通过静态方法的 形式实现 你无法在接口声明它们 你可以创建一个工厂类 但是那又变成另一个服务实体 类了 工厂服务是 一种不错的技巧 但是你需要某种方式实例化工厂对象 问题还在 如果要传入像字符串之类的简单类型 构造器注入也会又问题 使用属性注入你可以属性 方法的名字中说明参数的用途 而构造器注入你只能靠参数的位置决定每一个参数的作用 而这实践起来难很多 如果你的对象又有很多构造器并且对象间存在继承关系 事情就会变糟糕了 为了能够正 确初始化你必须将子类的构造器调用转到父类构造器然后处理自己的参数 这可能导致构造 器爆炸式膨胀 尽管诸多缺陷我还是首推构造器注入 不过我们已经做好准备一旦遇到上述的问题就转向 属性注入 在以依赖注入作为框架核心的几个开发团队之间 属性注入还是构造器注入这个话题上争 论不休 不过多数人都已经意识到即使有所偏重还是要兼顾两种注入方式 代码配置还是配置文件 另外一个相对独立却也纠结的话题是 使用配置文件还是使用代码来完成配置服务的组装 对于大多数要在多处部署的应用程序来说 配置文件的方式 更合适些 配置文件多为 XML XML 也确实胜任 但也有一些情况使用代
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 语谱图课件原理
- 语言区游戏理论知识培训课件
- 2025咨询合同-泄露后果
- 2025企业合作协议协议范本
- 2025员工派遣协议方案借调合同
- 2025房产按揭贷款购买合同
- 团队绩效评估体系评分标准模板
- 互联网技术咨询服务合作合同
- 合作社农田种植项目协议
- 2025年智能制造行业补贴资金申请策略与案例分析报告
- 佛山市顺德区容桂街道专业电镀产业发展规划(2023-2035年)环境影响报告书(简本)
- 高效人员管理的5大核心思路与方法
- 《物业管理条例》教学课件
- TCNAS 28─2023成人住院患者静脉血栓栓塞症的预防护理
- (高清版)DB3301∕T 0046-2017 智精残疾人托养机构护理服务规范
- 基层司法所规范化建设
- 经济学基础课件 项目三 支付结算法律制度
- 城市低空安全监管平台解决方案
- 员工入职申请表(完整版)
- 销售述职竞聘报告
- 超市安全知识培训内容
评论
0/150
提交评论