设计模式_访问者模式_Visitor

案例引入

要求

测评系统需求:将观众分为男人和女人,对歌手进行测评,当看完某个歌手表演后,得到他们对该歌手不同的评价(比如 成功、失败 等)

传统方案

在这里插入图片描述

Man和Woman里面都有“成功”、“失败”的方法

【分析】

  • 如果系统比较小,这样设置是可以的,但是考虑系统增加越来越多新的功能时,对代码改动较大(如需要增加一个新的评价方式,就需要在Man和Woman类中同时添加),违反了ocp原则,不利于维护
  • 扩展性不好,比如增加了新的人员类型,或者增加新的评价,都需要修改很多代码

【改进】

使用访问者模式

介绍

基本介绍

  • 在数据结构中保存着许多元素,我们会对这些元素进行“处理”。这时,“处理”代码放在哪里比较好呢?通常的做法是将它们放在表示数据结构的类中。但是,如果“处理”有许多种呢?这种情况下,每当增加一种处理,我们就不得不去修改表示数据结构的类。在Visitor模式中,数据结构与处理被分离开来。我们编写一个表示“访问者”的类来访问数据结构中的元素,并把对各元素的处理交给访问者类。这样,当需要增加新的处理时,我们只需要编写新的访问者,然后让数据结构可以接受访问者的访问即可
  • 即访问者模式主要将数据结构数据操作分离,解决数据结构和操作耦合性问题
  • 访问者模式的基本工作原理是: 在被访问的类里面提供一个对外接待访问者的接口

应用场景

  • 需要对一个对象结构中的对象进行很多不同操作(而且这些操作彼此没有关联),需要避免让这些操作"污染"这些对象的类,可以选用访问者模式解决

登场角色

在这里插入图片描述

  • Visitor :是抽象访问者,为该对象结构中的ConcreteElement的每一个类声明一个visit方法
  • ConcreteVisitor:是一个具体的访问者,实现Visitor声明的每个方法
  • ObjectStructure:能枚举它的元素,可以提供一个高层的接口,用来允许访问者访问元素(比如案例一的ObjectStructure类的display方法)
  • Element:定义一个accept 方法,接收一个访问者对象
  • ConcreteElement:为具体元素,实现了accept 方法
尚硅谷版本
《图解设计模式》版本

在这里插入图片描述

  • Visitor(访问者):Visitor角色负责对数据结构中每个具体的元素(ConcreteElement角色)声明一个用于访问XXXXX的visit(XXXXX)方法。visit(XXXXX)是用于处理XXXXX的方法负责实现该方法的是ConcreteVisitor角色
  • ConcreteVisitor(具体的访问者):ConcreteVisitor角色负责实现 Visitor角色所定义的接口(API)。它要实现所有的visit(XXXXX)方法,即实现如何处理每个ConcreteElement角色
  • Element(元素):Element角色表示Visitor角色的访问对象。它声明了接受访问者的accept方法。accept 方法接收到的参数是Visitor角色
  • ConcreteElement(具体元素):ConcreteElement角色负责实现Element角色所定义的接口(API)
  • ObjectStructure(对象数据结构):ObjectStructur角色负责处理Element角色的集合,能够枚举它的元素(案例二的Directory类同时扮演该角色和ConcreteElement角色)

案例实现

案例一

在这里插入图片描述

实现

【Action(Visitor)】

package com.test.visitor;public abstract class Action {/**
* 得到男性 的测评
* @param man
*/
public abstract void getManResult(Man man);/**
* 得到女性 的测评
* @param woman
*/
public abstract void getWomanResult(Woman woman);
}

【Success(ConcreteVisitor)】

package com.test.visitor;public class Success extends Action {@Override
public void getManResult(Man man) {
System.out.println(" 男人给的评价该歌手很成功 !");
}@Override
public void getWomanResult(Woman woman) {
System.out.println(" 女人给的评价该歌手很成功 !");
}}

【Fail(ConcreteVisitor)】

package com.test.visitor;public class Fail extends Action {@Override
public void getManResult(Man man) {
System.out.println(" 男人给的评价该歌手失败 !");
}@Override
public void getWomanResult(Woman woman) {
System.out.println(" 女人给的评价该歌手失败 !");
}}

