在这一章,我们将介绍和探索在开发 Jenkins 插件时使用的理论和设计概念。我们将在这里用一些通用的例子来介绍高级概念,为接下来的两章做准备,在这两章中我们将看到如何真正实现这些想法。
在本章中,我们将了解以下设计模式:
- 接口
- 抽象类
- 一个
此外,我们将回顾几个重要的设计概念:
- 合同设计
- 扩展点
- 创建扩展
- 释文
有成千上万的插件可供 Jenkins 使用,它们涵盖了广泛的任务,并为使用和使用 Jenkins 的社区提供了大量宝贵的资源。许多现有的插件都是从提供简单的功能和有限的功能开始的,但是它们中的大多数已经成长和发展成为非常成熟的软件,提供很大程度的功能。许多插件也被合并到 Jenkins 的核心功能中——将它们从一个额外的、可选的附加组件变成默认情况下在 Jenkins 内部提供的代码。
Jenkins 及其插件系列成功的一个主要原因是从一开始就被用来开发和扩展 Jenkins 的设计理念。这种软件开发方法鼓励人们一起工作,使项目能够相互受益,并为这个项目创建了一个由开发人员和贡献者组成的高效协作社区。
当您第一次考虑为 Jenkins 开发自己的插件时,有几个问题您应该首先解决——以下链接详细描述了您在开始开发自己的新插件之前应该采取的步骤:
https://wiki . JENKINS-ci . org/display/JENKINS/Before+starting+a+new+plugin
这背后的意图是提高插件的质量,避免重复。这种方法试图鼓励现有的和未来的或提议的插件的开发者一起工作,并建立在现有的功能上,而不是有过多的非常相似的插件,所有的插件都做一些稍微不同的事情。
如果你正在寻找一些在当前插件列表中不可用的附加功能,可能有人正在努力提供这个功能。如果您在开发社区中公开您的需求和意图,这可能会为您节省很多时间和麻烦。你可以提供合作开发这个新插件,而不是自己滚动。这种合作的最终结果更有可能产生一个受欢迎的高质量产品,而不是两个开发人员创建一个类似的功能。也有可能你会发现你正在寻找的绝大部分功能已经在一个相关的插件中可用,通过一点信息和协作,你可能能够利用这一点通过重用大部分现有的代码来添加一个新的特性。
所有这些协作、代码重用和增强在很大程度上都是通过使用扩展点来实现的,这些扩展点代表了插件或 Jenkins 功能的某个方面。这些是接口和抽象类,通过声明和公开的入口点来实现不同插件和 Jenkins 核心功能之间的交互和重用,这些入口点为文档化的合同提供和执行服务。
**我们现在将快速了解这些想法背后的理论,这样当我们编写自己的插件时,我们将了解幕后发生了什么,以及为什么我们从一开始就考虑到这种重用和扩展。
Java 中的接口是机制,用于提供和声明契约 ,该契约定义了如何与现有软件进行交互和重用。这种方法背后的主要思想是,它取消了知道事情是如何在内部完成的要求;您只需要知道所需的输入参数应该是什么,以及通过调用接口可以预期什么。代码的内部工作方式和处理方式并不重要,只要你遵守声明的契约,一切都应该没问题。
这种“契约式设计”方法的另一个主要好处是,它减少了代码和流程更新对外部用户的影响。例如,如果您在一个名为calculator
的类上调用一个add
接口,该接口接受两个数字并返回结果,您(作为该服务的消费者)不需要知道或关心加法是如何完成的——在内部,该类可以简单地将两个 Java 整数相加,或者输入变量被传递给云中某个地方的 web 服务,该服务将答案返回给calculator
。所使用的代码和方法可以按照calculator
的开发者所希望的任何方式完全重新设计和重写,但是只要每个人都坚持约定的合同和接口,外部消费者就不应该受到影响。
这个明确定义的接口也使得编写自动化回归测试变得更加容易。当您知道有一个定义明确且稳定的接口时,针对它编写测试通常很简单,不需要太多维护,因为接口通常不太可能被更改。只要有相关的代码更改,这些测试就可以作为配置项构建的一部分自动重新运行,任何差异都应该很容易识别。
要用 Java 创建一个接口,我们在类定义中使用接口关键字:
interface Vehicle {
// Vehicle methods
// …
}
对于使用这个接口的外部类,我们在类声明中使用实现关键字,如下:
class Motorbike implements Vehicle {
// Vehicle Methods
// …
// Motorbike Methods
// …
}
由于Motorbike
类已经声明它实现了Vehicle
,它将需要实现在Vehicle
中声明的每一个方法。Java 编译器将确保这是在编译时完成的。对于我们的Vehicle
示例,这些方法可能包括逻辑功能,例如启动、停止、左转、右转、制动和加速。Motorbike
类特定的方法可以包括细节,如“弹出一个轮子”,扩展支架,摔倒,等等。
Java 中的抽象类提供了其他类也可以使用的高级功能。不能直接创建抽象类,但可以实现从抽象类派生的另一个类。
最简单的解释是,抽象类是一个事物的类型,但不是一个事物——我的意思是,你可以有一个抽象类,就像我们的Vehicle
例子,它声明了我们提到的所有方法,但你永远不能只创建一个交通工具——你必须有特定的东西,比如汽车、摩托车、气垫船、直升机等等;你不能只有一辆普通车。
我们所有的车辆都略有不同,但共享相同的基本功能——它们可以行驶,可以停车,也可以转弯。因此,这种通用的功能集可以被建模为抽象(Vehicle
)类的基本方法,无论何时你创建一种新的车辆类型,你都可以使用它们。
要在 Java 中创建一个抽象类,必须使用abstract
关键字:
abstract class Vehicle{}
通常,抽象类将只定义方法(go、stop、turn),子类将提供它们的实际实现。
我们的Motorbike
类将扩展这个抽象类:
class Motorbike extends Vehicle {}
扩展抽象类的子类称为 具体类:
与抽象类的概念和逻辑分组不同,这些表示真实的、有形的对象。
扩展点利用抽象和接口来允许和鼓励功能的重用。
在下面的图中,存款声明了一个名为的扩展点转移到储蓄。如果我们认为这是一段现有的代码,为了这个例子,如果我们想创建一个新的储蓄账户对象,我们可以扩展存款货币已经提供的功能,并使用它来实现一个名为储蓄账户的新功能,它扩展了存款货币。这意味着它将使用大部分存款货币功能,并且还将提供自己的附加功能。
在另一个例子中,我们将现有的Open Account
代码扩展到Add Joint Account Holder
。这使用了许多Open Account
方法,但也声明了一些特定于第二个申请人的方法。下图显示了这些关系:
如果我们有多个应用,我们可以扩展开放账户来创建一个新的添加联合账户持有人对象。这个新的对象将包含并重用大量的开放账户代码,但是为了迎合二级账户持有人,它会做一些稍微不同的事情。
抽象类型通常是 Java 编程和面向对象设计中的一个关键概念。它们有时被称为 存在型类型,这有助于强化它们是什么——一个事物的类型,但是没有必要的实现或属性来实际成为一个事物*。*
*# 单线态
在我们继续讨论高级和设计理论主题并看一下在 Jenkins 中实现扩展之前,还有一个 Java 设计模式我们仍然需要讨论 Singleton 模式。
当您希望确保给定类只有零个或一个实例时,可以使用单例。
通常,当您需要控制并发操作时,会出现这种模式——通过确保最多只有一个可能的实例,我们可以确定我们不会面临任何并发或竞争条件,因为这个类(及其代码)在任何给定时间都肯定会成为唯一可能的实例。通常,单例将被许多不同的函数使用,它的目的是安全地处理和管理这个需求。
一个常见的单例例子是日志实用程序。例如,在任何时间点从系统的几个不同区域获取消息的类。然后,它打开一个日志文件,并将消息附加到该文件中。我们不希望两个类同时写入同一个日志文件——这会导致混乱并以可怕的方式结束——所以控制和访问由类的一个实例来管理和限制。这个实例将被保证拥有所有权并可以自由地写入日志文件,并且它将是安全的,因为它知道同一类中没有其他实例同时做同样的事情——它安全地管理“将此信息写入日志文件”功能。
希望使用“写入日志文件”方法的每一段代码都将尝试初始化 Singleton 对象。如果这个对象的实例已经存在,我们将重用它,如果当前没有实例,将创建一个实例。然后,它将保持对其他用户可用,直到该程序存在,或者它被清理。
单例实例化是通过私有构造函数来管理的,因此只有单例中的代码才能创建它,如下所示:
public class Singleton {
private static Singleton uniqueInstance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return uniqueInstance;
}
public String getDescription() {
return "Singleton class";
}
}
这被称为急切实例化,因为我们将在每次调用getInstance()
方法之前创建一个新的 Singleton 对象。
对此的替代方法是使用 Lazy 实例化,如下所示:
public class Singleton {
private static Singleton uniqueInstance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}
return uniqueInstance;
}
public String getDescription() {
return "Singleton class";
}
}
这里,我们使用了一个静态 Singleton 实例并同步了getInstance()
方法。比较这两种方法应该有助于您决定最适合自己需求的方法。在 UML 中,单例可以这样记录:
正如我们到目前为止所看到的,一旦我们理解了接口或抽象类背后的逻辑,创建它们就很简单了。更容易声明一个接口或抽象类,然后实现所需的功能。
一旦您理解了何时使用每种设计模式以及哪种方法适合您的需求,创建单例也很简单。
如果我们在为 Jenkins 插件创建或添加组件时牢记这个模型,我们应该能够识别适当的机会,在这些机会中,公开一个接口并创建一个扩展点供其他人使用是有帮助的。例如,如果您正在开发一个插件,该插件出于某种原因将 Jenkins 作业的历史转换为 CSV 文件,以便可以在电子表格中导出和分析,您将编写函数将一些数据转换为 CSV 值—这可以声明为扩展点,只要传递的数据是指定的类型,其他人也可以重用您的代码将其数据转换为 CSV,而不是每个人都实现相同的函数,这将导致不必要的重复。
要在 Jenkins 中定义或创建扩展,我们使用@Extension
注释类型。
这个注释由 Jenkins 拾取,新的扩展将被添加到一个ExtensionList
对象中,然后可以通过ExtensionFinder
找到该扩展。
关于扩展注释的更多细节可以在这里找到:http://javadoc.jenkins-ci.org/?hudson/Extension.html。
以下示例显示了Animal
扩展点的声明:
/**
* Extension point that defines different kinds of animals
*/
public abstract class Animal implements ExtensionPoint {
...
/**
* All registered {@link Animal}s.
*/
public static ExtensionList<Animal> all() {
return Hudson.getInstance().getExtensionList(Animal.class);
}
}
这说明了一个实现ExtensionPoint
: 的抽象类。
在本章中,我们研究了几种主要设计模式背后的概念,并了解了何时使用每种方法以及为什么要这样做。
如果你是一名经验丰富的 Java 程序员,这些概念应该非常熟悉,如果不是,那么希望这将成为一个基础,帮助你不仅理解我们在后续章节中正在做什么,而且理解我们为什么要这样做。
在这一章的开始,我们触及了插件开发背后的哲学——人们应该尽可能寻求协作、重用和扩展现有代码来提供新的功能。如果每个人都为自己的特殊需求创建自己的插件,而不是合作和贡献现有的努力,将会有大量的重复和复制,结果质量会差得多。
这种精神和前面的设计方法创造了一个插件开发人员社区,他们通过提供大量的功能来生产高质量的软件,这些功能使 Jenkins 用户能够适应和扩展 Jenkins 来执行令人难以置信的多种任务。
在下一章中,我们将建立在这些知识的基础上,并在开发我们的第一个 Jenkins 插件时,看到我们在这里介绍的概念被真正使用。***