08.装饰者模式设计思想
目录介绍
- 01.装饰者模式基础
- 1.1 装饰者模式由来
- 1.2 装饰者模式定义
- 1.3 装饰者模式场景
- 1.4 装饰者模式思考
- 02.装饰者模式实现
- 2.1 罗列一个场景
- 2.2 装饰者结构
- 2.3 装饰者基本实现
- 03.装饰者实例演示
- 3.1 需求分析
- 3.2 案例基础实现
- 3.3 演变设计思想
- 3.4 使用装饰者模式
- 3.5 装饰器能否精简
- 3.6 透明性的要求
- 3.7 半透明装饰者模式
- 04.装饰器模式场景
- 4.1 IO流中装饰者模式
- 4.2 IO流设计结构
- 4.3 半透明装饰者模式
- 4.4 InputStream装饰者模式
- 05.装饰器模式分析
- 5.1 装饰模式优点
- 5.2 装饰模式缺点
- 5.3 总结一下学习
- 5.4 更多内容推荐
推荐一个好玩网站
一个最纯粹的技术分享网站,打造精品技术编程专栏!编程进阶网
01.装饰者模式基础
1.0 本博客AI摘要
装饰者模式是一种设计模式,用于在不修改原始类的情况下动态地给对象添加新功能。本文档详细介绍了装饰者模式的基础概念、实现方法、应用场景及优缺点。通过具体的咖啡示例和Java I/O流中的应用,展示了装饰者模式的灵活性和实用性。此外,还探讨了半透明装饰者模式的概念及其在实际开发中的应用。适合初学者和有一定经验的开发者学习。
1.1 装饰者模式由来
一般有两种方式可以实现给一个类或对象增加行为:
- 继承机制,使用继承机制是给现有类添加功能的一种有效途径,通过继承一个现有类可以使得子类在拥有自身方法的同时还拥有父类的方法。但是这种方法是静态的,用户不能控制增加行为的方式和时机。
- 关联机制,即将一个类的对象嵌入另一个对象中,由另一个对象来决定是否调用嵌入对象的行为以便扩展自己的行为,我们称这个嵌入的对象为装饰器(Decorator)
装饰模式以对客户透明的方式动态地给一个对象附加上更多的责任,换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。
装饰模式可以在不需要创造更多子类的情况下,将对象的功能加以扩展。这就是装饰模式的模式动机。
1.2 装饰者模式定义
装饰者模式又名包装(Wrapper)模式。装饰者模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案。
装饰者模式动态地将责任附加到对象身上。若要扩展功能,装饰者提供了比继承更有弹性的替代方案。
1.3 装饰者模式场景
装饰者模式适用于以下场景:
- 动态地给一个对象添加额外的功能,而不需要修改其原始类的代码。装饰者模式允许在运行时动态地添加、修改或删除对象的行为。
- 需要扩展一个类的功能,但是使用继承会导致类的数量急剧增加。通过装饰者模式,可以避免创建大量的子类,而是通过组合不同的装饰器来实现功能的扩展。
- 需要在不影响其他对象的情况下,对对象的功能进行动态组合和排列。装饰者模式可以通过不同的装饰器组合来实现不同的功能组合,而不需要修改原始对象或其他装饰器。
装饰者模式常见的应用场景包括:
- Java中IO流操作中的装饰者模式、Swing GUI组件中的装饰者模式、Java I/O库中的BufferedInputStream和BufferedOutputStream等。
- Android中自定义View的功能扩展、RecyclerView的ItemDecoration、Fragment的功能增强等。
- 其他日志功能,日志记录功能的实现也是装饰者模式的理想应用场景。例如,传统的日志记录器Logger可以通过装饰者模式添加不同的日志处理和记录策略,如格式化日志、输出到文件、发送电子邮件等。
1.4 装饰者模式思考
当考虑使用装饰者模式时,可以思考以下几个方面:更多内容
- 是否需要在运行时动态地添加、修改或删除对象的行为?如果是的话,装饰者模式符合要求
- 需要扩展一个类的功能,但是使用继承会导致类的数量急剧增加吗?如果是的话,装饰者模式可以通过组合不同的装饰器来实现功能的扩展,而不需要创建大量的子类。
- 是否需要为一个对象的不同部分或属性提供独立的扩展?如果是的话,装饰者模式允许对对象的不同部分进行独立的扩展,而不会影响其他部分。
02.装饰者模式实现
2.1 罗列一个场景
我们将以一个具体的咖啡示例来介绍装饰者模式的实现。在这个例子中,我们有一个基本的咖啡对象,可以动态地添加不同的配料(如牛奶和糖)。更多内容
比如:
- 咖啡A,加牛奶和半糖
- 咖啡B,加牛奶和全糖
- 咖啡C,加牛奶和多糖
- 咖啡D,不加牛奶和半糖
2.2 装饰者结构
装饰者模式以对客户透明的方式动态地给一个对象附加上更多的责任。
换言之,客户端并不会觉得对象在装饰前和装饰后有什么不同。装饰者模式可以在不使用创造更多子类的情况下,将对象的功能加以扩展。
在装饰模式中的角色有:更多内容
- ● 抽象构件(Component)角色:给出一个抽象接口,以规范准备接收附加责任的对象。
- ● 具体构件(ConcreteComponent)角色:定义一个将要接收附加责任的类。
- ● 装饰(Decorator)角色:持有一个构件(Component)对象的实例,并定义一个与抽象构件接口一致的接口。
- ● 具体装饰(ConcreteDecorator)角色:负责给构件对象“贴上”附加的责任。
2.3 装饰者基本实现
抽象构件角色
// Java实现
public interface Coffee {
double cost();
String getDescription();
}
具体构件角色
public class SimpleCoffee implements Coffee {
public double cost() {
return 5.0;
}
public String getDescription() {
return "Simple Coffee";
}
}
装饰角色
public abstract class CoffeeDecorator implements Coffee {
protected Coffee coffee;
public CoffeeDecorator(Coffee coffee) {
this.coffee = coffee;
}
public double cost() {
return coffee.cost();
}
public String getDescription() {
return coffee.getDescription();
}
}
具体装饰角色
public class MilkDecorator extends CoffeeDecorator {
public MilkDecorator(Coffee coffee) {
super(coffee);
}
public double cost() {
return super.cost() + 1.5;
}
public String getDescription() {
return super.getDescription() + ", Milk";
}
}
public class SugarDecorator extends CoffeeDecorator {
public SugarDecorator(Coffee coffee) {
super(coffee);
}
public double cost() {
return super.cost() + 0.5;
}
public String getDescription() {
return super.getDescription() + ", Sugar";
}
}
使用装饰者模式
private void test1() {
Coffee coffee = new SimpleCoffee();
System.out.println(coffee.getDescription() + " Cost: $" + coffee.cost());
coffee = new MilkDecorator(coffee);
System.out.println(coffee.getDescription() + " Cost: $" + coffee.cost());
coffee = new SugarDecorator(coffee);
System.out.println(coffee.getDescription() + " Cost: $" + coffee.cost());
}
03.装饰者实例演示
3.1 需求分析
齐天大圣的例子
孙悟空有七十二般变化,他的每一种变化都给他带来一种附加的本领。他变成鱼儿时,就可以到水里游泳;他变成鸟儿时,就可以在天上飞行。
3.2 案例基础实现
3.3 演变设计思想
3.4 使用装饰者模式
本例中,Component的角色便由鼎鼎大名的齐天大圣扮演;ConcreteComponent的角色属于大圣的本尊,就是猢狲本人;Decorator的角色由大圣的七十二变扮演。而ConcreteDecorator的角色便是鱼儿、鸟儿等七十二般变化。
抽象构件角色“齐天大圣”接口定义了一个move()方法,这是所有的具体构件类和装饰类必须实现的。
//大圣的尊号
public interface TheGreatestSage {
void move();
}
具体构件角色“大圣本尊”猢狲类更多内容
public class Monkey implements TheGreatestSage {
@Override
public void move() {
//代码
System.out.println("Monkey Move");
}
}
抽象装饰角色“七十二变”
public class Change implements TheGreatestSage {
private TheGreatestSage sage;
public Change(TheGreatestSage sage){
this.sage = sage;
}
@Override
public void move() {
// 代码
sage.move();
}
}
具体装饰角色“鱼儿”
public class Fish extends Change {
public Fish(TheGreatestSage sage) {
super(sage);
}
@Override
public void move() {
// 代码
System.out.println("Fish Move");
}
}
具体装饰角色“鸟儿”
public class Bird extends Change {
public Bird(TheGreatestSage sage) {
super(sage);
}
@Override
public void move() {
// 代码
System.out.println("Bird Move");
}
}
客户端调用
public class Client {
public static void main(String[] args) {
TheGreatestSage sage = new Monkey();
// 第一种写法 单层装饰
TheGreatestSage bird = new Bird(sage);
TheGreatestSage fish = new Fish(bird);
// 第二种写法 双层装饰
//TheGreatestSage fish = new Fish(new Bird(sage));
fish.move();
}
}
“大圣本尊”是ConcreteComponent类,而“鸟儿”、“鱼儿”是装饰类。要装饰的是“大圣本尊”,也即“猢狲”实例。
上面的例子中,第二种些方法:系统把大圣从一只猢狲装饰成了一只鸟儿(把鸟儿的功能加到了猢狲身上),然后又把鸟儿装饰成了一条鱼儿(把鱼儿的功能加到了猢狲+鸟儿身上,得到了猢狲+鸟儿+鱼儿)。
3.5 装饰器能否精简
大多数情况下,装饰者模式的实现都要比上面给出的示意性例子要简单。更多内容
如果只有一个ConcreteComponent类,那么可以考虑去掉抽象的Component类(接口),把Decorator作为一个ConcreteComponent子类。
如果只有一个ConcreteDecorator类,那么就没有必要建立一个单独的Decorator类,而可以把Decorator和ConcreteDecorator的责任合并成一个类。甚至在只有两个ConcreteDecorator类的情况下,都可以这样做。
3.6 透明性的要求
装饰者模式的透明性要求是指在使用装饰者模式时,对于客户端来说,应该能够透明地使用原始对象和装饰器对象,而不需要关心具体的对象类型。
装饰者模式对客户端的透明性要求程序不要声明一个ConcreteComponent类型的变量,而应当声明一个Component类型的变量。
用孙悟空的例子来说,必须永远把孙悟空的所有变化都当成孙悟空来对待,而如果把老孙变成的鱼儿当成鱼儿,而不是老孙,那就被老孙骗了,而这时不应当发生的。下面的做法是对的:
TheGreatestSage sage = new Monkey();
TheGreatestSage bird = new Bird(sage);
而下面的做法是不对的:
Monkey sage = new Monkey();
Bird bird = new Bird(sage);
在实际应用中,可能会因为装饰器对象的嵌套而导致透明性的破坏。因此,在使用装饰者模式时,需要权衡透明性和复杂性,并根据具体的需求和情况做出决策。
3.7 半透明装饰者模式
然而,纯粹的装饰者模式很难找到。装饰者模式的用意是在不改变接口的前提下,增强所考虑的类的性能。在增强性能的时候,往往需要建立新的公开的方法。更多内容
即便是在孙大圣的系统里,也需要新的方法。比如齐天大圣类并没有飞行的能力,而鸟儿有。这就意味着鸟儿应当有一个新的fly()方法。再比如,齐天大圣类并没有游泳的能力,而鱼儿有,这就意味着在鱼儿类里应当有一个新的swim()方法。
这就导致了大多数的装饰者模式的实现都是“半透明”的,而不是完全透明的。换言之,允许装饰者模式改变接口,增加新的方法。这意味着客户端可以声明ConcreteDecorator类型的变量,从而可以调用ConcreteDecorator类中才有的方法:
TheGreatestSage sage = new Monkey();
Bird bird = new Bird(sage);
bird.fly();
半透明的装饰者模式是介于装饰者模式和适配器模式之间的。适配器模式的用意是改变所考虑的类的接口,也可以通过改写一个或几个方法,或增加新的方法来增强或改变所考虑的类的功能。大多数的装饰者模式实际上是半透明的装饰者模式,这样的装饰者模式也称做半装饰、半适配器模式。
04.装饰器模式场景
4.1 IO流中装饰者模式
装饰者模式在Java语言中的最著名的应用莫过于Java I/O标准库的设计了。
由于Java I/O库需要很多性能的各种组合,如果这些性能都是用继承的方法实现的,那么每一种组合都需要一个类,这样就会造成大量性能重复的类出现。而如果采用装饰者模式,那么类的数目就会大大减少,性能的重复也可以减至最少。因此装饰者模式是Java I/O库的基本模式。
Java I/O库的对象结构图如下,由于Java I/O的对象众多,因此只画出InputStream的部分。
- InputStream是最上层的父类,表示字节流
- FileInputStream是一个具体类,用来对文件进行读取
- BufferedInputStream是字节缓冲流,主要是为了减少I/O操作的次数,提高效率。
4.2 IO流设计结构
IO流中装饰者模式结构如下所示:更多内容
- 抽象构件(Component)角色:由InputStream扮演。这是一个抽象类,为各种子类型提供统一的接口。
- 具体构件(ConcreteComponent)角色:由ByteArrayInputStream、FileInputStream、PipedInputStream、StringBufferInputStream等类扮演。它们实现了抽象构件角色所规定的接口。
- 抽象装饰(Decorator)角色:由FilterInputStream扮演。它实现了InputStream所规定的接口。
- 具体装饰(ConcreteDecorator)角色:由几个类扮演,分别是BufferedInputStream、DataInputStream以及两个不常用到的类LineNumberInputStream、PushbackInputStream。
4.3 半透明装饰者模式
装饰者模式和适配器模式都是“包装模式(Wrapper Pattern)”,它们都是通过封装其他对象达到设计的目的的,但是它们的形态有很大区别。
理想的装饰者模式在对被装饰对象进行功能增强的同时,要求具体构件角色、装饰角色的接口与抽象构件角色的接口完全一致。而适配器模式则不然,一般而言,适配器模式并不要求对源对象的功能进行增强,但是会改变源对象的接口,以便和目标接口相符合。
装饰者模式有透明和半透明两种,这两种的区别就在于装饰角色的接口与抽象构件角色的接口是否完全一致。透明的装饰者模式也就是理想的装饰者模式,要求具体构件角色、装饰角色的接口与抽象构件角色的接口完全一致。相反,如果装饰角色的接口与抽象构件角色接口不一致,也就是说装饰角色的接口比抽象构件角色的接口宽的话,装饰角色实际上已经成了一个适配器角色,这种装饰者模式也是可以接受的,称为“半透明”的装饰模式。
在适配器模式里面,适配器类的接口通常会与目标类的接口重叠,但往往并不完全相同。换言之,适配器类的接口会比被装饰的目标类接口宽。更多内容
显然,半透明的装饰者模式实际上就是处于适配器模式与装饰者模式之间的灰色地带。如果将装饰者模式与适配器模式合并成为一个“包装模式”的话,那么半透明的装饰者模式倒可以成为这种合并后的“包装模式”的代表。
4.4 InputStream装饰者模式
InputStream类型中的装饰者模式是半透明的。为了说明这一点,不妨看一看作装饰者模式的抽象构件角色的InputStream的源代码。这个抽象类声明了九个方法,并给出了其中八个的实现,另外一个是抽象方法,需要由子类实现。
public abstract class InputStream implements Closeable {
public abstract int read() throws IOException;
public int read(byte b[]) throws IOException {}
public int read(byte b[], int off, int len) throws IOException {}
public long skip(long n) throws IOException {}
public int available() throws IOException {}
public void close() throws IOException {}
public synchronized void mark(int readlimit) {}
public synchronized void reset() throws IOException {}
public boolean markSupported() {}
}
下面是作为装饰模式的抽象装饰角色FilterInputStream类的源代码。更多内容可以看出,FilterInputStream的接口与InputStream的接口是完全一致的。也就是说,直到这一步,还是与装饰模式相符合的。
public class FilterInputStream extends InputStream {
protected FilterInputStream(InputStream in) {}
public int read() throws IOException {}
public int read(byte b[]) throws IOException {}
public int read(byte b[], int off, int len) throws IOException {}
public long skip(long n) throws IOException {}
public int available() throws IOException {}
public void close() throws IOException {}
public synchronized void mark(int readlimit) {}
public synchronized void reset() throws IOException {}
public boolean markSupported() {}
}
下面是具体装饰角色PushbackInputStream的源代码。
public class PushbackInputStream extends FilterInputStream {
private void ensureOpen() throws IOException {}
public PushbackInputStream(InputStream in, int size) {}
public PushbackInputStream(InputStream in) {}
public int read() throws IOException {}
public int read(byte[] b, int off, int len) throws IOException {}
public void unread(int b) throws IOException {}
public void unread(byte[] b, int off, int len) throws IOException {}
public void unread(byte[] b) throws IOException {}
public int available() throws IOException {}
public long skip(long n) throws IOException {}
public boolean markSupported() {}
public synchronized void mark(int readlimit) {}
public synchronized void reset() throws IOException {}
public synchronized void close() throws IOException {}
}
查看源码,你会发现,这个装饰类提供了额外的方法unread(),这就意味着PushbackInputStream是一个半透明的装饰类。更多内容,换言 之,它破坏了理想的装饰者模式的要求。如果客户端持有一个类型为InputStream对象的引用in的话,那么如果in的真实类型是 PushbackInputStream的话,只要客户端不需要使用unread()方法,那么客户端一般没有问题。但是如果客户端必须使用这个方法,就 必须进行向下类型转换。将in的类型转换成为PushbackInputStream之后才可能调用这个方法。但是,这个类型转换意味着客户端必须知道它 拿到的引用是指向一个类型为PushbackInputStream的对象。这就破坏了使用装饰者模式的原始用意。
现实世界与理论总归是有一段差距的。纯粹的装饰者模式在真实的系统中很难找到。一般所遇到的,都是这种半透明的装饰者模式。
下面是使用I/O流读取文件内容的简单操作示例。
public static void main(String[] args) throws IOException {
// 流式读取文件
DataInputStream dis = null;
try{
dis = new DataInputStream(new BufferedInputStream(new FileInputStream("test.txt")));
//读取文件内容
byte[] bs = new byte[dis.available()];
dis.read(bs);
String content = new String(bs);
System.out.println(content);
}finally{
dis.close();
}
}
观察上面的代码,会发现最里层是一个FileInputStream对象,然后把它传递给一个BufferedInputStream对象,经过BufferedInputStream处理,再把处理后的对象传递给了DataInputStream对象进行处理,这个过程其实就是装饰器的组装过程。
FileInputStream对象相当于原始的被装饰的对象,而BufferedInputStream对象和DataInputStream对象则相当于装饰器。更多内容
05.装饰器模式分析
5.1 装饰模式优点
- 1)装饰模式与继承关系的目的都是要扩展对象的功能,但是装饰模式可以提供比继承更多的灵活性。装饰模式允许系统动态决定“贴上”一个需要的“装饰”,或者除掉一个不需要的“装饰”。继承关系则不同,继承关系是静态的,它在系统运行前就决定了。
- 2)通过使用不同的具体装饰类以及这些装饰类的排列组合,设计师可以创造出很多不同行为的组合。
5.2 装饰模式缺点
- 由于使用装饰模式,可以比使用继承关系需要较少数目的类。使用较少的类,当然使设计比较易于进行。更多内容,但是,在另一方面,使用装饰模式会产生比使用继承关系更多的对象。更多的对象会使得查错变得困难,特别是这些对象看上去都很相像。
5.3 总结一下学习
01.装饰者模式基础
给一个类或者对象增加行为:1.继承,子类通过重写父类方法修改行为。2.关联机制,将一个类的对象嵌入另一个对象中用于行为拓展。
装饰者模式以对客户端透明的方式扩展对象的功能,是继承关系的一个替代方案。
装饰者模式适用于以下场景:更多内容
- 动态地给一个对象添加额外的功能,而不需要修改其原始类的代码。
- 需要扩展一个类的功能,但是使用继承会导致类的数量急剧增加。
- 需要在不影响其他对象的情况下,对对象的功能进行动态组合和排列。
02.装饰者模式实现
比如:有一个基本的咖啡对象,可以动态地添加不同的配料(如牛奶和糖)。在不改变对象情况下,对行为进行拓展!
- 咖啡A,加牛奶和半糖
- 咖啡B,加牛奶和全糖
- 咖啡C,加牛奶和多糖
- 咖啡D,不加牛奶和半糖
在装饰模式中的角色有:
- ● 抽象构件(Component)角色:给出一个抽象接口,以规范准备接收附加责任的对象。
- ● 具体构件(ConcreteComponent)角色:定义一个将要接收附加责任的类。
- ● 装饰(Decorator)角色:持有一个构件(Component)对象的实例,并定义一个与抽象构件接口一致的接口。
- ● 具体装饰(ConcreteDecorator)角色:负责给构件对象“贴上”附加的责任。
03.装饰者实例演示
半透明的装饰者模式是介于装饰者模式和适配器模式之间的。适配器模式的用意是改变所考虑的类的接口,也可以通过改写一个或几个方法,或增加新的方法来增强或改变所考虑的类的功能。大多数的装饰者模式实际上是半透明的装饰者模式,这样的装饰者模式也称做半装饰、半适配器模式。
04.装饰器模式场景
装饰者模式在Java语言中的最著名的应用莫过于Java I/O标准库的设计了。
IO流中装饰者模式结构如下所示:
- 抽象构件(Component)角色:由InputStream扮演。这是一个抽象类,为各种子类型提供统一的接口。
- 具体构件(ConcreteComponent)角色:由ByteArrayInputStream、FileInputStream、PipedInputStream、StringBufferInputStream等类扮演。它们实现了抽象构件角色所规定的接口。
- 抽象装饰(Decorator)角色:由FilterInputStream扮演。它实现了InputStream所规定的接口。
- 具体装饰(ConcreteDecorator)角色:由几个类扮演,分别是BufferedInputStream、DataInputStream以及两个不常用到的类LineNumberInputStream、PushbackInputStream。
5.4 更多内容推荐
模块 | 描述 | 备注 |
---|---|---|
GitHub | 多个YC系列开源项目,包含Android组件库,以及多个案例 | GitHub |
博客汇总 | 汇聚Java,Android,C/C++,网络协议,算法,编程总结等 | YCBlogs |
设计模式 | 六大设计原则,23种设计模式,设计模式案例,面向对象思想 | 设计模式 |
Java进阶 | 数据设计和原理,面向对象核心思想,IO,异常,线程和并发,JVM | Java高级 |
网络协议 | 网络实际案例,网络原理和分层,Https,网络请求,故障排查 | 网络协议 |
计算机原理 | 计算机组成结构,框架,存储器,CPU设计,内存设计,指令编程原理,异常处理机制,IO操作和原理 | 计算机基础 |
学习C编程 | C语言入门级别系统全面的学习教程,学习三到四个综合案例 | C编程 |
C++编程 | C++语言入门级别系统全面的教学教程,并发编程,核心原理 | C++编程 |
算法实践 | 专栏,数组,链表,栈,队列,树,哈希,递归,查找,排序等 | Leetcode |
Android | 基础入门,开源库解读,性能优化,Framework,方案设计 | Android |
23种设计模式
23种设计模式 & 描述 & 核心作用 | 包括 |
---|---|
创建型模式 提供创建对象用例。能够将软件模块中对象的创建和对象的使用分离 |
工厂模式(Factory Pattern) 抽象工厂模式(Abstract Factory Pattern) 单例模式(Singleton Pattern) 建造者模式(Builder Pattern) 原型模式(Prototype Pattern) |
结构型模式 关注类和对象的组合。描述如何将类或者对象结合在一起形成更大的结构 |
适配器模式(Adapter Pattern) 桥接模式(Bridge Pattern) 过滤器模式(Filter、Criteria Pattern) 组合模式(Composite Pattern) 装饰器模式(Decorator Pattern) 外观模式(Facade Pattern) 享元模式(Flyweight Pattern) 代理模式(Proxy Pattern) |
行为型模式 特别关注对象之间的通信。主要解决的就是“类或对象之间的交互”问题 |
责任链模式(Chain of Responsibility Pattern) 命令模式(Command Pattern) 解释器模式(Interpreter Pattern) 迭代器模式(Iterator Pattern) 中介者模式(Mediator Pattern) 备忘录模式(Memento Pattern) 观察者模式(Observer Pattern) 状态模式(State Pattern) 空对象模式(Null Object Pattern) 策略模式(Strategy Pattern) 模板模式(Template Pattern) 访问者模式(Visitor Pattern) |
5.5 更多内容
- GitHub:https://github.com/yangchong211
- 我的编程网站:https://yccoding.com
- 博客汇总:https://github.com/yangchong211/YCBlogs
- 设计模式专栏:https://github.com/yangchong211/YCDesignBlog
- Java高级进阶专栏:https://github.com/yangchong211/YCJavaBlog
- 网络协议专栏:https://github.com/yangchong211/YCNetwork
- 计算机基础原理专栏:https://github.com/yangchong211/YCComputerBlog
共同学习,写下你的评论
评论加载中...
作者其他优质文章