【Person(Element)】

package com.test.visitor;public abstract class Person {/**
* 提供一个方法,让访问者可以访问
*
* @param action
*/
public abstract void accept(Action action);
}

【Woman(ConcreteElement )】

package com.test.visitor;/**
* 这里我们使用到了双分派, 即首先在客户端程序中,将具体状态作为参数传递到Woman中(第一次分派)
* 然后 Woman 类调用作为参数的 "具体方法" 中方法getWomanResult, 同时将自己(this)作为参数传入,完成第二次的分派
* 即互为对方方法的参数
*/
public class Woman extends Person{@Override
public void accept(Action action) {
action.getWomanResult(this);
}}

【Man(ConcreteElement )】

package com.test.visitor;public class Man extends Person {@Override
public void accept(Action action) {
// 自己认为是什么结果就是什么结果
action.getManResult(this);
}}

【ObjectStructure】

package com.test.visitor;import java.util.LinkedList;
import java.util.List;/**
* 数据结构,管理很多人(Man , Woman)
*/
public class ObjectStructure {/**
* 维护了一个集合
*/
private List<Person> persons = new LinkedList<>();/**
* 将元素增加到list,在访问者模式中,一般使用attach,不使用add
*
* @param p
*/
public void attach(Person p) {
persons.add(p);
}/**
* 移除
*
* @param p
*/
public void detach(Person p) {
persons.remove(p);
}/**
* 显示测评情况
* @param action
*/
public void display(Action action) {
for (Person p : persons) {
p.accept(action);
}
}
}

【客户端】

package com.test.visitor;public class Client {public static void main(String[] args) {
//创建ObjectStructure
System.out.println("=======添加观众========");
ObjectStructure objectStructure = new ObjectStructure();
objectStructure.attach(new Man());
objectStructure.attach(new Woman());//成功
System.out.println("=======测评结果是成功晋级========");
Success success = new Success();
objectStructure.display(success);System.out.println("=======测评结果是失败========");
Fail fail = new Fail();
objectStructure.display(fail);
}}

【运行】

=======添加观众========
=======测评结果是成功晋级========
男人给的评价该歌手很成功 !
女人给的评价该歌手很成功 !
=======测评结果是失败========
男人给的评价该歌手失败 !
女人给的评价该歌手失败 !Process finished with exit code 0
拓展

上面的程序使用了双重分发,所谓双重分发是指不管类怎么变化,我们都能找到期望的方法运行。双重分发意味着得到执行的操作取决于请求的种类和两个接收者的类型。假设我们要添加一个Wait的状态类,考察Man类和Woman类的反应,由于使用了双重分发,只需增加一个Action子类即可在客户端调用即可,不需要改动任何其他类的代码

【增加类:Wait】

package com.test.visitor;public class Wait extends Action {@Override
public void getManResult(Man man) {
System.out.println(" 男人给的评价是该歌手待定 ..");
}@Override
public void getWomanResult(Woman woman) {
System.out.println(" 女人给的评价是该歌手待定 ..");
}}

【客户端】

package com.test.visitor;public class Client {public static void main(String[] args) {
//创建ObjectStructure
System.out.println("=======添加观众========");
ObjectStructure objectStructure = new ObjectStructure();
objectStructure.attach(new Man());
objectStructure.attach(new Woman());System.out.println("=======测评结果是待定========");
Wait wait = new Wait();
objectStructure.display(wait);
}}

【运行】

=======添加观众========
=======测评结果是待定========
男人给的评价是该歌手待定 ..
女人给的评价是该歌手待定 ..Process finished with exit code 0

案例二(个人感觉这个案例较好)

在这里插入图片描述

实现

【访问者抽象类】

package com.test.visitor.Sample;/**
* 访问者抽象类
* 依赖要访问的数据结构,File和Directory
*/
public abstract class Visitor {
/**
* 访问File类的方法
*
* @param file
*/
public abstract void visit(File file);/**
* 访问Directory类的方法
*
* @param directory
*/
public abstract void visit(Directory directory);
}

【接受访问的接口】

