`
xyheqhd888
  • 浏览: 409203 次
  • 性别: Icon_minigender_1
  • 来自: 秦皇岛
社区版块
存档分类
最新评论

Interpreter(解释器)模式

阅读更多

   

       与Command模式一样,Interpreter模式也会产生一个可执行的对象.差别在于Interpreter模式会创建一个类层次,其中每个类会实现或者解释一个公共操作,这个操作与类的名称相匹配.在这方面,Interpreter模式类似于State模式和Strategy模式.在Interpreter模式、State模式以及Strategy模式中,一个公共操作遍布类集合,每个类都以不同的方式实现这个操作。

     Interpreter模式也类似于Composite模式。Composite模式通常会为单个对象和群组对象定义一个公共接口。不过,Composite模式并不要求支持以不同方式组织的结构,尽管该模式可以支持这些结构。例如,介绍Composite模式时曾描述过ProcessComponent类层次结构,该结构允许生产流程顺序进行,也允许生产流程交替进行。Interpreter模式通常都会涉及不同类型的组合结构(Interpreter模式通常处于Composite模式结构之上)。一个类组成其他组件的方式定义了解释器类实现或解释一个操作的方式。

 

  Interpreter模式的主要意图是可以按照自己定义的组合规则集合来组合可执行对象

 

1.Interpreter模式范例

   Oozinoz公司的机器人可以沿着生产线移动原材料。Oozinoz公司开发人员提供一个解释器,用于控制这些机器人;另外,该解释器对生产线上的机器具有一定的控制能力。你也许认为解释器是编程语言,实际上解释器允许对指令进行组合。Oozinoz机器人解释器实际上就是一个类层次结构,它封装了控制机器人的命令。该类层次结构的顶部是一个抽象的Command类。execute()方法遍布该类层次结构中。图1显示了Robot类以及机器人解释器支持的两个命令。


解释器类层次结构提供一种在运行时对工厂机器人进行编程的能力

 第一眼看该图时,发现类层次的最高层是Command类,你也许会建议使用Command模式。但是Command模式的目的是在对象中封装方法。图1的Command类层次结构不是这样做的,这个类层次结构要求Command子类重新解释execute()操作的含义。这就是Interpreter模式的目的:允许你组合复杂的对象。

 

  典型的解释器类层次包含至少两个子类,我们将很快扩展Command类层次.图1的两个类足够实现本书的范例要求,代码如下所示:

 

package app.interpreter;
 import com.oozinoz.machine.*:
 import com.oozinoz.robotInterpreter.*; 
public class ShowInterpreter {
 public static void main(String[] args){
 MachineComposite dublin = OozinozFactory.dublin();
 ShellAssembler assembler = (ShellAssembler)dublin.find("ShellAssembler:3302"); 
StarPress press = (StarPress)dublin.find("StarPress:3404");
 Fuser user = (Fuser)dublin.find("Fuser:"); 
assembler.load(new Bin(11011)); 
press.load(new Bin(11015));
 CarryCommand carry1 = new CarryCommand(assembler,fuser); 
 CarryCommand carry2 = new CarryCommand(press,fuser);
 CommandSequence seq = new CommandSequence();
 seq.add(carry1); seq.add(carry2);
 seq.execute(); 
} 
}

 

 

上述演示代码让工厂机器人把两箱原材料从操作机器转移到一个倾销的缓冲池中,这需要使用OozinozFactory类的dublin()方法返回的机器组合对象.这个数据模型表示位于爱尔兰都柏林的新Oozinoz工厂.上述代码定位这个工厂中的三台机器,把原材料箱放在其中两台机器上面,组合Command类层次结构提供的命令.程序的最后一条语句CommandSequence对象的execute()方法,导致机器人按照seq命令中的指令开始操作.

  CommandSequence对象解释execute()操作,把整个方法调用转发给每个子命令:

 

package com.oozinoz.robotInterpreter; 
import java.util.ArrayList; 
import java.util.List; 
public class CommandSequence extends Command { 
protected List commands = new ArrayList(); 
public void add(Command c){ 
commands.add(c); 
}
public void execute(){ 
for(int i=0;i<commands.size();i++){ 
Command c = (Command)commands.get(i); c.execute(); 
} 
}
}

 

 

CarryCommand类完成execute()操作的过程如下 :与工厂机器人进行交互,把材料箱从一个机器移到其他机器.

 

package com.oozinoz.robotInterpreter;
 import com.oozinoz.machine.Machine;
 public class CarryCommand extends Command { 
protected Machine fromMachine; 
protected Machine toMachine; 
public CarryCommand( Machine fromMachine,Machine toMachine){
 this.fromMachine = fromMachine;
 this.toMachine = toMachine; 
} 
public void execute(){
 Robot.singleton.carry(fromMachine,toMachine);
 } 
}

 

 

CarryCOmmand类专用于机器人控制工厂生产线的领域环境下.我们很容易想像其他领域相关的类,诸如控制机器的StartUpCommand和ShutdownCommand类.创建操作多个特定机器的Forcommand类也是有用的.图2给出对Command类层次的扩展.


Interpreter模式允许多个子类重新解释公共操作的含义

 

ForCommand类的部分设计思路是非常显而易见的.推测起来,该类的构造器接收机器集合和命令对象,以便作为for循环体处理.这种设计更难的部分是如何把方法体和循环连接起来.Java5对for语句进行了扩展,建立了一个新变量,每次循环体执行一次,该变量就接收一个新值.我们将模拟这种处理方式.请参考如下语句:

 

 

 

for(Command c:commands) c.execute();

 

  Java把for语句声明中的c标识符与循环体中的c变量链接起来.为创建模拟这种处理方式的解释器类,我们需要处理和计算变量的方式.图3的Term类层次结构说明了处理方式.


提供变量对机器进行处理的Term类层次结构

 

 Term类层次结构类似于Command类层次结构的地方在于类层次中到处存在的公共操作(eval()).你也许认为这个类层次本身是Interpreter模式的范例,尽管它并没有提供通常与Interpreter模式一起使用的组合类,诸如CommandSequence.

  Term类层次结构允许我们把每个机器命名为常量,可以给这些常量或者其他变量分配变量.也允许我们让域相关的解释器类更加灵活.比如,StartUpCommand代码可以与Term对象一起使用,而不是只能应用于特定机器.

package com.oozinoz.robotInterpreter2; 
import com.oozinoz.machine.Machine; 
public class StartUpCommand extends Command { 
protected Term term;
 public StartUpCommand(Term term){ 
this.term = term; 
} 
public void execute(){
 Machine m = term.eval();
 m.startup();
 } 
}
    

  

同样,为了增强CarrryCommand类的灵活性,我们可以修改该类,以便于使用Term对象,修改代码如下:

package com.oozinoz.robotInterpreter2;
 public class CarryCommand extends Command {
 protected Term from; 
protected Term to;
 public CarryCommand(Term fromTerm,Term toTerm){ 
from = fromTerm; 
to = toTerm;
 }
 public void execute(){ 
Robot.singleton.carry(from.eval(),to.eval()); 
} 
}

 

一旦Command类层次可以操作Term对象,就可以修改ForCommand类,使之设置变量的值,并在循环中执行方法体命令.

package com.oozinoz.robotInterpreter2; 
import java.util.List;
 import com.oozinoz.machine.Machine;
 import com.oozinoz.machine.MachineComponent; 
import com.oozinoz.machine.MachineComposite; 
public class ForCommand extends Command { 
protected MachineComponent root; 
protected Variable variable; 
protected Command body;

public ForCommand(MachineComponent mc,Variable v,Command body){
 this.root = mc; 
this.variable = variable;
 this.body = body; 
} 
public void execute(){ 
execute(root); 
} 

public void execute(MachineComponent mc){ 
if(mc instanceof Machine){
 Machine m = (Machine)mc; 
variable.assign(new Constant(m)); 
body.execute(); return; 
} 
MachineComposite comp = (MachineComposite)mc;
 List children = comp.getComponents();
 for(int i = 0;i<children.size();i++){ 
MachineComponent child = (MachineComponent)children.get(i); 
execute(child); 
} 
} 
}

 

 ForCommand类中的execute()代码使用投射来查看机器组件树.28章将介绍一种更好的技术来对组合结构进行迭代处理.对于Interpreter模式,最重要的一点是为树中每个节点正确解释execute()请求.

  借助于ForCommand类,我们可以开始为工厂组合命令的"程序"或者"脚本".比如,下面程序组合一个解释器对象,可以关闭一个工厂中的所有机器.

 

package app.interpreter; 
import com.oozinoz.machine.*;
import com.oozinoz.robotInterpreter2.*;
 class ShowDown {
 public static void main(String[] args){ 
MachineComposite dublin = OozinozFactory.dublin(); 
Variable v = new Variable("machine"); 
Command c = new ForCommand(dublin,v,new ShutDownCommand(v)); 
c.execute(); 
} 
}

 

当这个程序调用execute()方法时,ForCommand对象c会解释execute()方法,方式为:遍历所提供的机器组件,并且为每个机器完成如下操作:

(1)设置变量v的值;

(2)调用已提供ShutDownCommand对象的execute()操作.

如果我们添加控制处理逻辑请求的类,诸如IfCommand类和WhileCommand类,可以得到一个更加完善的解释器.这些类需要某种方式来模拟Boolean条件.比如,我们也许需要某种方式来模拟机器变量是否等于特定机器的情况.我们也许会引入新的Term类层次结构,但是如果借用C语言的处理思想会更加简单:让null代表假,其余的值代表真.借助于这个思路,我们可以扩展Term类层次结构.如下图所示:

 


Term类层次结构包含模拟Boolean条件的类

 

Equals类比较两个条件,并返回null来代表假.更合理的设计思路是如果两个条件相等,Equals类的eval()方法返回其中一个条件,如下所示:

package com.oozinoz.robotInterpreter2;
 import com.oozinoz.machine.Machine; 
public class Equals extends Term { 
protected Term term1; 
protected Term term2; 
public Equals(Term term1,Term term2){
 this.term1 = term1; 
this.term2 = term2;
 } 

public Machine eval(){ 
Machine m1 = term1.eval();
 Machine m2 = term2.eval(); 
return m1.equals(m2)?m1:null; 
} 
} 

 

HasMaterial类把Boolean类中值的概念扩展到特定领域的例子,诸如如下代码:

 

package com.oozinoz.robotInterpreter2; 
import com.oozinoz.machine.Machine; 
public class HasMaterial extends Term {
 protected Term term;
 public HasMaterial(Term term){ 
 this.term = term; 
}

 public Machine eval(){ 
Machine m = term.eval();
 return m.hasMaterial()?m:null;
 } 
} 

 

现在我们已经把Boolean条件的概念添加到解释器包,接下来可以添加流程控制类,如图5所示.

通过往解释器类层次结构添加流程控制逻辑,解释器功能更加丰富

当我们需要一个不做任何事情的命令时,NullCommand类很有用处,诸如当if命令的else分支是空时:

package com.oozinoz.robotInterpreter2; 
public class NullCommand extends Command {
 public void execute(){ } 
} 

package com.oozinoz.robotInterpreter2; 
public class IfCommand extends Command { 
protected Term term;
 protected Command body;
 protected Command elseBody;
 public IfCommand( Term term,Command body,Command elseBody){ 
this.term = term; 
this.body = body; 
this.elseBody = elseBody; } 
public void execute(){ 
if(term.eval() != null) body.execute(); 
else elseBody.execute(); 
} 
} 

  

突破题:请完成IfCommand类中execute()方法的代码(上面已经写出).

突破题:请完成WhileCommand类的代码.

package com.oozinoz.robotInterpreter2;
 public class WhileCommand extends Command {
 protected Term term; protected Command body; 
public WhileCommand(Term term,Command body){ 
this.term = term;
 this.body = body; 
} 
public void execute(){ 
while(term.eval()!= null) body.execute(); 
} 
} 

 

你也许把WhileCommand类与能够卸载火药球填压机的解释器一起使用,代码如下:

package app.interpreter; 
import com.oozinoz.machine.*;
 import com.oozinoz.robotInterpter2.*; 
public class ShowWhile { 
public static void main(String[] args){
 MachineComposite dublin = OozinozFactory.dubline(); 
Term starPress = new Constant( (Machine) dublin.find("StarPress:1401")); 
Term fuser = new Constant( (Machine) dublin.find("Fuser:1101")); 
starPress.eval().load(new Bin(77)); 
starPress.eval().load(new Bin(88)); 
whileCommand command = new WhileCommand(
 new HasMaterial(starPress),new CarryCommand(starPress,fuser)); 
command.execute();
 } 
} 

Command对象是个解释器,可以解释execute()方法,从火药球填压机1401卸载所有火药箱.

突破题:解释Command模式和Interpreter模式之间的区别.答:Interpreter模式的意图在于使开发人员可以组合可执行对象,这些对象来自于对某公共操作提供各种解释的类层次结构.命令模式的意图仅仅是在一个对象中请求.

 解释器对象能否作为命令使用?当然可以.具体应该使用哪种设计模式,这取决于你的意图.比如,你是期望创建组合可执行对象的工具集?还是期望将请求封装在对象中?总之,不同的需求要求使用不同的设计模式.

  如想增加其他应用领域相关任务的控制,我们可以往解释器类层次结构添加更多功能.也可以扩展Term类层次结构.比如让Term子类发现靠近其他机器的卸载缓冲池.

 Command和Term类层次结构的用户可以随意组合丰富而复杂的执行"程序".比如,创建下面一个对象并不难,当这个对象执行时,将卸载除工厂中卸载缓冲池之外的来自于所有机器的原材料.我们可以使用伪代码来描述这个程序,如下所示:

 

for(m in factory) 
   if (not (m is unloadBuffer)) 
         ub = findUnload for m 
               while( m hasMaterial) carry(m,ub) 

 

如果要编写Java代码来完成这些任务,相对于伪代码而言,Java代码会更加庞大和复杂.所以为什么不使用伪代码,通过创建获取领域语言的解析器来管理工厂中的原材料,并创建对应的解释器对象呢?

 2.解释器、语言和解析器: 

 Interpreter模式意在强调解释器的工作方式,但是它并没有指定如何在运行时组合或者实例化它们。在本章,我们通过编写Java代码来“手工”生成新的解释器。但是创建一个新的解释器最常见的方法就是使用解析器。解析器对象可以按照预定规则识别文本和分解文本结构,以便于进一步处理。比如,你可以编写一个解析器,用它创建一个对应到早期伪代码形式文本程序的机器命令解释器对象。  用Java语言编写的解析器工具很少,讨论这个主题的书籍也非常少。可以使用关键字“Java 解析器工具"来搜索网络资源。大多数解析器都包含一个解析器生成器。为了使用这个生成器,可以使用专门的语法来描述这个模式,工具会根据描述产生解析器。所产生的解析器会识别出你的语言实例。有时,不必使用解析器生成工具,可以通过应用Interpreter模式来编写通用目的的解析器。Builder Parser with Java[Metsker,2001]使用Java语言解释了这种技术。

 3. 小结

 Interpreter模式使我们可以根据创建的类层次结构来组合可执行对象。该类层次结构中的每个类分别实现一个公共操作,诸如execute()。尽管前面没有讨论过,但这个方法需要一个“上下文”对象,用来保存重要的状态信息。

 每个类的名称通常意味着这个类实现这个公共操作的方式。每个类或者定义组合命令的方式,或者导致某种动作的终端命令。

 解释器通常伴随着引入变量的设计,以及Boolean或算术表达式。解释器也经常使用解析器来简化新的解释器对象的创建过程。

  • 大小: 7.3 KB
  • 大小: 3.8 KB
  • 大小: 5.6 KB
  • 大小: 4.5 KB
  • 大小: 4.4 KB
分享到:
评论
2 楼 cctt_1 2014-10-27  
不要误人子弟,那根本就不是 解释器模式!!!
那是Composite模式!!!
1 楼 悲剧了 2011-11-30  
这不是那个设计模式 java手册上面的文章

相关推荐

    VB控制计算机并口示例(含完整可以运行源代码)

    VB控制计算机并口示例(含完整可以运行源代码) 可以通过并口直接控制MCU,做SW控制不错,关键还可以学习并口硬件控制学习。含详细源代码哦

    python毕业设计基于PyTorch的手语识别系统源码+数据集(完整项目代码)

    python毕业设计基于PyTorch的手语识别系统源码+数据集(完整项目代码),本资源中的源码都是经过本地编译过可运行的,评审分达到98分,资源项目的难度比较适中,内容都是经过助教老师审定过的能够满足学习、毕业设计、期末大作业和课程设计使用需求,如果有需要的话可以放心下载使用。 python毕业设计基于PyTorch的手语识别系统源码+数据集(完整项目代码)python毕业设计基于PyTorch的手语识别系统源码+数据集(完整项目代码)python毕业设计基于PyTorch的手语识别系统源码+数据集(完整项目代码)python毕业设计基于PyTorch的手语识别系统源码+数据集(完整项目代码)python毕业设计基于PyTorch的手语识别系统源码+数据集(完整项目代码)python毕业设计基于PyTorch的手语识别系统源码+数据集(完整项目代码)python毕业设计基于PyTorch的手语识别系统源码+数据集(完整项目代码)python毕业设计基于PyTorch的手语识别系统源码+数据集(完整项目代码)python毕业设计基于PyTorch的手语识别系统源码+数据集(完整项目代

    基于Unet的树种分别识别模型

    基于Unet的树种分别识别模型

    精选毕设项目-富文本解析,折线图,MD5,bluebird.zip

    精选毕设项目-富文本解析,折线图,MD5,bluebird

    图书管理系统(基于ASP .NET)

    《图书管理系统(基于ASP .NET)》是一款专为学习者设计的应用程序,旨在提供一个全面的图书管理平台。系统的设计采用ASP .NET技术,这是一款由微软开发的用于构建动态网站、web应用和web服务的强大工具。ASP .NET框架以其高效、安全和易于维护的特点,深受开发者的喜爱。 该系统包含了多个核心模块,这些模块覆盖了图书管理的主要功能。有图书录入模块,它允许管理员录入图书的基本信息,如书名、作者、出版社、ISBN号、分类等。图书查询模块提供给用户方便快捷的搜索功能,用户可以根据书名、作者、关键词等条件进行检索。此外,借阅与归还模块确保图书的流通管理,记录图书的借阅状态,提醒用户按时归还,并处理超期罚款等事务。 系统还具备用户管理模块,允许用户注册、登录、修改个人信息。对于权限管理,后台有专门的管理员角色,他们可以对用户进行操作,如分配权限、冻结或解冻账户。同时,系统的统计分析模块能够生成各类报表,如图书借阅量、热门书籍、用户活跃度等,这些数据对于图书馆运营决策有着重要参考价值。 在。内容来源于网络分享,如有侵权请联系我删除。另外如果没有积分的同学需要下载,请私信我。

    精选毕设项目-查拼音.zip

    精选毕设项目-查拼音

    精选毕设项目-音乐在线歌词搜索.zip

    精选毕设项目-音乐在线歌词搜索

    思维导图制作-会计初级知识重难点-会计务实-所有者权益

    本专刊的主要目的是帮助初学者系统化和结构化地掌握会计知识。我们采用思维导图的形式,将复杂的会计概念和流程进行有效的简化,旨在让学习者能够更清晰地理解这些内容,并增强记忆效果。通过视觉化的方式,读者不仅能够感受到会计知识的关联性,还能轻松掌握关键点,提升学习效率。无论是在学习新知识还是复习旧知识时,这种方法都能够为学习者提供极大的便利和帮助。

    配网两阶段鲁棒优化调度模型 关键词:两阶段鲁棒优化,CCG算法,储能 仿真算例采用33节点,采用matlab+yalmip+cplex编写,两阶段模型采用CCG算法求解 模型中一阶段变量主要包括01

    配网两阶段鲁棒优化调度模型 关键词:两阶段鲁棒优化,CCG算法,储能 仿真算例采用33节点,采用matlab+yalmip+cplex编写,两阶段模型采用CCG算法求解。 模型中一阶段变量主要包括01变量和无功优化变量,核心变量主要存在于二阶段,因此在叠加二阶段变量优化过程中更容易得到最优解,所以有限次迭代即得到收敛的结果。 模型以网损为目标,包括功率平衡、网络潮流、电压电流、蓄电池出力以及无功设备出力等约束。 复现《两阶段鲁棒优化的主动配电网动态无功优化》-熊壮壮,具体内容可自行下载了解。

    1..1行列式的定义.ppt

    1..1行列式的定义.ppt

    精选毕设项目-地图定位.zip

    精选毕设项目-地图定位

    MMC整流器平均值模型simulink仿真,19电平,采用交流电流内环,直流电压外环控制,双二阶广义积分器锁相环,PI解耦环流抑制器,调制方式为最近电平逼近调制,完美运行 波形一二为直流侧电压电流

    MMC整流器平均值模型simulink仿真,19电平,采用交流电流内环,直流电压外环控制,双二阶广义积分器锁相环,PI解耦环流抑制器,调制方式为最近电平逼近调制,完美运行。 波形一二为直流侧电压电流,波形三四分别为主控制器及环流抑制器输出调制信号。

    疫苗发布和接种预约系统-springboot毕业项目,适合计算机毕-设、实训项目、大作业学习.zip

    Spring Boot是Spring框架的一个模块,它简化了基于Spring应用程序的创建和部署过程。Spring Boot提供了快速启动Spring应用程序的能力,通过自动配置、微服务支持和独立运行的特性,使得开发者能够专注于业务逻辑,而不是配置细节。Spring Boot的核心思想是约定优于配置,它通过自动配置机制,根据项目中添加的依赖自动配置Spring应用。这大大减少了配置文件的编写,提高了开发效率。Spring Boot还支持嵌入式服务器,如Tomcat、Jetty和Undertow,使得开发者无需部署WAR文件到外部服务器即可运行Spring应用。 Java是一种广泛使用的高级编程语言,由Sun Microsystems公司(现为Oracle公司的一部分)在1995年首次发布。Java以其“编写一次,到处运行”(WORA)的特性而闻名,这一特性得益于Java虚拟机(JVM)的使用,它允许Java程序在任何安装了相应JVM的平台上运行,而无需重新编译。Java语言设计之初就是为了跨平台,同时具备面向对象、并发、安全和健壮性等特点。 Java语言广泛应用于企业级应用、移动应用、桌面应用、游戏开发、云计算和物联网等领域。它的语法结构清晰,易于学习和使用,同时提供了丰富的API库,支持多种编程范式,包括面向对象、命令式、函数式和并发编程。Java的强类型系统和自动内存管理减少了程序错误和内存泄漏的风险。随着Java的不断更新和发展,它已经成为一个成熟的生态系统,拥有庞大的开发者社区和持续的技术创新。Java 8引入了Lambda表达式,进一步简化了并发编程和函数式编程的实现。Java 9及以后的版本继续在模块化、性能和安全性方面进行改进,确保Java语言能够适应不断变化的技术需求和市场趋势。 MySQL是一个关系型数据库管理系统(RDBMS),它基于结构化查询语言(SQL)来管理和存储数据。MySQL由瑞典MySQL AB公司开发,并于2008年被Sun Microsystems收购,随后在2010年,Oracle公司收购了Sun Microsystems,从而获得了MySQL的所有权。MySQL以其高性能、可靠性和易用性而闻名,它提供了多种特性来满足不同规模应用程序的需求。作为一个开源解决方案,MySQL拥有一个活跃的社区,不断为其发展和改进做出贡献。它的多线程功能允许同时处理多个查询,而其优化器则可以高效地执行复杂的查询操作。 随着互联网和Web应用的快速发展,MySQL已成为许多开发者和公司的首选数据库之一。它的可扩展性和灵活性使其能够处理从小规模应用到大规模企业级应用的各种需求。通过各种存储引擎,MySQL能够适应不同的数据存储和检索需求,从而为用户提供了高度的定制性和性能优化的可能性。

    jQuery实现左右切换全屏轮播图特效源码.zip

    这是一种全屏轮播风格的特效,使用HTML、CSS和Javript编写。轮播图包含多张图片和对应的文本介绍,通过自动滑动和手动切换两种方式,展示出不同的内容。该轮播图在网页头部或者特定板块上使用,能够为用户提供直观的视觉体验和丰富的内容呈现。而且,该轮播图可以灵活地设置大小、位置、动画等属性,便于根据实际需求进行个性化定制。

    精选毕设项目-图片预览带后端.zip

    精选毕设项目-图片预览带后端

    精选毕设项目-番茄时钟.zip

    精选毕设项目-番茄时钟

    精选毕设项目-简单的商城小应用.zip

    精选毕设项目-简单的商城小应用

    精选毕设项目-仿zcool站酷.zip

    精选毕设项目-仿zcool站酷

    精选毕设项目-录音机.zip

    精选毕设项目-录音机

    南京理工大学毕业论文overleaf LaTex模板,微调版

    南京理工大学毕业论文overleaf LaTex模板,按照我个人的写作需求修改后的版本

Global site tag (gtag.js) - Google Analytics