写点什么

动态规划解决爬楼梯算法,彻底搞懂 AppStore 证书体系、彻底搞懂控制反转 IoC,依赖注入 DIP, John 易筋 ARTS 打卡 Week 28

用户头像
John(易筋)
关注
发布于: 2020 年 11 月 29 日

1. Algorithm: 每周至少做一个 LeetCode 的算法题

笔者的文章:

算法:动态规划解决爬楼梯Climbing Stairs python3

题目

70. Climbing Stairs

You are climbing a staircase. It takes n steps to reach the top.



Each time you can either climb 1 or 2 steps. In how many distinct ways can you climb to the top?



Example 1:



Input: n = 2
Output: 2



Explanation: There are two ways to climb to the top.



1. 1 step + 1 step
2. 2 steps



Example 2:



Input: n = 3
Output: 3



Explanation: There are three ways to climb to the top.



1. 1 step + 1 step + 1 step
2. 1 step + 2 steps
3. 2 steps + 1 step



动态规划解法

假设下一步就达到N,那么前一步要么是f(n-1), 或者是f(n-2)。因为可以选择一步,或者两步到达终点。这就产生了递推公式 f(n) = f(n-1) + f(n-2), 其中f(1)=1, f(2)=2. 也就是两种走法之和。结果就是res[n].



class ClimbingStairs:
def climbStairsWithDP(self, n: int) -> int:
if n == 1:
return 1
if n == 2:
return 2
res = [0] * (n + 1)
res[1] = 1
res[2] = 2
for i in range(3, n+1):
res[i] = res[i - 1] + res[i -2]
return res[n]



斐波那系数解法

因为中间结果都不需要存储,那么直接用两个变量存储前两个值就好。

class ClimbingStairs:
def climbStairs(self, n: int) -> int:
c1 = 1
if n == 1:
return 1
c2 = 2
for i in range(3, n+1):
c1, c2 = c2, c1 + c2
return c2



2. Review: 阅读并点评至少一篇英文技术文章

笔者博客:

翻译:控制容器的反转IoC和依赖注入模式DIP 概念发源地 Martin Fowler

说明

Martin Fowler 写于 2004年1月23日,为了更好了解对控制容器的反转和依赖注入模式的历史,所以翻译。

1. 控制容器的反转和依赖注入模式

Inversion of Control Containers and the Dependency Injection pattern

在Java社区中,涌现了许多轻量级的容器,这些容器可帮助将来自不同项目的组件组装成一个内聚的应用程序。这些容器的底层是它们执行接线方式的常见模式,它们以非常通用的名称“控制反转”引用。在本文中,我将以更具体的名称“依赖关系注入”深入研究此模式的工作原理,并将其与Service Locator替代方法进行对比。它们之间的选择不如将配置与使用分开的原理重要。



关于企业Java世界的有趣的事情之一是,为构建主流J2EE技术​​的替代品而进行了大量的活动,其中许多活动是在开源中进行的。这在很大程度上是对主流J2EE世界中重量级复杂性的一种反应,但其中很大一部分还在探索替代方案并提出创意。一个常见的问题是如何将不同的元素组合在一起:当Web控制器体系结构由数据库团队支持并且由彼此之间几乎不了解的不同团队构建时,如何将它们组合在一起。许多框架都在解决这个问题,并且正在扩展一些框架,以提供从不同层组装组件的一般能力。这些通常称为轻量级容器,示例包括PicoContainer和[Spring](http://www.springsource.org/)



这些容器的基础是许多有趣的设计原理,这些原理超出了这些特定的容器以及Java平台的范围。在这里,我想开始探讨其中一些原则。我使用的示例是在Java中编写的,但是像我的大多数写作一样,这些原理同样适用于其他OO环境,尤其是.NET。



2. 组件和服务

布线元素的主题几乎使我立即陷入围绕服务和组件一词的棘手的术语问题。您可以轻松找到关于这些事物的定义的长篇文章和相互矛盾的文章。出于我的目的,这是这些重载术语的当前用法。



我使用组件的意思是一整套软件,该软件原本打算由不受组件编写者控制的应用程序使用而无需更改。“无更改”是指使用的应用程序不会更改组件的源代码,尽管它们可能会通过以组件编写者允许的方式扩展组件来更改组件的行为。



服务与组件相似,供外部应用程序使用。主要区别在于,我希望组件在本地使用(例如jar文件,程序集,dll或源导入)。服务将通过同步或异步的某个远程接口(例如,Web服务,消息系统,RPC或套接字)远程使用。



我在本文中主要使用服务,但是许多相同的逻辑也可以应用于本地组件。实际上,通常您需要某种本地组件框架来轻松访问远程服务。但是编写“组件或服务”很容易引起阅读和书写的困扰,并且服务现在更加流行。



3. 一个天真的例子

为了使所有这些更加具体,我将使用一个正在运行的示例来讨论所有这些。像我所有的示例一样,它也是那些超级简单的示例之一。足够小以至于不真实,但希望足以让您直观地了解正在发生的情况而不会陷入真实示例的泥潭。



在此示例中,我正在编写一个组件,该组件提供由特定导演执导的电影列表。这个惊人的有用功能是通过一种方法实现的。



class MovieLister...
public Movie[] moviesDirectedBy(String arg) {
List allMovies = finder.findAll();
for (Iterator it = allMovies.iterator(); it.hasNext();) {
Movie movie = (Movie) it.next();
if (!movie.getDirector().equals(arg)) it.remove();
}
return (Movie[]) allMovies.toArray(new Movie[allMovies.size()]);
}

这个功能的实现极端幼稚,它要求一个取景器对象(稍后我们将介绍)返回它知道的每部电影。然后,它只是在此列表中搜寻以返回由特定导演指挥的那些。我不会解决这个天真的问题,因为这只是本文的重点。



本文的重点是该查找程序对象,或者特别是我们如何将列表器对象与特定查找程序对象连接。之所以如此有趣,是因为我希望我的奇妙 moviesDirectedBy方法完全独立于所有电影的存储方式。因此,该方法所做的只是引用一个查找程序,而查找程序所做的全部就是知道如何响应该 findAll方法。我可以通过为查找器定义一个接口来实现这一点。



public interface MovieFinder {
List findAll();
}



现在,所有这些都已经很好地分离了,但是在某些时候,我必须提出一个具体的课程来实际提出电影。在这种情况下,我将其代码放入lister类的构造函数中。



class MovieLister...
private MovieFinder finder;
public MovieLister() {
finder = new ColonDelimitedMovieFinder("movies1.txt");
}

实现类的名称来自以下事实:我从冒号分隔的文本文件中获取列表。我将为您保留所有细节,毕竟所有这些只是一些实现。



现在,如果我只为自己使用该类,那么一切都很好。但是,当我的朋友对这种出色功能的渴望不知所措,并且想要我的程序的副本时,会发生什么?如果他们还将电影列表存储在以冒号分隔的文本文件“ movies1.txt”中,那么一切都很好。如果他们的电影文件具有不同的名称,则可以很容易地将文件名称放入属性文件中。但是,如果他们以完全不同的方式存储电影列表的形式:SQL数据库,XML文件,Web服务或只是另一种格式的文本文件怎么办?在这种情况下,我们需要一个不同的类来获取该数据。现在,因为我已经定义了一个MovieFinder接口,所以不会改变我的moviesDirectedBy方法。但是我仍然需要某种方法来使正确的finder实现实例生效。

图1:在lister类中使用简单创建的依赖项



图1显示了这种情况的依赖性。该MovieLister班是取决于两个上 MovieFinder取决于实施方案接口和。如果它仅依赖于接口,我们会更喜欢它,但是我们如何使一个实例可以使用呢?



在我的书EAA中,我们将这种情况描述为插件。查找器的实现类在编译时未链接到程序中,因为我不知道我的朋友将使用什么。取而代之的是,我们希望我的列表器可以与任何实现一起使用,并在以后的某个时间点将其插入我的手中。问题是如何建立链接,以便我的列表器类不了解实现类,但仍然可以与实例对话以完成其工作。



将其扩展到一个真实的系统中,我们可能会有数十个这样的服务和组件。在每种情况下,我们都可以通过通过接口与这些组件进行通信来抽象化这些组件的使用(如果该组件在设计时并未考虑接口,则可以使用适配器)。但是,如果我们希望以不同的方式部署此系统,则需要使用插件来处理与这些服务的交互,以便我们可以在不同的部署中使用不同的实现。



因此,核心问题是如何将这些插件组装到应用程序中?这是这种新型的轻量级容器面临的主要问题之一,并且普遍地,它们都使用控制反转来解决。



4. 控制反转

当这些容器谈论它们因实现“控制反转”而如此有用时,我最终感到非常困惑。控制反转是框架的常见特征,因此说这些轻量级容器之所以特别是因为它们使用控制反转,就好像说我的汽车很特别,因为它带有4个轮子。



问题是:“控制权在什么方面倒置?” 当我第一次遇到控件反转时,它位于用户界面的主控件中。早期的用户界面由应用程序控制。您将有一系列命令,例如“输入名称”,“输入地址”;您的程序将驱动提示并获取对每个提示的响应。使用图形(甚至基于屏幕)UI时,UI框架将包含此主循环,而您的程序将为屏幕上的各个字段提供事件处理程序。程序的主控件被反转,从您移至框架。



对于这种新型容器,反转是关于它们如何查找插件实现的。在我幼稚的示例中,列表器通过直接实例化查找器实现。这将阻止查找程序成为插件。这些容器使用的方法是确保插件的任何用户都遵循某种约定,该约定允许单独的汇编器模块将实现注入到列表器中。



因此,我认为我们需要为该模式指定一个更具体的名称。控制反转是一个过于笼统的术语,因此人们会感到困惑。与IoC倡导者进行了大量讨论之后,我们决定使用依赖注入这个名称 。



我将首先讨论各种形式的依赖注入,但是现在我要指出,这并不是从应用程序类到插件实现中删除依赖的唯一方法。可以用来执行此操作的另一种模式是Service Locator,在解释完依赖注入之后,我将讨论它。



5. 依赖注入的形式

Dependency Injection的基本思想是拥有一个单独的对象,即一个汇编器(Assembler),该汇编器使用finder接口的适当实现填充lister类中的字段,从而产生图2所示的依赖关系图。





图2:依赖注入器的依赖



依赖项注入有三种主要样式。我为它们使用的名称是构造函数注入,Setter注入和接口注入。如果在当前有关控制反转的讨论中读到这些内容,您会听到这些信息分别称为1型IoC(接口注入),2型IoC(设定者注入)和3型IoC(构造函数注入)。我发现很难记住数字名称,这就是为什么我使用这里的名称的原因。



5.1 PicoContainer的构造方法注入

我将首先展示如何使用名为PicoContainer的轻型容器完成此注入。我从这里开始的主要原因是,ThoughtWorks的几个同事对PicoContainer的开发非常活跃(是的,这是一种公司裙带关系。)



PicoContainer使用构造函数来决定如何将finder实现注入到lister类中。为此,电影列表器类需要声明一个构造函数,其中包括需要注入的所有内容。



class MovieLister...
public MovieLister(MovieFinder finder) {
this.finder = finder;
}

取景器本身也将由pico容器管理,因此,容器将注入文本文件的文件名。



class ColonMovieFinder...
public ColonMovieFinder(String filename) {
this.filename = filename;
}

然后,需要告知pico容器与每个接口关联的实现类,以及将哪些字符串注入到查找器中。



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;
}