package com.test.visitor.Sample;/**
* 接受访问的接口
*/
public interface Element {
/**
* 接受访问
*
* @param v
*/
public abstract void accept(Visitor v);
}

【接受访问的抽象类】

这个类不需要实现accept方法,因为不是最终被访问的类

package com.test.visitor.Sample;import java.util.Iterator;public abstract class Entry implements Element {
/**
* 获取名字
*
* @return
*/
public abstract String getName();/**
* 获取大小
*
* @return
*/
public abstract int getSize();/**
* 增加目录条目
*
* @param entry
* @return
* @throws FileTreatmentException
*/
public Entry add(Entry entry) throws FileTreatmentException {
// 对 Directory 才有效,这里先简单报个错,让它自己重写一遍
throw new FileTreatmentException();
}/**
* 生成Iterator
*
* @return
* @throws FileTreatmentException
*/
public Iterator iterator() throws FileTreatmentException {
// 对 Directory 才有效,这里先简单报个错,让它自己重写一遍
throw new FileTreatmentException();
}/**
* 显示字符串
*
* @return
*/
public String toString() {
return getName() + " (" + getSize() + ")";
}
}

【异常类】

package com.test.visitor.Sample;public class FileTreatmentException extends RuntimeException {
public FileTreatmentException() {
}public FileTreatmentException(String msg) {
super(msg);
}
}

【接受访问的具体类:File(ConcreteElement角色)】

package com.test.visitor.Sample;public class File extends Entry {
private String name;
private int size;public File(String name, int size) {
this.name = name;
this.size = size;
}public String getName() {
return name;
}public int getSize() {
return size;
}public void accept(Visitor v) {
// 把自己交给访问者访问
v.visit(this);
}
}

【接受访问的具体类:Directory(ConcreteElement角色、ObjectStructure角色)】

package com.test.visitor.Sample;import java.util.ArrayList;
import java.util.Iterator;public class Directory extends Entry {
/**
* 文件夹名字
*/
private String name;
/**
* 目录条目集合
*/
private ArrayList dir = new ArrayList();/**
* 构造函数
* @param name
*/
public Directory(String name) {
this.name = name;
}/**
* 获取名字
* @return
*/
public String getName() {
return name;
}/**
* 获取大小
* @return
*/
public int getSize() {
int size = 0;
Iterator it = dir.iterator();
while (it.hasNext()) {
Entry entry = (Entry) it.next();
size += entry.getSize();
}
return size;
}/**
* 增加目录条目
* @param entry
* @return
*/
public Entry add(Entry entry) {
dir.add(entry);
return this;
}/**
* 生成Iterator
* @return
*/
public Iterator iterator() {
return dir.iterator();
}/**
* 接受访问者的访问
* @param v
*/
public void accept(Visitor v) {
v.visit(this);
}
}

【具体访问者】

package com.test.visitor.Sample;import java.util.Iterator;public class ListVisitor extends Visitor {
/**
* 当前访问的文件夹的名字
*/
private String currentdir = "";/**
* 在访问文件时被调用
* 访问的是文件,就简单输出一下,文件的信息
* @param file
*/
public void visit(File file) {
System.out.println(currentdir + "/" + file);
}/**
* 在访问文件夹时被调用
* 访问的是文件夹,不仅输出文件夹的信息,还要递归输出子文件和子文件夹的相关信息
* @param directory
*/
public void visit(Directory directory) {
System.out.println(currentdir + "/" + directory);
String savedir = currentdir;
currentdir = currentdir + "/" + directory.getName();
Iterator it = directory.iterator();
while (it.hasNext()) {
Entry entry = (Entry) it.next();
// 继续让当前访问者访问子文件或者文件夹
entry.accept(this);
}
currentdir = savedir;
}
}

【主类】

