java反射用法示例
by David Rieger
大卫·里格(David Rieger)
通过实际示例学习Go反射和通用设计 (Learn Go reflections and generic designs with a practical example)
Reflections allow us to inspect and modify a program’s structure at its own runtime. In this article we will be looking at some parts of the go reflect
package API and applying them to a real-world use case by building a generic application configuration mechanism.
反射使我们能够在自己的运行时检查和修改程序的结构。 在本文中,我们将研究go reflect
包API的某些部分,并通过构建通用的应用程序配置机制将其应用于实际用例。
我们拥有和想要的 (What we have and what we want)
We have implemented a Go data analysis application which will take data from an inventory database of a bookshop, process it, and turn it into human-readable statistical models that reflect the status of our inventory.
我们已经实现了Go 数据分析应用程序,该应用程序将从书店的库存数据库中获取数据,进行处理,然后将其转换为可人类读取的统计模型 ,以反映我们的库存状态。
The output could, for example, be a list of books that were published by a certain author, or a bar chart depicting the number of books in our inventory per decade of publishing.
例如,输出可以是某位作者出版的书籍列表,或者是描绘每十年出版中我们库存中的书籍数量的条形图。
What we want is a way for us to configure what the application generates from the raw data in an abstract manner. There might be hundreds of algorithms that all process the raw data in different ways and produce different outputs, but not all outputs are relevant to us every time we run the application. We want to be able to configure our application to suit our needs and then just let it do what it has to do in order to deliver what we want from it.
我们想要的是一种以抽象方式配置应用程序从原始数据生成的内容的方式。 可能有数百种算法,它们全部以不同的方式处理原始数据并产生不同的输出,但是并不是每次我们运行该应用程序时,所有输出都与我们相关。 我们希望能够配置我们的应用程序以满足我们的需求,然后让它做它必须做的事情,以便从中交付我们想要的东西。
We also want to be able to let the analytical process run — with a fixed configuration — autonomously and periodically on a job scheduler (e.g. Jenkins, Rundeck or cron) and report to us the output we are interested in, without having to interact with it before each run.
我们还希望能够使分析过程以固定的配置在作业调度程序 (例如Jenkins,Rundeck或cron) 上自动且定期地运行 ,并向我们报告感兴趣的输出,而无需与之交互每次运行之前。
Eventually, we want the application to be easily extensible so that we can add new algorithms with as little effort as possible and without breaking any workflows.
最终,我们希望该应用程序易于扩展,以便我们可以在不中断任何工作流的情况下以最小的努力添加新的算法。
Our core requirements on the architecture, therefore, are, in short:
简而言之,我们对架构的核心要求是:
- Persistent and abstracted configuration 持久和抽象的配置
- An interface through which both human users as well as other software (the job scheduler) can run the application 一个界面,人类用户和其他软件(作业计划程序)都可以通过该界面运行应用程序
- Intuitive extensibility 直观的可扩展性
方法1:自满 (Approach 1: The complacent)
The most straightforward way to implement this is by first loading the raw data into our program, calling every analytical algorithm we want to run against it from the application’s main function, and eventually putting it all together in one nice report.
实现此目的最直接的方法是,首先将原始数据加载到我们的程序中,然后从应用程序的主函数中调用我们要针对它运行的所有分析算法,最后将所有这些算法汇总到一个好的报告中。
package main
func main() {
rawData := Parse("books_db.flatfile")
// Run algorithms of our choice against the data
bpDChart := amountOfBooksPerDecade(rawData)
bpAList := booksPerAuthor(rawData)
createReport(bpDChart, bpAList)
}
In terms of intuitive extensibility, this is probably as good as it gets (intuitive doesn’t imply good). Whenever we want to add a new analytical algorithm, we just implement it and call it in the main function if we want to run it.
就直观可扩展性而言,这可能和它获得的效果一样好(直觉并不意味着好)。 每当我们想添加一个新的解析算法时,只要执行它就可以在main函数中调用它。
However, there’s an obvious drawback: We need to touch the core application code and recompile the application not only whenever we extend the business logic, but also each time we want to change what is being generated by it (that is, each time we want to reconfigure it).
但是,有一个明显的缺点:我们不仅需要扩展业务逻辑,还需要触摸核心应用程序代码并重新编译应用程序,还需要每次更改其生成的内容时(即,每次需要更改时)重新配置)。
This approach doesn’t provide any sort of interface for the user to configure the application. It also doesn’t really address our requirement in an abstract manner to define what the application does. The developer of the software is basically the only one who can configure its behavior.
这种方法不为用户提供任何类型的界面来配置应用程序。 它也并没有以抽象的方式真正满足我们的要求来定义应用程序的功能 。 该软件的开发人员基本上是唯一可以配置其行为的人。
Of course, we could add a configuration file to the mix. This would allow us to select what algorithms to execute and which ones to omit without having to touch or recompile the application code. However, it will require some logic that maps the entries in the configuration file to (potentially hundreds of) functions in my application.
当然,我们可以将配置文件添加到混合中。 这将使我们无需触摸或重新编译应用程序代码即可选择要执行的算法和要忽略的算法。 但是,将需要一些逻辑将配置文件中的条目映射到我的应用程序中的功能(可能有数百个)。
This logic will need to be adjusted each time I extend the application by a new analytical algorithm. Eventually, we will probably end up with a bunch of if statements running or omitting algorithms based on the presence or absence of strings in the configuration file. More on this later.
每当我通过新的分析算法扩展应用程序时,都需要调整此逻辑。 最终,基于配置文件中字符串的存在或不存在,我们可能最终会遇到一堆if语句运行或省略算法。 稍后再详细介绍。
方法2:客户友好 (Approach 2: The client-friendly)
The most common and perhaps cleanest way would be to provide a nice interface — a published API — on top of the analysis software, and a separate client, which talks to this API. The client can then decide whichever output it is interested in and call the appropriate API endpoints with according parameters. On the server side — our original analytics software — we then just run the relevant algorithms against the raw data, produce a report, and send it back to the client.
最常见,也许最简洁的方法是在分析软件之上提供一个不错的接口(已发布的API),并提供一个与该API通讯的单独客户端。 然后,客户端可以决定其感兴趣的任何输出,并根据参数调用适当的API端点。 在服务器端(我们的原始分析软件)上,我们仅对原始数据运行相关算法,生成报告,然后将其发送回客户端。
This probably makes for the cleanest architecture, but there are cases when it just isn’t the most ideal.
这可能是最干净的体系结构,但是在某些情况下,它并不是最理想的。
Looking at the requirements above and the previous approach, creating a separate client just moves the original problem from one place to another. Say I want to run the analytics application periodically with a certain setup (that is, with a certain definition of what output I’m interested in) and without having to interact with the application or its client each time it is run. I will need some sort of persistent configuration that maps to functions in my application. Whether I do that directly on the backend or on the client side doesn’t really matter to me.
考虑到上述要求和以前的方法,创建一个单独的客户只会将原始问题从一个地方转移到另一个地方。 假设我想通过某种设置(即,对我感兴趣的输出进行某种定义)定期运行分析应用程序,而不必每次运行时都与应用程序或其客户端进行交互。 我将需要某种持久性配置,以映射到应用程序中的功能。 我是直接在后端还是在客户端这样做对我来说并不重要。
Additionally, the extra interface and the client introduce a lot of inertia in terms of extensibility. Whenever we add a new analytical algorithm, which produces a different kind of statistical model, we need to adapt the interface of the service as well as the client which talks to this interface. Depending on who the client is, this might be well worth the effort, but in our case, it could just be unjustifiable overhead.
此外,额外的接口和客户端在可扩展性方面引入了很多惯性。 每当我们添加新的分析算法(该分析算法会产生不同类型的统计模型)时,我们都需要调整服务的接口以及与该接口进行通信的客户端。 根据客户是谁,这可能值得付出努力,但就我们而言,这可能只是不合理的开销。
If my client was a human user who wants to press a couple of buttons to receive certain results at certain times, then yes, having a clean API exposing my analytics software is a must. But in this case, where the client is some job scheduler which we only touch every now and then, something else might be more suitable.
如果我的客户是人类用户,想要按几个按钮在特定时间接收特定结果,那么是的,必须有一个干净的API来暴露我的分析软件。 但是在这种情况下,如果客户是某个工作计划程序,而我们偶尔会碰到它,那么其他方法可能会更合适。
方法3:通用 (Approach 3: The generic)
In order to address our requirements, we need to do two things: provide a design that doesn’t require us to maintain separate application layers (such as a segregated interface and a client) when extending the business logic, while at the same time allowing the client to define behavior without having to touch the application’s source code.
为了满足我们的需求,我们需要做两件事:提供一种在扩展业务逻辑时不需要我们维护单独的应用程序层(例如,隔离的接口和客户端)的设计,而同时允许客户端定义行为,而无需接触应用程序的源代码。
The first approach plus a configuration file would provide such a design. Writing a simple configuration file, parsing it, and then finding the appropriate algorithms to run is obvious enough. However, it doesn’t address extensibility very well.
第一种方法加上配置文件将提供这种设计。 编写一个简单的配置文件,进行解析,然后找到合适的算法来运行就足够了。 但是,它不能很好地解决可扩展性。
Extending the business logic by adding new analytical algorithms will also require us to map parameters from the configuration file to the new algorithms. It will be necessary to adapt the config reader in order to support the new functionality. So we still have this extra layer that makes our software harder to extend.
通过添加新的分析算法来扩展业务逻辑,还需要我们将参数从配置文件映射到新算法。 为了支持新功能,有必要调整配置读取器。 因此,我们仍然拥有额外的一层,这使得我们的软件难以扩展。
It would be great if we could map parameters from the configuration file to functions in our business logic without having to explicitly implement a one to one (config to code) mapping for each of our (potentially hundreds of) analytical algorithms. Reflections enter the room.
如果我们可以将配置文件中的参数映射到业务逻辑中的功能,而不必为每个(可能数百种)分析算法显式实现一对一(配置到代码)映射,那将是很好的。 反射进入房间。
Thankfully, each of our analytical algorithms has a unique name, and we can identify which one we want to run by specifying their names in the configuration. We tell the program which statistics we would like to see in the final report by typing the names of the associated algorithms into the config file. Now we only need something that takes these names from the config file, finds the functions with the same names in the business logic, and executes them (plus some code to put all results together in a report).
幸运的是,我们的每个分析算法都有一个唯一的名称,我们可以通过在配置中指定它们的名称来确定要运行的算法。 通过在配置文件中键入相关算法的名称,告诉程序我们希望在最终报告中查看哪些统计信息。 现在,我们只需要从配置文件中获取这些名称,在业务逻辑中查找具有相同名称的功能并执行它们(加上一些代码即可将所有结果汇总到报告中)的功能。
The part that finds the functions (given their names) is done by reflections. Reflections allow us to examine the business logic code structure and retrieve the actual function type given the function’s name.
查找功能(给定名称)的部分是通过反射完成的。 通过反射,我们可以检查业务逻辑代码结构,并在给定函数名称的情况下检索实际的函数类型。
This architecture allows us to easily extend out business logic without having to maintain another layer of our application or touch an existing code. The mapping of the configuration parameters to the analytics algorithms is done in a generic manner.
这种体系结构使我们能够轻松扩展业务逻辑,而不必维护应用程序的另一层或触摸现有代码。 配置参数到分析算法的映射以通用方式完成。
It also allows us to reconfigure the behavior of the application through an abstracted interface.
它还允许我们通过抽象的接口重新配置应用程序的行为。
Note that we still need to recompile the whole application each time we extend the application by a new analytical algorithm. Plugin frameworks could help us avoid that, but this is beyond the scope of this article.
请注意,每次使用新的解析算法扩展应用程序时,仍然需要重新编译整个应用程序。 插件框架可以帮助我们避免这种情况,但这超出了本文的范围。
Of course, this architecture doesn’t come without drawbacks. Our reflect layer will just blindly retrieve functions and call them without knowing what these functions require or return. So we will need to implement all our analytical algorithms in a way that they can accept a generic type of input and return a generic type of output.
当然,这种架构并非没有缺点。 我们的反射层将只是盲目地检索函数并调用它们,而不知道这些函数需要什么或返回什么。 因此,我们将需要以一种可以接受通用输入类型并返回通用输出类型的方式实施所有分析算法。
Each algorithm will return a different statistical model, but we need to return those models wrapped up in something the caller (that is, the reflect layer) can deal with. It can then send the wrapped-up model forward to the layer that produces the report which, of course, needs to be able to access the actual statistical model.
每种算法都会返回不同的统计模型,但是我们需要返回包装在调用者(即反射层)可以处理的内容中的那些模型。 然后,它可以将包裹的模型转发到生成报告的层,该报告当然需要能够访问实际的统计模型。
The way this is realized varies from language to language. We will look at (one way) to solve it in Go.
实现的方式因语言而异。 我们将研究(一种方式)在Go中解决它。
按名称查找功能 (Finding Functions by their Names)
The first step of getting from a plain YAML, JSON or XML configuration file to a callable algorithm (after loading the file into the application, of course) is to find a function in the software that matches the function name given in the configuration file.
从纯YAML,JSON或XML配置文件获取可调用算法的第一步(当然,是在将文件加载到应用程序中之后)是在软件中查找与配置文件中指定的函数名称匹配的函数。
Unfortunately, there is not really an intuitive way of retrieving a function residing in a package by its name in Go. We can’t just hand in a function name on one end and retrieve something callable on the other end.
不幸的是,实际上并没有一种直观的方法来通过Go中的名称来检索驻留在包中的函数。 我们不能只在一端输入一个函数名,而在另一端检索可调用的东西。
However, it is possible to find a method of a type by its name.
但是,可以通过名称找到类型的方法 。
A method in Go is a function that has a receiver, whereas a receiver can be any type that has been defined in the same package as the method.
Go中的方法是具有接收器的函数,而接收器可以是与该方法在同一包中定义的任何类型。
type Library struct {
books []Book
}
func (l *Library) GetMostSoldBooks(startYear, endYear int) SoldStat {
...
}
Here we define a type Library
which is based on type struct
. We now take the above defined function GetMostSoldBooks
, pull the library parameter out, and turn it into a receiver type which turns the function into a method of the *Library
type. On the other hand, *Library
describes a pointer to Library
.
在这里,我们定义了一个基于struct
类型的类型Library
。 现在,我们使用上面定义的函数GetMostSoldBooks
,拉出库参数,然后将其转换为接收器类型,该接收器类型会将函数转换为*Library
类型的方法。 另一方面, *Library
描述了指向Library
的指针。
This is practical, not only because Go offers a way to find methods of types by their names, but also because it allows us to tie all statistical algorithms to the *Library
type. We will need an instance of this anyway in all these algorithms, as it contains all the data about the library we want to process in the algorithms.
这是实际的,不仅因为Go提供了一种通过名称查找类型方法的方法,而且还因为它使我们能够将所有统计算法与*Library
类型相关联。 无论如何,我们在所有这些算法中都需要一个实例,因为它包含有关我们要在算法中处理的库的所有数据。
Instead of passing the library as just another parameter to each function, we can use the library as a receiver and — in return — get stronger coupling. In this case, it makes our code cleaner. Each new statistical algorithm that is to be added to the application now must be a method of the *Library
type.
与其将库作为另一个参数传递给每个函数,我们还可以使用该库作为接收器,并获得更强的耦合。 在这种情况下,它使我们的代码更整洁。 现在,要添加到应用程序中的每个新统计算法都必须是*Library
类型的方法。
Now let’s take a look at how we can actually use the reflect package to retrieve the above method if all we’ve got is its name.
现在,让我们看一下如果我们只知道它的名称,我们如何才能实际使用反射包来检索上述方法。
import "reflect"
m := reflect.ValueOf(&Library{}).MethodByName("GetMostSoldBooks")
First we need to take an instance of the receiver type (receiver type being *Library
) and turn it into a reflect.Value
by passing it to reflect.ValueOf()
. On the returned value, we can then call MethodByName()
with the name of the method we want to retrieve.
首先,我们需要接受一个接收器类型的实例(接收器类型为*Library
),并将其传递给reflect.ValueOf()
,将其转换为一个reflect.Value
。 然后,对于返回的值,我们可以使用要检索的方法的名称调用MethodByName()
。
What we get in return is a callable function wrapped in reflect.Value
which will accept exactly the parameters as we defined them in the method definition. Note that upon the call of this function, the instance of *Library
we passed to reflect.ValueOf()
will be used as the receiver type. This means it is important that you already passed the correct instance to the reflect.ValueOf()
function.
作为回报,我们得到了一个包裹在reflect.Value
的可调用函数,该函数将完全接受我们在方法定义中定义的参数。 请注意,在调用此函数时,我们传递给reflect.ValueOf()
的*Library
实例将用作接收器类型。 这意味着重要的是您已经将正确的实例传递给了reflect.ValueOf()
函数。
To make the returned value m
in the above example actually callable, we will need to cast it from reflect.Value
to an actual function type with the correct signatures. This will look as follows:
为了使上面示例中的返回值m
实际可调用,我们需要将其从reflect.Value
转换为具有正确签名的实际函数类型。 如下所示:
mCallable := m.Interface().(func(int, int) SoldStat)
Note how we need to first turn it into an interface type and only then cast it to the function type.
请注意,我们首先需要将其转换为接口类型,然后才将其强制转换为函数类型。
通用制作方法 (Making Methods Generic)
Ok, great. We now have a callable method which we retrieved by passing the function’s name to our application and letting reflections do the rest. But when it comes to calling the actual function, we’ve still got a bit of a problem.
好的太棒了。 现在,我们有了一个可调用方法,该方法通过将函数的名称传递给我们的应用程序并由反射完成其余操作来检索。 但是,在调用实际函数时,我们仍然有一些问题。
We need to know the function’s signature in order to to be able to cast the reflect.Value
returned by MethodByName()
into a callable function. Since we have a lot of different analytical algorithms, chances are that the parameters they accept differ (we definitely don’t want to force a specific function signature onto developers who want to extend the application). This means that the method signatures vary and that we can’t just cast all values returned by the reflection into the exact same function type.
我们需要知道函数的签名,以便能够投射的reflect.Value
返回由MethodByName()
成一个可调用的函数。 由于我们有许多不同的分析算法,因此它们接受的参数可能会有所不同(我们绝对不希望将特定的功能签名强加给想要扩展应用程序的开发人员)。 这意味着方法签名会有所不同,我们不能仅仅将反射返回的所有值都强制转换为完全相同的函数类型。
What we have to do is provide a generic function signature. We can do this by creating a wrapper method.
我们要做的是提供一个通用的函数签名。 我们可以通过创建包装方法来做到这一点。
func (l *Library) GetMostSoldBooksWrap(p GenericParams) Reportable {
return l.GetMostSoldBooks(p.(*MostSoldBooksParams))
}
Here we’ve got a wrapper method GetMostSoldBooksWrap
for the concrete method GetMostSoldBooks
. Like the concrete method, the wrapper is a method of type *Library
. The difference is its signature. It accepts a generic prameter GenericParams
and returns an instance of type Reportable
. In its body, it invokes the concrete analytical method that processes the library data. Also new is the type MostSoldBooksParams
which wraps the parameters for the concrete method.
在这里,我们已经有了一个包装方法GetMostSoldBooksWrap
的具体方法GetMostSoldBooks
。 类似于具体方法,包装器是*Library
类型的方法。 区别在于其签名。 它接受一个通用prameter GenericParams
和返回类型的实例Reportable
。 它本身调用了处理库数据的具体分析方法。 MostSoldBooksParams
类型也是新的,它包装了具体方法的参数。
Now, let’s see where those new types come from.
现在,让我们看看这些新类型的来源。
In order to be able to pass the GenericParams
parameter to the concrete GetMostSoldBooks()
method, the concrete method also needs to only accept one single parameter which we can cast the generic parameter to. We do this by changing the method signature of the concrete function to accept a *MostSoldBooksParams
parameter.
为了能够将GenericParams
参数传递给具体的GetMostSoldBooks()
方法,具体方法还只需要接受一个单一参数即可将通用参数转换为该参数。 我们通过更改具体函数的方法签名来接受*MostSoldBooksParams
参数来实现此目的。
This may first sound as though we are forcing a method signature upon the analytical algorithms after all, therefore contradicting the statement made above. And in some ways that is true. But in some way it isn’t, because MostSoldBooksParams
is of type struct and can therefore contain multiple fields.
这听起来可能首先听起来像是我们毕竟在分析算法上强加方法签名,因此与上面的陈述相矛盾。 在某些方面是正确的。 但是从某种意义上讲并不是这样,因为MostSoldBooksParams
是struct类型的,因此可以包含多个字段。
type MostSoldBooksParams struct {
startYear int
endYear int
}
func (l *Library) GetMostSoldBooks(p *MostSoldBooksParams) SoldStat {
...
}
As you can see, the parameter for the analytical method still incorporates both integer parameters startYear
and endYear
we had defined in the method signature originally. The method also still returns the concrete type SoldStat
.
如您所见,分析方法的参数仍包含我们在方法签名中最初定义的整数参数startYear
和endYear
。 该方法还返回具体类型SoldStat
。
Let’s go back to the wrapper method.
让我们回到包装方法。
As we now need to map the strings from the configuration file to the wrapper methods rather than the concrete methods, we need a wrapper for each analytical algorithm. It needs to be named so that it makes sense to add this name to the config file.
由于现在我们需要将配置文件中的字符串映射到包装方法而不是具体方法,因此,每种分析算法都需要一个包装。 需要命名它,以便将这个名称添加到配置文件中是有意义的。
In this solution here, we name wrappers with <concrete method name&g
t;Wrap. In the config file, we can then just provide the same concrete method’s name and the reflect logic will append “Wrap” to the string before looking for the method.
在这里的解决方案中,我们使用<concrete method name&g
t; Wrap命名包装器。 在配置文件中,我们可以只提供相同的具体方法的名称和反映逻辑将寻找方法实施前的PPEND“包装”的字符串。
The signatures, however, are the exact same for every wrapper function (otherwise they would be futile).
但是,对于每个包装函数,签名是完全相同的(否则,它们将是徒劳的)。
type GenericParams interface {
IsValid() (bool, string)
}
The GenericParam
parameter type is an interface. We declare one method IsValid() (bool, string)
for this interface, meaning that every struct which defines this method automatically implements the GenericParams
interface.
GenericParam
参数类型是一个接口。 我们为此接口声明一个方法IsValid() (bool, string)
,这意味着定义此方法的每个结构都将自动实现GenericParams
接口 。
This is relevant because in our wrapper method, we cast the GenericParams interface to the concrete struct type MostSoldBooksParams
. This only works if MostSoldBooksParams
implements the GenericParams
interface.We therefore now provide a IsValid()
method to our concrete parameter type.
这很重要,因为在包装方法中,我们将GenericParams接口转换为具体的结构类型MostSoldBooksParams
。 这仅在MostSoldBooksParams
实现GenericParams
接口的情况下有效。因此,我们现在为具体参数类型提供IsValid()
方法。
func (p *MostSoldBooksParams) IsValid() (bool, string) {
…
return true, “”
}
The IsValid()
function itself can be used to check the validity of the parameters passed to the concrete analytical method. We can call it at the very beginning of the method.
IsValid()
函数本身可用于检查传递给具体分析方法的参数的有效性。 我们可以在方法的开始处调用它。
func (l *Library) GetMostSoldBooks(p *MostSoldBooksParams) SoldStat
{
if isValid, reason := p.IsValid(); !isValid {
log.Fatalf(“\nParams invalid:: %s”, reason)
}
...
}
Lastly, we have the Reportable
type, which is our generic return value.
最后,我们有Reportable
类型,这是我们的通用返回值。
type Reportable interface {
Report() HTMLStatisticReport
}
Like the generic parameter type, Reportable
is an interface. It declares one method Report()
which will return a statistical report in HTML format.
与通用参数类型一样, Reportable
是一个接口。 它声明了一个方法Report()
,它将以HTML格式返回统计报告。
Since our generic wrapper method directly returns the output of the concrete method, the concrete method’s return type must be of the generic wrapper method’s return type. This means that our SoldStat
type, which is the type returned by the concrete analytical method, must implement the Reportabe
interface.
由于我们的通用包装器方法直接返回具体方法的输出,因此具体方法的返回类型必须是通用包装器方法的返回类型。 这意味着我们的SoldStat
类型(具体的分析方法返回的类型)必须实现Reportabe
接口。
We do this again by writing an implementation of the method declared by the interface.
我们通过编写接口声明的方法的实现来再次执行此操作。
func (p SoldStat) Report() HTMLStatisticReport {
...create report...
}
We will need to implement these methods for all different return types of all statistics algorithms so that the types can be returned by the generic wrappers. While this may appear to introduce a lot of overhead effort, converting the statistical output of each algorithm into a human-readable report is something that needs to be done either way.
我们将需要为所有统计算法的所有不同返回类型实现这些方法,以便通用包装可以返回这些类型。 尽管这似乎会带来很多开销,但是将每种算法的统计输出转换为人类可读的报告都是需要以任何一种方式完成的工作。
Now that we have our generic design, we can go back to the reflections.
现在我们有了通用的设计,我们可以回到反思。
m := reflect.ValueOf(library).MethodByName("GetMostSoldBooksWrap")
mCallable = m.Interface().(func(GenericParams) Reportable)
These two lines of reflection can now be used to retrieve any analytical method wrapper by its name, whereby mCallable
will be the callable wrapper method.
这两条反射线现在可用于按名称检索任何分析方法包装器,其中mCallable
将是可调用包装器方法。
传递参数 (Passing Parameters)
What’s missing are the method parameters. These will need to be parsed from the config file just like the method name and then passed to the wrapper method we retrieved through the reflection. This is where things become a bit convoluted.
缺少的是方法参数。 这些将需要像方法名称一样从配置文件中进行解析,然后传递给我们通过反射检索的包装方法。 在这里事情变得有些混乱。
statistics:
— statsMethodName: GetMostSoldBooks
startYear: 1984
endYear: 2018
The above shows an example configuration file in YAML format. We have a root element statistics
which maps to a list. Each element in the list is an analytical algorithm we want to run and include its output in the report. The elements consist of a key statsMethodName
, with the name of the analytical method as the value, and one key per parameter with their corresponding values. Note that the names of the parameters must match the names of the fields in the parameter struct declared for the associated method. In this case here, the parameter struct is the one we declared before, namely MostSoldBooksParams
, with fields startYear
and endYear
, both of which are of type integer.
上面显示了YAML格式的示例配置文件。 我们有一个根元素statistics
,它映射到一个列表。 列表中的每个元素都是我们要运行的分析算法,并将其输出包括在报告中。 元素由一个键statsMethodName
(以分析方法的名称作为值)和每个参数一个键及其对应的值组成。 请注意,参数名称必须与为关联方法声明的参数struct中的字段名称匹配。 在这种情况下,参数struct是我们之前声明的参数,即MostSoldBooksParams
,其字段startYear
和endYear
均为整数类型。
What we need to add to our reflection now is the mapping of the strings (and other value types) defining the parameters, from the configuration file to the method parameter struct’s fields.
现在我们需要添加到反射中的是定义参数的字符串(和其他值类型)的映射,从配置文件到方法参数struct的字段。
Since the concrete method parameter struct is in the concrete method’s signature but not in the signature of the wrapper method, we will need to retrieve the concrete method through the reflection logic in addition to the wrapper method.
由于具体方法参数struct是在具体方法的签名中而不是在包装方法的签名中,因此除了包装方法之外,我们还需要通过反射逻辑来检索具体方法。
methodName := "GetMostSoldBooks" // taken from configuration file
conreteMethod := reflect.ValueOf(library).MethodByName(methodName)
wrapperName := fmt.Sprintf("%sWrap", methodName)
wrapperMethod := reflect.ValueOf(library).MethodByName(wrapperName)
Next we need to access the parameter type passed to the concrete method.
接下来,我们需要访问传递给具体方法的参数类型。
concreteMethodParamsType := conreteMethod.Type().In(0).Elem()
concreteMethodParamsType
will now hold the type of the method parameter struct. For the case of the GetMostSoldBooks
this is MostSoldBooksParams
.
concreteMethodParamsType
现在将保存方法参数struct的类型。 对于GetMostSoldBooks
这是MostSoldBooksParams
。
In order to be able to retrieve the struct fields (which represent the parameters needed by the analytical algorithm) by their names (which are given in the configuration file), we need to create an instance of the method parameter struct type. We need both a pointer to the instance as well as the instance itself (as will be seen later).
为了能够通过名称(在配置文件中给出)的结构字段(表示解析算法所需的参数)检索结构字段,我们需要创建方法参数结构类型的实例。 我们既需要指向实例的指针,也需要指向实例本身的指针(将在后面看到)。
concreteMethodParamsPtr := reflect.New(concreteMethodParamsType)
concreteMethodParams := concreteMethodParamsPtr.Elem()
At this stage, you can iterate over the keys of the stats element from the configuration file and map the parameter types one-by-one to the fields in the parameter (that is, retrieving the fields of the method parameter struct according to their names). To retrieve a field of a struct by its name, we can use reflect.FieldByName()
.
在此阶段,您可以从配置文件中遍历stats元素的键,并将参数类型一对一地映射到参数中的字段(即,根据它们的名称检索方法参数struct的字段) )。 要按其名称检索结构的字段,我们可以使用reflect.FieldByName()
。
parameterField := concreteMethodParams.FieldByName(configParam)
Once we have the parameter fields retrieved, we can map the value given for this parameter in the configuration file to the actual field.
一旦检索到参数字段,就可以将配置文件中为此参数给定的值映射到实际字段。
if configValueInt, isInt := configValue.(int); isInt {
parameterField.SetInt(int64(configValueInt)
)
The above is for the case of integer values, but we could do the same for each value type we expect (in a trial and error fashion) for each parameter given in the configuration file. Setting the value on the parameter field here will also directly affect the method parameters struct, so we don’t need to explicitly alter concreteMethodParams
in order to store the parameter value retrieved from the configuration file.
上面是针对整数值的情况,但是我们可以对配置文件中给定的每个参数(以反复试验的方式)期望的每种值类型都执行相同的操作。 在此处在参数字段上设置值还将直接影响方法参数的结构,因此我们无需显式更改concreteMethodParams
即可存储从配置文件中检索到的参数值。
Lastly, just as we did with the wrapper method, we will cast the concreteMethodParams
struct to a GenericParams
type. Note that we need to use the pointer type there.
最后,就像对wrapper方法所做的那样,我们将concreteMethodParams
结构转换为GenericParams
类型。 请注意,我们需要在此处使用指针类型。
wrapperParams := concreteMethodParamsPtr.Interface().(GenericParams)
全部放在一起 (Putting it all together)
Once we have our wrapper method and our generic method parameter, we can call the wrapper as follows.
一旦有了包装器方法和通用方法参数,就可以按以下方式调用包装器。
wrapperMethod(wrapperParams)
As you can see, that’s just a normal function call. It does exactly the same as calling the wrapper would do without going through the reflection process.
如您所见,这只是一个普通的函数调用。 它的功能与调用包装程序完全相同,而无需经过反射过程。
Finally, you will just need a function that calls Report()
on all of the return values from the invoked analytical method’s wrapper functions and puts the reports of each statistic into one coherent report file.
最后,您只需要一个函数,就调用的分析方法的包装函数中的所有返回值调用Report()
,并将每个统计信息的报告放入一个一致的报告文件中。
Now the question that you should ask is: Is this good code?
现在您应该问的问题是: 这是好的代码吗?
My answer: I don’t know.
我的回答: 我不知道。
What I can tell you is that it is an option worth exploring. Especially if you end up in a situation where you are in the need of a software design with similar requirements as the ones in this example — even if it’s just for the purpose of learning about reflections.
我可以告诉你的是,这是一个值得探索的选择。 尤其是当您最终遇到需要与本示例中的需求具有相似要求的软件设计时,即使这只是出于学习反射的目的。
If you want to see the full code of an application using this design, check out: https://github.com/Demonware/harbor-analytics
如果您想使用此设计查看应用程序的完整代码,请查看: https : //github.com/Demonware/harbor-analytics
java反射用法示例