此配置代码通常在其他类中设置。对于我们的示例,使用我的列表器的每个朋友都可以在自己的某些安装程序类中编写相应的配置代码。当然,通常将这种配置信息保存在单独的配置文件中。您可以编写一个类来读取配置文件并适当地设置容器。尽管PicoContainer本身不包含此功能,但是有一个密切相关的项目NanoContainer,该项目提供适当的包装以允许您拥有XML配置文件。这样的纳米容器将解析XML,然后配置底层的pico容器。该项目的理念是将配置文件格式与底层机制分开。



要使用容器,您需要编写类似以下的代码。

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());
}

尽管在此示例中,我使用了构造函数注入,但PicoContainer也支持setter注入,尽管其开发人员确实更喜欢构造函数注入。

5.2 Spring框架注入

Spring框架是企业Java开发一个广泛的框架。它包括事务,持久性框架,Web应用程序开发和JDBC的抽象层。像PicoContainer的它同时支持构造函数和setter注入,但它的开发者倾向于更喜欢setter注入-这使得它在这个例子中一个合适的选择。



为了让我的电影列表管理员接受注入,我为该服务定义了一种设置方法



class MovieLister...
private MovieFinder finder;
public void setFinder(MovieFinder finder) {
this.finder = finder;
}

同样,我为文件名定义了一个setter。

class ColonMovieFinder...
public void setFilename(String filename) {
this.filename = filename;
}

第三步是设置文件的配置。Spring支持通过XML文件以及通过代码进行配置,但是XML是实现它的预期方式。

<beans>
<bean id="MovieLister" class="spring.MovieLister">
<property name="finder">
<ref local="MovieFinder"/>
</property>
</bean>
<bean id="MovieFinder" class="spring.ColonMovieFinder">
<property name="filename">
<value>movies1.txt</value>
</property>
</bean>
</beans>

然后测试看起来像这样。

public void testWithSpring() throws Exception {
ApplicationContext ctx = new FileSystemXmlApplicationContext("spring.xml");
MovieLister lister = (MovieLister) ctx.getBean("MovieLister");
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}

5.3 接口注入

第三种注入技术是定义和使用接口进行注入。Avalon是在某些地方使用此技术的框架的示例。稍后我将详细讨论,但是在这种情况下,我将通过一些简单的示例代码来使用它。



通过这项技术,我首先定义了一个用于执行注入的接口。这是将电影查找器注入对象的界面。

public interface InjectFinder {
void injectFinder(MovieFinder finder);
}

该界面由提供MovieFinder界面的人定义。任何想要使用查找器的类(例如列表器)都需要实现它。

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;
}

然后,像往常一样,我需要一些配置代码来连接实现。为简单起见,我将在代码中进行。



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);
}

当依赖项是为此容器编写的类时,对于组件来说,实现注入器接口本身是有意义的,就像我在电影查找器中所做的那样。对于通用类,例如字符串,我在配置代码中使用内部类。



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");
}
}

然后测试将使用容器。

class Tester
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());
}



容器使用声明的注入接口找出依赖关系,并使用注入器注入正确的依赖关系。(我在这里执行的特定容器实现对该技术并不重要,并且我不会展示它,因为您只会笑。)

6 使用Service Locator

依赖注入器的主要好处是,它消除了MovieLister类对具体 MovieFinder实现的依赖。这使我可以将列表提供给朋友,并让他们为自己的环境插入合适的实现。注入不是打破这种依赖关系的唯一方法,另一种方法是使用Service Locator



服务定位器的基本思想是拥有一个对象,该对象知道如何掌握应用程序可能需要的所有服务。因此,此应用程序的服务定位器将具有一种在需要时返回电影查找器的方法。当然,这只是转移了一点负担,我们仍然必须将定位器放入列表器中,从而产生图3的依赖项。