package com.test.visitor.Sample;public class Main {
public static void main(String[] args) {
try {
System.out.println("Making root entries...");
Directory rootdir = new Directory("root");
Directory bindir = new Directory("bin");
Directory tmpdir = new Directory("tmp");
Directory usrdir = new Directory("usr");
rootdir.add(bindir);
rootdir.add(tmpdir);
rootdir.add(usrdir);
bindir.add(new File("vi", 10000));
bindir.add(new File("latex", 20000));
// 接受访问,打印整个根目录下面的所有文件信息
rootdir.accept(new ListVisitor());              System.out.println("");
System.out.println("Making user entries...");
Directory yuki = new Directory("yuki");
Directory hanako = new Directory("hanako");
Directory tomura = new Directory("tomura");
usrdir.add(yuki);
usrdir.add(hanako);
usrdir.add(tomura);
yuki.add(new File("diary.html", 100));
yuki.add(new File("Composite.java", 200));
hanako.add(new File("memo.tex", 300));
tomura.add(new File("game.doc", 400));
tomura.add(new File("junk.mail", 500));
// 接受访问,打印整个根目录下面的所有文件信息
rootdir.accept(new ListVisitor());              
} catch (FileTreatmentException e) {
e.printStackTrace();
}
}
}

【运行】

Making root entries...
/root (30000)
/root/bin (30000)
/root/bin/vi (10000)
/root/bin/latex (20000)
/root/tmp (0)
/root/usr (0)Making user entries...
/root (31500)
/root/bin (30000)
/root/bin/vi (10000)
/root/bin/latex (20000)
/root/tmp (0)
/root/usr (1500)
/root/usr/yuki (300)
/root/usr/yuki/diary.html (100)
/root/usr/yuki/Composite.java (200)
/root/usr/hanako (300)
/root/usr/hanako/memo.tex (300)
/root/usr/tomura (900)
/root/usr/tomura/game.doc (400)
/root/usr/tomura/junk.mail (500)Process finished with exit code 0
分析
  • ConcreteVisitor 角色的开发可以独立于File类和Directory类。也就是说,Visitor模式提高了File类和Directory类作为组件的独立性。如果将进行处理的方法定义在File类和Directory类中,当每次要扩展功能,增加新的“处理”时,就不得不去修改File类和Directory类
拓展一

在示例程序中增加一个FileFindvistor类,用于将带有指定后缀名的文件手机起来,存储到集合中

【FileFindvistor】

package com.test.visitor.A1;import java.util.ArrayList;
import java.util.Iterator;public class FileFindVisitor extends Visitor {
private String filetype;
private ArrayList found = new ArrayList();/**
* 指定.后面的文件后缀名,如".txt"
*
* @param filetype
*/
public FileFindVisitor(String filetype) {
this.filetype = filetype;
}/**
* 获取已经找到的文件
*
* @return
*/
public Iterator getFoundFiles() {
return found.iterator();
}/**
* 在访问文件时被调用
*
* @param file
*/
public void visit(File file) {
if (file.getName().endsWith(filetype)) {
// 将符合格式的文件,添加到集合中
found.add(file);
}
}/**
* 在访问文件夹时被调用
*
* @param directory
*/
public void visit(Directory directory) {
Iterator it = directory.iterator();
while (it.hasNext()) {
Entry entry = (Entry) it.next();
entry.accept(this);
}
}
}

【主类】

package com.test.visitor.A1;import java.util.Iterator;public class Main {
public static void main(String[] args) {
try {
Directory rootdir = new Directory("root");
Directory bindir = new Directory("bin");
Directory tmpdir = new Directory("tmp");
Directory usrdir = new Directory("usr");
rootdir.add(bindir);
rootdir.add(tmpdir);
rootdir.add(usrdir);
bindir.add(new File("vi", 10000));
bindir.add(new File("latex", 20000));Directory yuki = new Directory("yuki");
Directory hanako = new Directory("hanako");
Directory tomura = new Directory("tomura");
usrdir.add(yuki);
usrdir.add(hanako);
usrdir.add(tomura);
yuki.add(new File("diary.html", 100));
yuki.add(new File("Composite.java", 200));
hanako.add(new File("memo.tex", 300));
hanako.add(new File("index.html", 350));
tomura.add(new File("game.doc", 400));
tomura.add(new File("junk.mail", 500));// 筛选出.html结尾的文件
FileFindVisitor ffv = new FileFindVisitor(".html");     
rootdir.accept(ffv);
// 输出.html结尾的文件
System.out.println("HTML files are:");
Iterator it = ffv.getFoundFiles();                      
while (it.hasNext()) {                                  
File file = (File)it.next();                        
System.out.println(file.toString());
}                                                       
} catch (FileTreatmentException e) {
e.printStackTrace();
}
}
}

【运行】

HTML files are:
diary.html (100)
index.html (350)Process finished with exit code 0
拓展二

Directory类的getSize方法的作用是获取文件夹大小,请编写一个获取大小的SizeVisitor类,用它替换掉 Directory类的getSize方法

【SizeVisitor】

import java.util.Iterator;public class SizeVisitor extends Visitor {
private int size = 0;public int getSize() {
return size;
}public void visit(File file) {
size += file.getSize();
}public void visit(Directory directory) {
Iterator it = directory.iterator();
while (it.hasNext()) {
Entry entry = (Entry) it.next();
entry.accept(this);
}
}
}

【修改Directory的方法】

package com.test.visitor.A2;import java.util.ArrayList;
import java.util.Iterator;public class Directory extends Entry {
private String name;
private ArrayList dir = new ArrayList();public Directory(String name) {         // 构造函数
this.name = name;
}public String getName() {               // 获取名字
return name;
}public int getSize() {
// 使用visitor来替换原来的方式
SizeVisitor v = new SizeVisitor();
accept(v);
return v.getSize();
}public Entry add(Entry entry) {
dir.add(entry);
return this;
}public Iterator iterator() {
return dir.iterator();
}public void accept(Visitor v) {
v.visit(this);
}
}
拓展三

基于java.util.ArrayList类编写一个具有Element接口的ElementArrayList类,使得Directory类和File类可以被add至ElementArrayList 中,而且它还可以接受(accept) ListVisitor 的实例访问它

【ElementArrayList】

package com.test.visitor.A3;import java.util.ArrayList;
import java.util.Iterator;/**
* 继承ArrayList,这样就不用定义集合的add remove等操作
*/
class ElementArrayList extends ArrayList implements Element {
public void accept(Visitor v) {
// 使用迭代器遍历
Iterator it = iterator();
while (it.hasNext()) {
Element e = (Element)it.next();
e.accept(v);
}
}
}

由于visit方法不用传入ElementArrayList类作为参数,因此不用修改Visitor

【主类】

package com.test.visitor.A3;public class Main {
public static void main(String[] args) {
try {
Directory root1 = new Directory("root1");
root1.add(new File("diary.html", 10));
root1.add(new File("index.html", 20));Directory root2 = new Directory("root2");
root2.add(new File("diary.html", 1000));
root2.add(new File("index.html", 2000));ElementArrayList list = new ElementArrayList();
list.add(root1);
list.add(root2);
list.add(new File("etc.html", 1234));list.accept(new ListVisitor());
} catch (FileTreatmentException e) {
e.printStackTrace();
}
}
}

【运行】

/root1 (30)
/root1/diary.html (10)
/root1/index.html (20)
/root2 (3000)
/root2/diary.html (1000)
/root2/index.html (2000)
/etc.html (1234)Process finished with exit code 0
123456789

总结

【优点】

  • 访问者模式符合单一职责原则、让程序具有优秀的扩展性、灵活性非常高
  • 访问者模式可以对功能进行统一,可以做报表、UI、拦截器与过滤器,适用于数据结构相对稳定的系统
  • 如果一个系统有比较稳定的数据结构,又有经常变化的功能需求,那么访问者模式就是比较合适的
  • 易于增加 ConcreteVisitor 角色

【缺点】

  • 具体元素对访问者公布细节,也就是访问者关注其他类的内部细节(如Success里面传入了Man,且调用其accept方法),这是迪米特法则不建议的,这样造成了具体元素变更比较困难
  • 违背了依赖倒转原则。访问者依赖的是具体元素,而不是抽象元素(Action里面依赖的是Man和Woman,而不是Person)
  • 难以增加ConcreteElement 角色。一旦增加了ConcreteElement 角色,需要在Visitor类中声明新的visit方法,而且所有的ConcreteVisitor都需要实现这个方法

额外知识

双重分发

// accept (接受)方法的调用方式
element.accept(visitor);// visit(访问)方法的调用方式
visitor.visit(element);