图3:服务定位器的依赖关系



在这种情况下,我将ServiceLocator用作单例注册表。然后,列表器可以在实例化时使用它来获取查找器。



class MovieLister...
MovieFinder finder = ServiceLocator.movieFinder();
class ServiceLocator...
public static MovieFinder movieFinder() {
return soleInstance.movieFinder;
}
private static ServiceLocator soleInstance;
private MovieFinder movieFinder;

与注入方法一样,我们必须配置Service Locator。在这里,我正在用代码进行操作,但是使用从配置文件中读取适当数据的机制并不难。

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;
}

这是测试代码。

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());
}

我经常听到抱怨,这些服务定位器是一件坏事,因为它们不可测试,因为您不能用它们替代实现。当然,您可以对它们进行糟糕的设计,以免遇到此类麻烦,但是您不必这样做。在这种情况下,服务定位器实例只是一个简单的数据持有者。我可以使用服务的测试实现轻松地创建定位器。



对于更复杂的定位器,我可以对服务定位器进行子类化,然后将该子类传递到注册表的类变量中。我可以更改静态方法以在实例上调用方法,而不是直接访问实例变量。我可以通过使用特定于线程的存储来提供特定于线程的定位器。所有这些都可以在不更改服务定位器客户的情况下完成。



想到这一点的一种方法是,服务定位器是注册表而不是单例。单例提供了实现注册表的简单方法,但是该实现决策很容易更改。



6.1 对定位器使用隔离的接口

上面的简单方法的问题之一是,即使MovieLister仅使用一项服务,它也依赖于完整的服务定位器类。我们可以通过使用角色接口来减少这种情况 。这样,列表器可以仅声明它需要的接口位,而不必使用完整的服务定位器接口。



在这种情况下,列表提供者的提供者还将提供一个定位器接口,它需要掌握查找器。

public interface MovieFinderLocator {
public MovieFinder movieFinder();

然后,定位器需要实现此接口以提供对查找器的访问。

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;

您会注意到,由于我们要使用接口,因此不能再通过静态方法访问服务了。我们必须使用该类来获取定位器实例,然后使用该类来获取所需的对象。



6.2 动态服务定位器

上面的示例是静态的,因为服务定位器类具有用于您需要的每个服务的方法。这不是唯一的方法,您还可以创建一个动态服务定位器,使您可以将所需的任何服务存储在其中,并在运行时进行选择。



在这种情况下,服务定位器使用映射代替每个服务的字段,并提供获取和加载服务的通用方法。

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");

总的来说,我不喜欢这种方法。尽管它肯定是灵活的,但不是很明确。我唯一能找到服务的方法是通过文本键。我更喜欢显式方法,因为通过查看接口定义可以更轻松地找到它们的位置。



6.3 同时使用定位器和Avalon进行注射

依赖注入和服务定位器不一定是互斥的概念。Avalon框架是将两者一起使用的一个很好的例子。Avalon使用服务定位器,但是使用注入来告诉组件在哪里可以找到该定位器。



Berin Loritsch使用Avalon向我发送了这个运行示例的简单版本。

public class MyMovieLister implements MovieLister, Serviceable {
private MovieFinder finder;
public void service( ServiceManager manager ) throws ServiceException {
finder = (MovieFinder)manager.lookup("finder");
}

服务方法是接口注入的一个示例,它允许容器将服务管理器注入MyMovieLister。服务管理器是服务定位器的一个示例。在此示例中,列表器未将管理器存储在字段中,而是立即使用它来查找它存储的查找器。

7. 决定使用哪个选项

到目前为止,我一直专注于解释如何看待这些模式及其变化。现在,我可以开始讨论它们的优缺点,以帮助确定使用哪些以及何时使用。

7.1 服务定位器与依赖注入

基本选择是在服务定位器和依赖注入之间。第一点是,这两种实现都提供了天真的示例中缺少的基本解耦-在两种情况下,应用程序代码都独立于服务接口的具体实现。两种模式之间的重要区别在于如何将实现提供给应用程序类。使用服务定位器,应用程序类通过向定位器的消息显式地请求它。使用注入时,没有显式请求,该服务将出现在应用程序类中-因此控制权反转。



控制反转是框架的共同特征,但这是有代价的。当您尝试调试时,它往往难以理解并导致问题。因此,总的来说,我更喜欢避免它,除非我需要它。这并不是说这是一件坏事,只是我认为它需要证明自己比更直接的选择更合理。



关键区别在于,使用服务定位器时,服务的每个用户都对定位器具有依赖性。定位器可以隐藏对其他实现的依赖关系,但是您确实需要查看定位器。因此,定位器和注入器之间的决定取决于该依赖关系是否成问题。



使用依赖注入可以帮助您更轻松地了解组件的依赖关系。使用依赖注入器,您可以仅查看注入机制,例如构造函数,并查看依赖关系。使用服务定位器,您必须在源代码中搜索对定位器的调用。具有查找引用功能的现代IDE使此操作变得更容易,但仍不像查看构造函数或设置方法那样容易。



这在很大程度上取决于服务用户的性质。如果要使用使用服务的各种类来构建应用程序,则从应用程序类到定位器的依赖关系不大。在我的示例中,将电影列表提供给我的朋友,然后使用服务定位器非常有效。他们所需要做的就是通过一些配置代码或配置文件将定位器配置为挂接正确的服务实现。在这种情况下,我认为喷油器的倒置并不能提供任何令人信服的东西。



如果列表器是我提供给其他人正在编写的应用程序的组件,则会有区别。在这种情况下,我对客户要使用的服务定位器的API不太了解。每个客户可能都有自己不兼容的服务定位器。我可以通过使用隔离的接口来解决一些问题。每个客户都可以编写一个使我的界面与他们的定位器匹配的适配器,但是无论如何,我仍然需要看到第一个定位器来查找我的特定接口。一旦适配器出现,与定位器直接连接的简便性便开始下滑。



由于使用注入器,您没有组件到注入器的依赖关系,因此一旦配置,该组件就无法从注入器获得更多服务。



人们偏爱依赖注入的一个普遍原因是它使测试更加容易。这里的重点是要进行测试,您需要轻松地用存根或模拟替换真实的服务实现。但是,依赖注入和服务定位器之间实际上并没有什么区别:两者都非常适合存根。我怀疑这种观察来自于人们没有努力确保可以轻松替换其服务定位器的项目。这是持续测试的地方,如果您无法轻松地对服务进行存根测试,那么这意味着您的设计存在严重问题。



当然,非常麻烦的组件环境(例如Java的EJB框架)会加剧测试问题。我的观点是,这类框架应将其对应用程序代码的影响降至最低,尤其是不应做会减慢编辑-执行周期的事情。使用插件替代重量级组件在很大程度上帮助了此过程,这对于诸如测试驱动开发之类的实践至关重要。



因此,主要问题是编写代码的人员希望在编写者无法控制的应用程序中使用它们。在这些情况下,即使是关于服务定位器的最小假设也是一个问题。



7.2 构造函数注入与setter注入

对于服务组合,您始终必须有一些约定才能将它们连接在一起。注入的优点主要是它需要非常简单的约定-至少对于构造函数和setter注入而言。您不必在组件中做任何奇怪的事情,并且对于进样器而言,配置所有组件非常简单。



接口注入更具侵入性,因为您必须编写许多接口才能使所有事情都得到解决。对于容器所需的一小部分接口,例如Avalon的方法,这还不错。但是组装组件和依赖项需要做很多工作,这就是为什么当前的轻量级容器使用setter和构造函数注入的原因。



在setter和构造函数注入之间进行选择很有趣,因为它反映了面向对象编程的一个更普遍的问题-应该在构造函数中还是使用setter填充字段。



我对对象的长期默认设置是尽可能多的,以便在构造时创建有效的对象。该建议可以追溯到Kent Beck的Smalltalk最佳实践模式:构造方法和构造参数方法。带参数的构造函数使您清楚地说明了在明显的位置创建有效对象的含义。如果有多种方法可以实现,请创建多个构造函数以显示不同的组合。



构造函数初始化的另一个优点是,您可以通过不提供设置器来清楚地隐藏任何不可变的字段。我认为这很重要-如果某些事情不应该更改,那么缺少二传手就可以很好地传达这一点。如果使用setter进行初始化,则可能会很痛苦。(实际上,在这种情况下,我宁愿避免使用常规的设置约定,而宁愿使用像之类的方法initFoo来强调它是您只应在出生时才做的事情。)



但是在任何情况下都有例外。如果您有很多构造函数参数,则看起来会很混乱,尤其是在没有关键字参数的语言中。的确,一个长构造函数通常是一个忙碌对象的信号,应将其拆分,但是在某些情况下,这就是您所需要的。



如果您有多种构造有效对象的方法,则可能很难通过构造函数来显示该对象,因为构造函数只能在参数的数量和类型上有所不同。这是工厂方法发挥作用的时候,它们可以结合使用私有构造函数和setter方法来实现其工作。经典的用于组件组装的工厂方法的问题在于它们通常被视为静态方法,而您不能在接口上使用它们。您可以创建一个工厂类,但这将成为另一个服务实例。工厂服务通常是一个很好的策略,但是您仍然必须使用此处的一种技术来实例化工厂。



如果您具有简单的参数(例如字符串),构造函数也会受到影响。使用setter注入,您可以为每个setter命名,以指示该字符串应该执行的操作。对于构造函数,您只是依靠位置,这很难遵循。



如果您有多个构造函数和继承关系,那么事情可能会变得特别尴尬。为了初始化所有内容,您必须提供构造函数以转发到每个超类构造函数,同时还要添加自己的参数。这可能导致构造函数的爆炸式增长。



尽管有很多缺点,我还是希望从构造函数注入开始,但是一旦我上面概述的问题开​​始成为问题,就可以准备切换到setter注入。



这个问题在提供依赖注入器作为其框架一部分的各个团队之间引起了很多争论。但是,似乎大多数构建这些框架的人已经意识到,即使偏爱其中一种机制,支持这两种机制也很重要。



7.3 代码或配置文件

一个单独但经常混淆的问题是是否使用配置文件或API上的代码来连接服务。对于大多数可能部署在许多地方的应用程序,通常最有意义的是使用单独的配置文件。几乎所有时间都将是一个XML文件,这很有意义。但是,在某些情况下,使用程序代码进行汇编会更容易。一种情况是,您有一个简单的应用程序,并且没有太多的部署差异。在这种情况下,一些代码可以比单独的XML文件更清晰。



一个相反的情况是组装非常复杂,涉及条件步骤。一旦开始接近编程语言,XML就会开始崩溃,最好使用具有所有语法的真实语言编写清晰的程序。然后,您编写一个进行组装的构建器类。如果您有不同的构建器方案,则可以提供多个构建器类,并使用简单的配置文件在它们之间进行选择。



我经常认为人们过于渴望定义配置文件。通常,编程语言会提供一种简单而强大的配置机制。现代语言可以轻松地编译小型汇编程序,这些汇编程序可用于为大型系统汇编插件。如果编译很麻烦,那么有些脚本语言也可以很好地工作。



人们常说配置文件不应使用编程语言,因为它们需要由非程序员编辑。但是,这种情况多久发生一次呢?人们真的希望非程序员会改变复杂服务器端应用程序的事务隔离级别吗?非语言配置文件仅在简单的范围内才能正常工作。如果它们变得复杂,那么该考虑使用适当的编程语言了。



目前,我们在Java世界中看到的一件事是配置文件混乱,每个组件都有自己的配置文件,这些文件与其他人的文件不同。如果使用这些组件中的许多组件,则可以轻松获得十二个配置文件来保持同步。



我的建议是始终提供一种通过编程界面轻松进行所有配置的方法,然后将单独的配置文件视为可选功能。您可以轻松构建配置文件处理以使用编程界面。如果要编写组件,则由用户决定是使用编程接口,配置文件格式,还是编写自己的自定义配置文件格式并将其绑定到编程接口中



7.4 将配置与使用分开

所有这一切中的重要问题是确保服务的配置与使用分开。确实,这是一个基本的设计原则,它是接口与实现的分离。当条件逻辑决定实例化哪个类,然后通过多态性而不是通过重复的条件代码完成对该条件的将来评估时,便会在面向对象程序中看到这种情况。



如果这种分离在单个代码库中有用,那么在使用诸如组件和服务之类的外部元素时,这一点尤为重要。第一个问题是您是否希望将实现类的选择推迟到特定的部署。如果是这样,则需要使用一些插件实现。一旦使用了插件,那么至关重要的是,插件的组装必须与应用程序的其余部分分开完成,以便您可以轻松地将不同的配置替换为不同的部署。您如何实现这一目标是次要的。此配置机制可以配置服务定位器,也可以使用注入直接配置对象。



8 一些其他问题

在本文中,我集中讨论了使用依赖注入和服务定位器进行服务配置的基本问题。还有更多的话题也值得关注,但是我还没有时间去探讨。特别是存在生命周期行为的问题。一些组件具有不同的生命周期事件:例如停止和启动。另一个问题是在这些容器中使用面向方面的思想的兴趣日益浓厚。尽管目前我还没有在本文中考虑此材料,但我还是希望通过扩展本文或撰写另一篇文章来撰写更多有关此材料的文章。



通过查看专门针对轻量级容器的网站,您可以找到有关这些想法的更多信息。从picocontainer和[spring](http://www.springsource.org/)网站上冲浪 将使您对这些问题进行更多的讨论,并开始一些其他问题。



10. 结论思想

当前的轻量级容器热潮都具有它们如何进行组装的基本模式-依赖注入器模式。依赖注入是服务定位器的有用替代方法。在构建应用程序类时,这两个类大致相同,但是我认为Service Locator由于其更直接的行为而略有优势。但是,如果要构建要在多个应用程序中使用的类,则依赖注入是一个更好的选择。



如果使用依赖注入,则有多种样式可供选择。我建议您遵循构造函数注入,除非您遇到该方法的特定问题之一,在这种情况下,请切换到setter注入。如果选择构建或获取容器,请寻找一个既支持构造函数注入又支持setter注入的容器。



在服务定位器和依赖注入之间进行选择比将应用程序中的服务配置与服务的使用分开的原理没有那么重要。



翻译自

https://martinfowler.com/articles/injection.html



3. Tips: 学习至少一个技术技巧

笔者的文章:

详解iOS打包、发布与证书体系,深入解析证书非对称加密原理 知其所以然

一个iOS应用最终能在用户的设备上使用,是经过了开发 -> 打包 -> 发布 -> 下载安装过程的。为了更易于理解,以及避免从一开始就陷入细节,本文将逆序讲述整个过程。



一、背景

在iOS开发中,大概每个新手都被各种配置、证书、打包和发布等事情折腾过,我亦如此。



教程一搜一大堆,照着教程1234也能做下来。但是在这个过程中,我会产生很多问号:



  1. 为什么程序能在模拟器上运行,却无法在真机上运行?

  2. 为什么不是每个人都能在本地打包?具备什么条件才能打包?

  3. 为什么需要证书,描述文件?

  4. 生成证书的原理是怎样的?

... ...



很多事情是知其然而不知其所以然。



为了解决心中的疑惑,我借着项目的机会,研究了一番整个打包发布的流程,以及流程中每一步操作的背后都发生了什么。



之后便总结成了这篇文章,分享给大家,希望能使新手iOS开发同学对iOS的打包、发布和证书体系有更直观的了解。



一个iOS应用最终能在用户的设备上使用,是经过了开发 -> 打包 -> 发布 -> 下载安装的过程的。



为了更易于理解,以及避免从一开始就陷入细节,本文将逆序讲述整个过程。



二、iOS应用的安装方式

作为一个iOS用户,我能通过哪些途径安装app?



  1. App Store

App Store是Apple官方的App发布平台。在App Store中搜索并安装App,也是作为一个普通用户最常用的安装方式。



  1. TestFlight

TestFlight是Apple官方的App测试平台。在上架到App Store之前,可以通过TestFlight邀请一部分用户参与测试,类似于网络游戏的公测。



  1. App Center, FIR...

除了官方的Apple Store之外,市面上还存在着App Center, FIR等非Apple官方的App管理平台。在开发过程中,我们通常会将各个环境的App上传到这些非官方的平台中,用于日常测试;另外,我们也会将其作为企业级应用的最终发布平台。



  1. 通过Xcode安装到真机



  1. 通过Xcode安装到模拟器

在开发过程中,DEV们作为特殊的iOS用户,也会通过IDE直接在真机或模拟器上进行开发和测试。这里把真机和模拟器分开,是因为它们确有不同。关于不同之处,我们将会在后文中谈到。



上面列出的,是用户,以及DEV、QA同学最常用的5种安装方式。那么这篇文章是要讲打包和发布,为什么我们要了解这些安装方式呢?



是因为不同的安装方式本身,背后就对应着不同类型的发布方式。



或者更严谨的说,不同类型的发布方式,就决定了用这种发布方式打出来的app,最终能通过哪种安装方式安装到机器上。



三、iOS应用的发布方式

作为打包的那个人,我能通过选择发布方式,来决定我的应用能让哪些用户、通过何种安装方式下载安装



虽然我们有着以上不同的安装方式,但其实本质上都是从某个平台上,下载一个软件包到本地并安装(Xcode除外)。



不同的平台做的也是同样的事情,即提供一个存放软件包的仓库,可供用户下载软件包。



发布,就是把软件包上传到发布平台。这步就无需赘述了。



那么我们再往前一步:打包。



简单来说,所谓打包,就是将源码转换成iOS系统的软件包-ipa文件iPhone application archive



对于一个iOS应用,它的打包过程包括:



  1. 选择发布方式

  2. 选择证书和描述文件

  3. 编译 & 签名

  4. 导出ipa文件



本节我们关注第一步:选择一个发布方式。



Apple提供了4种发布方式:

<center>图1 iOS应用的发布方式</center>



  1. App Store Connect -上架App Store以及TestFlight的app,用于生产环境发布

  2. Ad Hoc - 部分机器可安装的app,用于非生产环境的测试

  3. Enterprise - 企业级应用发布

  4. Development - 与Ad Hoc类似,只有后续步骤所需要的证书和描述文件不同



结合上文,安装方式和发布方式之间的关系可以表示成:

<center>图2 安装方式和发布方式之间的关系</center>



我们再对比它们之间的主要区别:

<center>图3 安装方式和发布方式之间的区别</center>



从上图中我们能得出一些结论:



  1. 能从App Store和TestFlight上安装使用的,一定是App Store Connect的发布方式。

  2. 只有App Store中app和企业级应用没有安装数量上的限制。

  3. 只要向真机上安装app,无论选择哪种安装方式或发布方式,都需要证书,签名,描述文件。



这里我自己的一些额外猜想是,Apple通过发布方式上的限制,确保真正public的应用只能通过Apple审核 ,App Store下载安装。



但大家可能会发现,企业级应用也没有任何安装数量上的限制,甚至不需要审核。那是否可以把企业级应用public的发布呢?



答案是否定的。



首先,企业级应用需要Apple企业账号,Apple对于企业级账号的发放是非常严格的。



其次,Apple规定企业级应用的下载途径不可公开,若发现公开则会有封号,应用失效的后果。



因此,虽然从能力上看企业级应用能被安装在任意一台机器上,但是从途径上Apple限制了可能性。



至于只要向真机上安装app,都需要证书,签名,描述文件,我猜测这是对每一台设备负责吧。



现在我们讲完了打包的第一步发布方式,下一步就是选择证书和描述文件。



我们已经知道,只要向真机上安装app,哪怕是Xcode直接运行,就都需要证书,签名,描述文件。



那么这些资源从哪来、怎么来,就是我们接下来的话题。

四、Apple Developer Account和Member Center

作为负责打包发布的人,我要如何、在哪管理开发和发布所需要的资源?



证书、描述文件等资源被维护在Member Center中。它是开发者的资源管理中心,可以全生命周期管理:



证书,签名,描述文件等资源

TestFlight、Apple Store上的应用

... ...

登陆Member Center需要开发者账号Apple Developer Account。



开发者账号有不同的类型。不同类型的开发者账号对应的Member Center拥有不同的能力。



1. 开发者账号的种类

<center>图4 开发者账号的种类</center>



从大类上,开发者账号分为三种:个人、组织和教育机构。教育机构这个类别我并没有接触过,也就不在这里深入。



在4个小类中,公司和个人类型的账号只有能否有团队成员这一个区别。因此实际上很多开发者会把个人类型的账号转为公司类型,便于团队协作。



也正是因为大多数应用都需要不止一个DEV来开发,所以比较常用的开发者账号类型就是支持development team的公司和企业级应用。



对于公司和企业级应用,二者之间除了账号的年费不一样之外,最重要的区别在于,它能否将应用上架App Store。



那么为什么企业级账号无法将应用上架App Store呢?



这里大概解释一下:



从前文我们已经知道,想要上架App Store,就必须选择App Store Connect的发布方式。



选了某种发布方式之后,后续步骤所需要的证书,描述文件等的类型也是不一样的。



在Member Center中,企业级账号只能生成发布企业应用所需的证书,无法生成App Store Connect的发布方式所需的证书,当然也就没有上架App Store的能力。



同样,公司账号也无法生成企业级证书,无法发布企业级应用。

2. Member Center的用途之:管理ID、设备、证书、描述文件

全生命周期管理ID、设备、证书、描述文件,是Member Cente最重要的功能之一。



下面,我们分别看看它们的概念、用途和生成方式。



(1)ID - 唯一标识符,根据用途分为App ID、Music ID、Merchant IDs等



目前我们只考虑最简单的情况,就只介绍iOS应用必须的,用于标识一个或一组应用的App ID。



下图即用公司类型的开发者账号注册一个App ID的过程:

图5 注册一个App ID



从图中我们可以的看出:



App ID需要指定应用平台

App ID与Team ID绑定在一起。即,Apple知道一个应用的ID是注册在哪个开发者账号下的,也只允许这个账号内的成员在真机上调试或打包。

App ID指定了应用的capabilities,如:获取WI-FI信息、使用钱包、健康、SIRI....

(2)设备 - 能安装该开发者账号下的应用的设备



设备的概念就更简单了。每个苹果设备都有一个唯一标识符UDID - Unique Device Identifier



将某个设备注册到开发者账号下,就是在注册时将该设备的UDID填入。同一台设备可以被注册到多个开发者账号下。



可以理解为开发者账号通过UDID列表,形成自己的设备资源池。



(3)证书 - 由Apple 证书认证中心颁发的,用于确保应用内容可靠性和完整性的凭证



证书分为两种:



  1. 开发证书,用于日常开发;

  2. 发布证书,用于应用发布。



生成一个证书的步骤也很简单:



只需要在借助keychain在本地生成一个CSR文件,然后通过开发者账号上传,成功后就会存在于证书资源池中,在失效前可随时使用下载(这里我们只需要了解生成证书的步骤,至于这个过程中都发生了什么,以及证书如何能确保应用的可靠性,我们后面会详述)。



<center>图6 生成一个证书</center>



(4)描述文件 - 一个ID,设备,证书的集合



你可能已经发现了,前面的ID,设备和证书的都是各自独立的,我们看不到它们之间有任何的联系。



而描述文件正是把这些资源整合到一起的集合。



一个描述文件包含:



  • 一个App ID

  • 开发或发布证书

  • 一组可安装该应用的设备列表(非必有)



描述文件会被打包到应用中,描述该应用的App ID、持有的发布证书、以及能被哪些设备安装。



描述文件与证书一样,也分开发和*发布*两大类型。其中,发布又被细分为Ad HocApp StoreEnterprise类型。



还记得前面说的4种发布方式吗?它们和描述文件的类型是一一对应的。



在打包的第一步选择了一个发布方式后,第二步就必须要选择相应的描述文件。



生成一个描述文件的步骤,就是选择一个类型,然后在开发者账号下的ID、设备、证书资源池中选出资源,将它们整合到一起。



最后,我们用更直观的图来表述描述文件与安装方式、发布方式之间的关系:

<center>图7 描述文件与安装方式、发布方式之间的关系</center>



至此,我们已经大致了解了开发者账号和它管理的App ID、证书、设备和描述文件,能够完成打包的第二步了。



接下来就是第三步编译和签名,我们重点关注签名。



签名与证书紧密相关。



为了更好的了解签名的原理和作用,我们将从证书开始讲起。



五、证书的生成

上一节讲过证书的生成步骤:



  1. 借助keychain在本地生成一个CSR文件



  1. 通过开发者账号将CSR上传至Member Center



  1. 从Member Center下载证书



但看这个描述,我们根本无法得知每一步的原理和目的。比如:CSR是什么,有什么用;上传CSR成功为什么能生成一个证书?这中间Apple又做了什么?



相信这些问题在这一小节结束后,你会知道答案。



1. 生成CSR文件: Keychain -> 证书助理 -> 从证书颁发机构请求证书

CSR(Certificate Signing Request)文件是用keychain生成的,包含了请求证书者的个人信息的,用于向Apple证书颁发机构(Apple Worldwide Developer Relations Certification Authority,为了简单理解,后文统称Apple Root CA)申请证书的一个文件。

<center>图9 CSR文件的内容</center>



想象一个场景:如果你去银行办理一张储蓄卡,那么银行就会要求你提供身份证,并填一份申请单,添上姓名、籍贯、常用住址等个人信息。



我们简单做一下类比:Apple Root CA就相当于银行,证书相当于储蓄卡,CSR文件就相当于储蓄卡的申请单。



生成CSR的时候发生了什么?



  1. 通过非对称加密,在本地生成了证书的公钥和私钥,保存在Keychain中(虽然与非对称加密的方式并不一致,但为了便于理解,我们把私钥类比成储蓄卡密码)

  2. 将公钥和个人信息一起组合形成了CSR



这里插播一点对非对加密的简单理解:通过非对称加密生成的一对公钥和私钥,它们能互相解密出经过对方加密后的信息,并且也只有它们才能解密。



如果我们将+-分别定义为加密和解密,那么:

图10 非对称加密



2. 通过开发者账号将CSR上传至Member Center

成功后,我们就能在Member Center上下载证书了。



回到办理银行卡的流程:你将身份证、申请单交给工作人员,工作人员确认你本人和身份证相符,然后经过一系列的操作,最终会把办理好的银行卡交给你。



银行卡中是包含了你的个人信息的,因为办理很多的业务,都需要你本人携带身份证,并保证和开户信息一致。



这正是对应了当前这一步。



类比于银行工作人员的一系列操作,Apple Root CA在从CSR到证书的过程中做了什么呢?



首先,Apple Root CA是有一个由自己颁发的证书的(CA证书)。同样,这个证书也有它对应的公钥和私钥。



当我们将CSR传给Apple Root CA,它会在验证身份之后,后用CA证书的私钥,对公钥和部分个人信息做加密,然后连同CSR中的公钥一起,形成证书,并记录在Member Center中。

<center>图11 证书生成的原理</center>



3. 从Member Center下载证书

下载证书到本地并安装。由于证书中包含证书的公钥,我们本地保存着证书的私钥,所以它们在Keychain中可以匹配得上:

<center>图12 安装证书到本机</center>



六、签名

加密应用的内容



打包的第三步:编译和签名。对应用签名,就是用证书的私钥加密应用的内容。签名会一并打包到应用中。



签名是打包的必需步骤。



签名需要证书的私钥。



证书的私钥保存在证书申请人的keychain中。

<center>图13 App的签名</center>



因此:



作为非证书申请人,如果你想在本地打包,则需要向证书申请人请求私钥。

作为证书申请人,请像保护银行卡密码一样保护私钥,尽量不分发私钥。分发私钥意味着其他人可以以你的名义打包和发布应用。

至此,我们已经介绍完了打包的核心步骤。



那么我们为什么需要证书和签名呢?

七、证书和签名的作用

用证书验证签名,从而保证App来源可信



前面我们讲了签名和证书的生成过程,这里终于到了展现它们用处的时候了。



我们将通过2步验证,最终相信应用的可靠。



首先我们来回顾前面的内容:



  • 描述文件中包含有证书

  • App中包含有描述文件和签名



除此之外,iOS设备默认装有并信任Apple Root CA证书。

<center>图14 iOS 设备上的App和Apple Root CA证书</center>



下面我们开始验证:



1. 用Apple Root CA证书,验证应用证书的有效性

应用证书的签名,是由Apple Root CA的私钥加密应用证书的公钥和一些个人信息得到的。



如果用Apple Root CA证书中的公钥,能解密应用证书的签名得到应用证书上公钥,则能证明应用证书是由Apple颁发的。

<center>图15 验证App证书的有效性</center>



2. 用验证过的应用证书,验证应用签名的有效性

应用签名,是由应用证书的私钥加密应用内容得到的。



如果用应用证书中的公钥,能解密应用签名得到应用的内容,则能证明签名有效,应用可信。

<center>图16 验证App签名的有效性</center>



八、不是总结的总结

通过以上内容,我们了解到iOS应用打包发布的流程,和证书体系。



在这里,我刻意的没做总结。



开篇的那些问题,大家找到答案了吗?



转载自

https://insights.thoughtworks.cn/ios-package-release/



4. Share: 分享一篇有观点和思考的技术文章

你确定懂?彻底搞懂 控制反转(IoC Inversion of Control )与依赖注入(DI Dependency Inversion Principle )

说明

Spring框架,核心就是IoC容器。要掌握Spring框架,就必须要理解控制反转的思想以及依赖注入的实现方式。下面,我们将围绕下面几个问题来探讨控制反转与依赖注入的关系以及在Spring中如何应用。



什么是控制反转?

什么是依赖注入?

它们之间有什么关系?

如何在Spring框架中应用依赖注入?



1. 控制反转

在讨论控制反转之前,我们先来看看软件系统中耦合的对象。

图1:软件系统中耦合的对象



从图中可以看到,软件中的对象就像齿轮一样,协同工作,但是互相耦合,一个零件不能正常工作,整个系统就崩溃了。这是一个强耦合的系统。齿轮组中齿轮之间的啮合关系,与软件系统中对象之间的耦合关系非常相似。对象之间的耦合关系是无法避免的,也是必要的,这是协同工作的基础。现在,伴随着工业级应用的规模越来越庞大,对象之间的依赖关系也越来越复杂,经常会出现对象之间的多重依赖性关系,因此,架构师和设计师对于系统的分析和设计,将面临更大的挑战。对象之间耦合度过高的系统,必然会出现牵一发而动全身的情形。



为了解决对象间耦合度过高的问题,软件专家Michael Mattson提出了IoC理论,用来实现对象之间的“解耦”。



控制反转(Inversion of Control)是一种是面向对象编程中的一种设计原则,用来减低计算机代码之间的耦合度。其基本思想是:借助于“第三方”实现具有依赖关系的对象之间的解耦。



图2:IoC解耦过程



由于引进了中间位置的“第三方”,也就是IOC容器,使得A、B、C、D这4个对象没有了耦合关系,齿轮之间的传动全部依靠“第三方”了,全部对象的控制权全部上缴给“第三方”IOC容器,所以,IOC容器成了整个系统的关键核心,它起到了一种类似“粘合剂”的作用,把系统中的所有对象粘合在一起发挥作用,如果没有这个“粘合剂”,对象与对象之间会彼此失去联系,这就是有人把IOC容器比喻成“粘合剂”的由来。

我们再来看看,控制反转(IOC)到底为什么要起这么个名字?我们来对比一下:



  1. 软件系统在没有引入IOC容器之前,如图1所示,对象A依赖于对象B,那么对象A在初始化或者运行到某一点的时候,自己必须主动去创建对象B或者使用已经创建的对象B。无论是创建还是使用对象B,控制权都在自己手上。



  1. 软件系统在引入IOC容器之后,这种情形就完全改变了,如图2所示,由于IOC容器的加入,对象A与对象B之间失去了直接联系,所以,当对象A运行到需要对象B的时候,IOC容器会主动创建一个对象B注入到对象A需要的地方。

通过前后的对比,我们不难看出来:对象A获得依赖对象B的过程,由主动行为变为了被动行为,控制权颠倒过来了,这就是“控制反转”这个名称的由来。



  1. 控制反转不只是软件工程的理论,在生活中我们也有用到这种思想。再举一个现实生活的



例子:

海尔公司作为一个电器制商需要把自己的商品分销到全国各地,但是发现,不同的分销渠道有不同的玩法,于是派出了各种销售代表玩不同的玩法,随着渠道越来越多,发现,每增加一个渠道就要新增一批人和一个新的流程,严重耦合并依赖各渠道商的玩法。实在受不了了,于是制定业务标准,开发分销信息化系统,只有符合这个标准的渠道商才能成为海尔的分销商。让各个渠道商反过来依赖自己标准。反转了控制,倒置了依赖。



我们把海尔和分销商当作软件对象,分销信息化系统当作IOC容器,可以发现,在没有IOC容器之前,分销商就像图1中的齿轮一样,增加一个齿轮就要增加多种依赖在其他齿轮上,势必导致系统越来越复杂。开发分销系统之后,所有分销商只依赖分销系统,就像图2显示那样,可以很方便的增加和删除齿轮上去。



2. 依赖注入

依赖注入就是将实例变量传入到一个对象中去(Dependency injection means giving an object its instance variables)。



2.1 什么是依赖

如果在 Class A 中,有 Class B 的实例,则称 Class A 对 Class B 有一个依赖。例如下面类 Human 中用到一个 Father 对象,我们就说类 Human 对类 Father 有一个依赖。



public class Human {
...
Father father;
...
public Human() {
father = new Father();
}
}



仔细看这段代码我们会发现存在一些问题:



  1. 如果现在要改变 father 生成方式,如需要用new Father(String name)初始化 father,需要修改 Human 代码;

  2. 如果想测试不同 Father 对象对 Human 的影响很困难,因为 father 的初始化被写死在了 Human 的构造函数中;

  3. 如果new Father()过程非常缓慢,单测时我们希望用已经初始化好的 father 对象 Mock 掉这个过程也很困难。



2.2 依赖注入

上面将依赖在构造函数中直接初始化是一种 Hard init 方式,弊端在于两个类不够独立,不方便测试。我们还有另外一种 Init 方式,如下:



public class Human {
...
Father father;
...
public Human(Father father) {
this.father = father;
}
}



上面代码中,我们将 father 对象作为构造函数的一个参数传入。在调用 Human 的构造方法之前外部就已经初始化好了 Father 对象。像这种非自己主动初始化依赖,而通过外部来传入依赖的方式,我们就称为依赖注入。

现在我们发现上面 1 中存在的两个问题都很好解决了,简单的说依赖注入主要有两个好处:



  1. 解耦,将依赖之间解耦。

  2. 因为已经解耦,所以方便做单元测试,尤其是 Mock 测试。



2.3 控制反转和依赖注入的关系

我们已经分别解释了控制反转和依赖注入的概念。有些人会把控制反转和依赖注入等同,但实际上它们有着本质上的不同。



  • 控制反转是一种思想

  • 依赖注入是一种设计模式



IoC框架使用依赖注入作为实现控制反转的方式,但是控制反转还有其他的实现方式,例如说ServiceLocator,所以不能将控制反转和依赖注入等同。



2.4 Spring中的依赖注入

上面我们提到,依赖注入是实现控制反转的一种方式。下面我们结合Spring的IoC容器,简单描述一下这个过程。



class MovieLister...
private MovieFinder finder;
public void setFinder(MovieFinder finder) {
this.finder = finder;
}
class ColonMovieFinder...
public void setFilename(String filename) {
this.filename = filename;
}



我们先定义两个类,可以看到都使用了依赖注入的方式,通过外部传入依赖,而不是自己创建依赖。那么问题来了,谁把依赖传给他们,也就是说谁负责创建finder,并且把finder传给MovieLister。答案是Spring的IoC容器。



要使用IoC容器,首先要进行配置。这里我们使用xml的配置,也可以通过代码注解方式配置。下面是spring.xml的内容



<beans>
<bean id="MovieLister" class="spring.MovieLister">
<property name="finder">
<ref local="MovieFinder"/>
</property>
</bean>
<bean id="MovieFinder" class="spring.ColonMovieFinder">
<property name="filename">
<value>movies1.txt</value>
</property>
</bean>
</beans>



在Spring中,每个bean代表一个对象的实例,默认是单例模式,即在程序的生命周期内,所有的对象都只有一个实例,进行重复使用。通过配置bean,IoC容器在启动的时候会根据配置生成bean实例。具体的配置语法参考Spring文档。这里只要知道IoC容器会根据配置创建MovieFinder,在运行的时候把MovieFinder赋值给MovieLister的finder属性,完成依赖注入的过程。



下面给出测试代码



public void testWithSpring() throws Exception {
ApplicationContext ctx = new FileSystemXmlApplicationContext("spring.xml");//1
MovieLister lister = (MovieLister) ctx.getBean("MovieLister");//2
Movie[] movies = lister.moviesDirectedBy("Sergio Leone");
assertEquals("Once Upon a Time in the West", movies[0].getTitle());
}



  1. 根据配置生成ApplicationContext,即IoC容器。

  2. 从容器中获取MovieLister的实例。



2.5 总结

  1. 控制反转是一种在软件工程中解耦合的思想,调用类只依赖接口,而不依赖具体的实现类,减少了耦合。控制权交给了容器,在运行的时候才由容器决定将具体的实现动态的“注入”到调用类的对象中。



  1. 依赖注入是一种设计模式,可以作为控制反转的一种实现方式。依赖注入就是将实例变量传入到一个对象中去(Dependency injection means giving an object its instance variables)。



  1. 通过IoC框架,类A依赖类B的强耦合关系可以在运行时通过容器建立,也就是说把创建B实例的工作移交给容器,类A只管使用就可以。



3 汽车与轮子的关系 理解IoC 和 DI

要了解控制反转( Inversion of Control ), 我觉得有必要先了解软件设计的一个重要思想:依赖倒置原则(Dependency Inversion Principle )。



3.1 什么是依赖倒置原则?

假设我们设计一辆汽车:先设计轮子,然后根据轮子大小设计底盘,接着根据底盘设计车身,最后根据车身设计好整个汽车。这里就出现了一个“依赖”关系:汽车依赖车身,车身依赖底盘,底盘依赖轮子。

这样的设计看起来没问题,但是可维护性却很低。假设设计完工之后,上司却突然说根据市场需求的变动,要我们把车子的轮子设计都改大一码。这下我们就蛋疼了:因为我们是根据轮子的尺寸设计的底盘,轮子的尺寸一改,底盘的设计就得修改;同样因为我们是根据底盘设计的车身,那么车身也得改,同理汽车设计也得改——整个设计几乎都得改!



我们现在换一种思路。我们先设计汽车的大概样子,然后根据汽车的样子来设计车身,根据车身来设计底盘,最后根据底盘来设计轮子。这时候,依赖关系就倒置过来了:轮子依赖底盘, 底盘依赖车身, 车身依赖汽车。



这时候,上司再说要改动轮子的设计,我们就只需要改动轮子的设计,而不需要动底盘,车身,汽车的设计了。



这就是依赖倒置原则——把原本的高层建筑依赖底层建筑“倒置”过来,变成底层建筑依赖高层建筑。高层建筑决定需要什么,底层去实现这样的需求,但是高层并不用管底层是怎么实现的。这样就不会出现前面的“牵一发动全身”的情况。



3.2 控制反转 Inversion of Control



就是依赖倒置原则的一种代码设计的思路。具体采用的方法就是所谓的依赖注入(Dependency Injection)。其实这些概念初次接触都会感到云里雾里的。说穿了,这几种概念的关系大概如下:

为了理解这几个概念,我们还是用上面汽车的例子。只不过这次换成代码。我们先定义四个Class,车,车身,底盘,轮胎。然后初始化这辆车,最后跑这辆车。代码结构如下:

这样,就相当于上面第一个例子,上层建筑依赖下层建筑——每一个类的构造函数都直接调用了底层代码的构造函数。假设我们需要改动一下轮胎(Tire)类,把它的尺寸变成动态的,而不是一直都是30。我们需要这样改:



由于我们修改了轮胎的定义,为了让整个程序正常运行,我们需要做以下改动:



由此我们可以看到,仅仅是为了修改轮胎的构造函数,这种设计却需要修改整个上层所有类的构造函数!在软件工程中,这样的设计几乎是不可维护的——在实际工程项目中,有的类可能会是几千个类的底层,如果每次修改这个类,我们都要修改所有以它作为依赖的类,那软件的维护成本就太高了。



所以我们需要进行控制反转(IoC),及上层控制下层,而不是下层控制着上层。我们用依赖注入(Dependency Injection)这种方式来实现控制反转。所谓依赖注入,就是把底层类作为参数传入上层类,实现上层类对下层类的“控制”。这里我们用构造方法传递的依赖注入方式重新写车类的定义:



这里我们再把轮胎尺寸变成动态的,同样为了让整个系统顺利运行,我们需要做如下修改:

看到没?这里我只需要修改轮胎类就行了,不用修改其他任何上层类。这显然是更容易维护的代码。不仅如此,在实际的工程中,这种设计模式还有利于不同组的协同合作和单元测试:比如开发这四个类的分别是四个不同的组,那么只要定义好了接口,四个不同的组可以同时进行开发而不相互受限制;而对于单元测试,如果我们要写Car类的单元测试,就只需要Mock一下Framework类传入Car就行了,而不用把Framework, Bottom, Tire全部new一遍再来构造Car。



3.3 Setter传递和接口传递 的依赖注入

这里我们是采用的构造函数传入的方式进行的依赖注入。其实还有另外两种方法:Setter传递和接口传递。这里就不多讲了,核心思路都是一样的,都是为了实现控制反转。

看到这里你应该能理解什么控制反转和依赖注入了。



4 控制反转容器(IoC Container)

那什么是控制反转容器(IoC Container)呢?其实上面的例子中,对车类进行初始化的那段代码发生的地方,就是控制反转容器。

显然你也应该观察到了,因为采用了依赖注入,在初始化的过程中就不可避免的会写大量的new。这里IoC容器就解决了这个问题。这个容器可以自动对你的代码进行初始化,你只需要维护一个Configuration(可以是xml可以是一段代码),而不用每次初始化一辆车都要亲手去写那一大段初始化的代码。这是引入IoC Container的第一个好处。



IoC Container的第二个好处是:我们在创建实例的时候不需要了解其中的细节。在上面的例子中,我们自己手动创建一个车instance时候,是从底层往上层new的:

这个过程中,我们需要了解整个Car/Framework/Bottom/Tire类构造函数是怎么定义的,才能一步一步new/注入。



而IoC Container在进行这个工作的时候是反过来的,它先从最上层开始往下找依赖关系,到达最底层之后再往上一步一步new(有点像深度优先遍历):

这里IoC Container可以直接隐藏具体的创建实例的细节,在我们来看它就像一个工厂:





我们就像是工厂的客户。我们只需要向工厂请求一个Car实例,然后它就给我们按照Config创建了一个Car实例。我们完全不用管这个Car实例是怎么一步一步被创建出来。



实际项目中,有的Service Class可能是十年前写的,有几百个类作为它的底层。假设我们新写的一个API需要实例化这个Service,我们总不可能回头去搞清楚这几百个类的构造函数吧?IoC Container的这个特性就很完美的解决了这类问题——因为这个架构要求你在写class的时候需要写相应的Config文件,所以你要初始化很久以前的Service类的时候,前人都已经写好了Config文件,你直接在需要用的地方注入这个Service就可以了。这大大增加了项目的可维护性且降低了开发难度。



5 解耦的思路

消费者X需要消费类Y来完成某项工作。那都是自然而然的,但是X真的需要知道它使用Y吗?



知道X使用具有Y的行为,方法,属性等的东西而又不知道是谁真正实现了行为,这还不够吗?



通过提取X在Y中使用的行为的抽象定义(如下图I所示),并让消费者X使用该实例代替Y,它可以继续执行其操作而不必了解有关Y的细节。



在上面的图示中,Y实现了I,而X使用了I的实例。尽管X仍然很可能仍使用Y,但有趣的是X并不知道这一点。它只知道它使用实现I的东西。



参考

https://www.zhihu.com/question/23277575



https://www.jianshu.com/p/07af9dbbbc4b



https://martinfowler.com/articles/injection.html



http://joelabrahamsson.com/inversion-of-control-an-introduction-with-examples-in-net/



发布于: 2020 年 11 月 29 日阅读数: 29
用户头像

John(易筋)

关注

问渠那得清如许?为有源头活水来 2018.07.17 加入

工作10+年,架构师,曾经阿里巴巴资深无线开发,汇丰银行架构师/专家。开发过日活过亿的淘宝Taobao App,擅长架构、算法、数据结构、设计模式、iOS、Java Spring Boot。易筋为阿里巴巴花名。

评论

发布
暂无评论
动态规划解决爬楼梯算法,彻底搞懂AppStore证书体系、彻底搞懂控制反转IoC,依赖注入DIP, John 易筋 ARTS 打卡 Week 28