ConcreteElementConcreteVisitor这两个角色互相调用共同决定了实际进行的处理

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/647419.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

springboot优雅停机

import org.springframework.context.annotation.Configuration;import javax.annotation.PreDestroy;Configuration public class DataBackupConfig {PreDestroypublic void backData(){System.out.println("开始备份..."System.currentTimeMillis());System.out.pr…

6.Toast(Android)

愿你出走半生,归来仍是少年&#xff01; 环境&#xff1a;.NET 7、MAUI 在Maui开发中使用的Toast太丑了&#xff0c;在android项目中使用时不够看。通过Maui的安卓绑定库可实现将android中已有的包导入到C#项目中使用&#xff0c;借助这个方法就可以使用以前在android原生开发…

Python_NumPy——入门学习(概述,数据类型,创建数组)

作者&#xff1a;初次知晓 邮箱&#xff1a;lr_1052107892outlook.com 资料参考 [菜鸟教程](https://www.runoob.com/)NumPy概述 NumPy(Numerical Python)是python的一个扩展程序库&#xff0c;支持大量的维度数据与矩阵运算&#xff0c;针对数据运算提供大量的数学函数库,包…

如何降低微服务复杂度丨云栖大会微服务主题分享实录

作者&#xff1a;谢吉宝 本文整理自阿里云资深技术专家、中间件负责人谢吉宝在2023云栖大会《极简微服务模式&#xff0c;降低微服务复杂度的最佳实践》的分享 2023 云栖大会现场 当面临复杂的挑战时&#xff0c;"分而治之"的方法往往能取得显著的效果。微服务架构…

C++——类型转换与特殊类设计

我们在C语言中经常会使用到强制类型转换&#xff0c;例如指针和整形之间的转换是最为常见的&#xff0c;但是 在C中&#xff0c;C设计师认为这种强制类型转换是不安全的&#xff0c;所以在C标准中加入了四种强制 类型转换风格&#xff0c;这就是我将要介绍的强制类型转换。 在某…

VS2022联合Qt5开发学习11(QT5.12.3联合VTK在VS2022上开发医学图像项目5——qvtkWidget上显示STL三维图像并取点)

这篇博文是接着这个系列前面的博文&#xff0c;来讲如何实现医学图像三视图同步视图。我想到的一个思路是用Scrollbar来控制切面的改变&#xff0c;还有一个想法是在三维图像上取点&#xff0c;然后以这个点为切面中心更新三维视图。这篇博文主要介绍的就是第二个想法的三维图像…

C++ qt标题栏组件绘制

本博文源于笔者在学习C qt制作的标题栏组件&#xff0c;主要包含了&#xff0c;最小化&#xff0c;最大化&#xff0c;关闭。读者在看到这篇博文的时候&#xff0c;可以直接查看如何使用的&#xff0c;会使用了&#xff0c;然后进行复制粘贴源码部分即可。 问题来源 想要制作…

支持向量机(Support Vector Machines)(需要优化)

1.优化目标 一个更加强大的算法广泛的应用于工业界和学术界&#xff0c;它被称为支持向量机(Support Vector Machine)。与逻辑回归和神经网络相比&#xff0c;支持向量机&#xff0c;或者简称 SVM&#xff0c;在学习复杂的非线性方程时提供了一种更为清晰&#xff0c;更加强大…

css 中 flex 布局最后一行实现左对齐

问题 flex 布局最后一行没有进行左对齐显示&#xff1a; <div classparent><div classchild></div><div classchild></div><div classchild></div><div classchild></div><div classchild></div><div…

2022年至2023年广东省职业院校技能大赛高职组“信息安全管理与评估”赛项样题

2022 年至 2023 年广东省职业院校技能大赛高职组“信息安全管理与评估”赛项样题 一、 第一阶段竞赛项目试题 本文件为信息安全管理与评估项目竞赛第一阶段试题&#xff0c;第一阶段内容包 括&#xff1a;网络平台搭建、网络安全设备配置与防护。 本阶段比赛时间为 180 分钟…

华为数通方向HCIP-DataCom H12-831题库(判断题:121-140)

第121题 BGP/MPLS IP VPN内层采用MP-BGP分配的标签区分不同的VPN实例,外层可采用多种隧道类型,例如GRE隧道。 正确 错误 答案: 错误 解析: VPN业务的转发需要隧道来承载,隧道类型包括GRE隧道、LSP隧道、TE隧道(即CR-LSP)。 如果网络边缘的PE设备具备MPLS功能,但骨干网核…

林浩然与Hadoop的奇幻数据之旅

林浩然与Hadoop的奇幻数据之旅 Lin Haoran and the Enchanting Data Journey with Hadoop 在一个名为“比特村”的地方&#xff0c;住着一位名叫林浩然的程序员大侠。他并非江湖上常见的武艺高强之人&#xff0c;而是凭借一把键盘、一支鼠标&#xff0c;纵横在大数据的海洋里。…

UI跟随物体的关键是什么?重要吗?

引言 UI的跟随效果 在游戏开发中&#xff0c;UI的跟随效果是提高用户体验和交互性的重要组成部分。 本文将深入介绍如何创建一个高效且可定制的UI跟随目标组件&#xff0c;并分享一些最佳实践。 本文源工程在文末获取&#xff0c;小伙伴们自行前往。 UI跟随物体的关键 UI…

MQ回顾之kafka速通

不定期更新 官网概念自查 官网&#xff1a;Apache Kafka kafka结构 和kafka相关的关键名词有&#xff1a;Producer、Broker、Topic、Partition、Replication、Message、Consumer、Consumer Group、Zookeeper。 各名词解释已经泛滥&#xff0c;如果你想看点不一样的&#xf…

A 股承担着一个什么功能?

​A 股&#xff1a;中国资本市场的核心角色 A 股&#xff0c;即人民币普通股票&#xff0c;在中国资本市场中扮演着至关重要的角色。它不仅是投资者买卖交易的场所&#xff0c;更是中国经济发展的重要引擎。 首先&#xff0c;A 股为中国的企业提供了融资平台。中国有着庞大的…

从Elasticsearch来看分布式系统架构设计

从Elasticsearch来看分布式系统架构设计 - 知乎 分布式系统类型多&#xff0c;涉及面非常广&#xff0c;不同类型的系统有不同的特点&#xff0c;批量计算和实时计算就差别非常大。这篇文章中&#xff0c;重点会讨论下分布式数据系统的设计&#xff0c;比如分布式存储系统&…

Zookeeper3.5.7源码分析

文章目录 一、Zookeeper算法一致性1、Paxos 算法1.1 概述1.2 算法流程1.3 算法缺陷 2、ZAB 协议2.1 概述2.2 Zab 协议内容 3、CAP理论 二、源码详解1、辅助源码1.1 持久化源码(了解)1.2 序列化源码 2、ZK 服务端初始化源码解析2.1 启用脚本分析2.2 ZK 服务端启动入口2.3 解析参…

鸿蒙入门学习的一些总结

前言 刚开始接触鸿蒙是从2023年开始的&#xff0c;当时公司在调研鸿蒙开发板能否在实际项目中使用。我们当时使用的是OpenHarmony的&#xff0c;基于DAYU/rk3568开发板&#xff0c;最开始系统是3.2的&#xff0c;API最高是API9&#xff0c;DevecoStudio 版本3.1的。 鸿…

excel统计分析——Duncan法多重比较

参考资料&#xff1a;生物统计学 Duncan法又称新复极差检验法&#xff0c;是对S-N-K法的改进&#xff0c;根据秩次距m对临界值的显著水平α进行调整&#xff0c;是最常用的多重比较方法。最小显著极差表示如下&#xff1a; 其中&#xff0c;m为秩次距&#xff0c;df为方差分析中…

【软件测试】学习笔记-制定一份有效的性能测试方案

什么是性能测试方案&#xff1f; 性能测试方案&#xff0c;通俗一点说就是指导你进行性能测试的文档&#xff0c;包含测试目的、测试方法、测试场景、环境配置、测试排期、测试资源、风险分析等内容。一份详细的性能测试方案可以帮助项目成员明确测试计划和手段&#xff0c;更…