- 本篇首先展示在没有spring时编程会遇到的一些痛点,引出spring
- 介绍了耦合、工厂等概念
- 可以更好的理解spring的诞生,以及spring都解决了哪些问题,spring的优势
- 不涉及spring的具体使用,不想看的可以直接跳过本篇
- 目录介绍:
- 0 干货区:全文重点提炼,第一次看的可以直接跳过,之后当作工具栏回顾
- 1 耦合:介绍耦合的概念、解耦的目标、思路和方法
- 1.1 耦合是什么
- 1.2 解耦
- 2 工厂:举例了解工厂的概念和作用,理解spring中的工厂
- 2.1 理解工厂
- 2.2 工厂与spring
- 3 小结
0 干货区
- 耦合:
- 概念:编译时的程序间依赖关系,包括类间的、方法间的依赖
- 解耦的目标:编译时不依赖,运行时才依赖
- 解耦的思路:
- 通过反射来创建对象,而避免使用new关键字
- 通过读取配置文件来获取要创建对象的全限定类型
- 使用工厂来制造对象,减少重复代码
- 工厂:
- 目标:流水化生产对象
- 作用:减少重复代码
- spring:在某方面来说,spring就是一个帮我们创建对象并放入容器中保管的工厂
- 总结:拥有spring,我们就能高效简洁的编写出低耦合的优质代码
1、耦合
1.1 耦合是什么
- 在编程中,有一个常见术语:耦合
- 耦合度低,是衡量程序编码是否优秀的标准之一
- 那么什么是耦合呢
- 很多解释是:程序间的依赖关系。包括类之间的依赖、方法间的依赖等
- 那么像我这样刚学习的小白,就会很困惑,类和方法之间的依赖不是很常见吗?可以说,没有依赖是不可能写出程序的
- 所以学习之后,我认为应该更精准的定义一下:
- 耦合指的是编译时的程序间依赖关系
- 很多解释是:程序间的依赖关系。包括类之间的依赖、方法间的依赖等
- 还是像我这样的小白,可能不能很好的理解这个意思,没关系,我们先来看一个程序
// 使用jdbc连接数据库
// 第一步:注册驱动
DriverManager.registerDriver(new com.mysql.jdbc.Driver());
- 一行很简单的注册jdbc数据库驱动的代码,这里有一个重点
- 使用这行代码的前提是什么:引入com.mysql.jdbc的jar包。
- 不论是eclipse还是IDEA也好,如果没有引入该jar包,这一行代码肯定会变红
- 也就是说:无法编译,即,此处依赖Driver类,有了耦合
- 那么我们修改一下这行代码
// 使用jdbc连接数据库
// 第一步:注册驱动
Class.forName("com.mysql.jdbc.Driver");
- 好,我们来看下第二段代码
- 和第一段代码有什么区别呢
- 1、使用了反射,而不是new的方式来创建一个对象
- new依赖一个具体的类,而反射只是依赖一个字符串
- 2、没有相关jar包,可以编译通过(即解耦)
- 1、使用了反射,而不是new的方式来创建一个对象
- 相同之处
- 1、都是在注册驱动
- 2、都需要jdbc的Jar包
- 只不过第一段是在编译时就需要
- 第二段是运行时需要,否则会报错
- 和第一段代码有什么区别呢
1.2 解耦
- 通过1.1节,明确了耦合的概念是:编译时的程序间依赖关系
- 那么降低耦合的目标就是:编译时不依赖,运行时才依赖
- 通过1.1节的例子,也很明确地给出了解耦的思路:
- 使用反射来创建对象,而避免使用new关键字
- 好,确定了这个思路之后,再看看1.1节的例子,有没有觉得第二段程序哪里还是有点问题呢?
- 注意,第二段里的那串字符串,是在程序内写死的
- 在程序里写死,意味着非常的不灵活,需要改变
- 继续优化程序,由此可以引出第二条解耦的思路:
- 通过读取配置文件来获取要创建对象的全限定类型
- 举个例子,你要办个party,通过经纪人老王联系到明星美丽来表演
// 这是接口Star,全限定类名:com.star.Star
public interface Star {
public void sing(); // 明星要会唱歌
}
// 这是Star的实现类Meili,全限定类名:com.star.impl.Meili
public class Meili implements Star{
public void sing() {
System.out.println("Meili is singing.");
}
}
// 这是接口Agent,全限定类名:com.agent.Agent
public interface Agent {
public void sing(Star star);
}
// 这是Agent的实现类Laowang,全限定类名:com.agent.impl.Laowang
public class Laowang implements Agent {
private Star meili = new Meili();
public void sing() { // 叫明星来唱歌
meili.sing;
}
}
- 美丽和老王都准备好了,接下来准备我们的配置类party.properties
meili=com.star.impl.Meili
laowang=com.agent.impl.Laowang
- OK,终于可以开party了
public class Party {
public static void main(String[] args) {
Agent laowang = null;
try {
// 1.创建配置文件读取类对象
Properties props = new Properties();
// 2.获取properties文件的流对象
props.load(Party.class.getClassLoader().getResourceAsStream("party.properties"));
// 3.获得laowang这个key对应的value,即全限定类名
String beanPath = props.getProperty("laowang");
System.out.println(beanPath);
// 4.通过反射调用默认构造函数创建对象,因为反射返回的是Object,强转为Laowang
laowang = (Laowang) Class.forName(beanPath).newInstance();
// 5.通过老王让美丽唱歌
laowang.sing();
} catch (Exception e) {
e.printStackTrace();
}
}
}
运行一下,获得的结果是:
com.agent.impl.Laowang
Meili is singing.
- 看了上面这段程序,肯定会有人内心崩溃吐槽:这不是还是各种使用了new吗?!!!
- OK,我们来分开分析一下应该出现new的三个地方
- 1、Party类中读取配置文件的props对象,使用了new
- 首先我们必须声明,解耦只能做到尽量降低耦合度,并不可能完全消除耦合,所以显然是无法在程序中消除所有new关键字的
- 显然,props的new,就是无法避免的情况
- 2、Party类中的经纪人laowang对象
- 这里我们成功消除了new,来创建一个laowang对象,鼓掌!
- 那么还有什么问题呢,laowang这个对象名称,还是在程序中写死了是吧?
- 这就类似于解耦,也是尽量降低而做不到完全消除
- 当然后面讲到spring,会发现通过一种默认方式其实还是可以做到的(本篇不讲)
- 3、Laowang类中的明星meili对象
- 这里依然是通过new来创建了一个meili对象
- 但是我们明明在配置文件中也配置了Meili的全限定类名
- 其实这里也可以通过读取配置文件然后反射出meili对象,去掉new
- 那么会发生什么呢,我们要再创建一个props,获取流,打开配置文件,读取value。。。
- 优秀的程序应该尽量减少重复代码
- 所以显然,我们可以把读取配置文件、反射生成对象的这部分功能,单独抽出来,编写一个专门用于制造对象的工厂类
- 1、Party类中读取配置文件的props对象,使用了new
2、工厂
2.1 理解工厂
- 这里涉及到一个设计模式:工厂模式
- 工厂模式的具体内容,本篇不细讲,后面有空专门写一篇工厂模式再贴上来
- 这里大概意会一下工厂模式的作用就行了
- 工厂,通过现实里的概念也很好理解:
- 有投入、有产出
- 流水化作业
- 标准产品
- 映射到编程里,可以对应以下几个概念:
- 有传入参数,有返回值
- 一个工具类
- 返回值要具有通用性
- 回到我们的例子里,现在要建造一个专门用于生产对象的工厂类啦
public class ObjectFactory {
// 定义一个properties对象。定义为static,避免不必要的内存开销
private static Properties props;
// 使用静态代码块为props对象赋值
static {
try {
// 实例化对象
props = new Properties();
// 获取properties文件的流对象
props.load(PartyFactory.class.getClassLoader().getResourceAsStream("party.properties"));
} catch (Exception e) {
throw new ExceptionInInitializerError("初始化properties失败!");
}
}
/**
* 生成对象方法。定义为static,可以通过类名直接调用
* @param beanName 要生成的对象名称,配置文件中的key值
* @return 返回生成的对象,定义为Object类型,具有通用性
*/
public static Object getBean(String beanName) {
Object bean = null;
try {
// 获取key对应的value,即全限定类名
String beanPath = props.getProperty(beanName);
// 通过反射生成对象
bean = Class.forName(beanPath).newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return bean;
}
}
- 再调整一下Laowang类
public class Laowang implements Agent {
public void sing() {
// 使用对象工厂获得meili对象,去掉new实现解耦
Star meili = (Meili)ObjectFactory.getBean("meili");
meili.sing();
}
}
- 又可以开party啦
public class Party {
public static void main(String[] args) {
// 通过对象工厂类获取laowang对象
Agent laowang = (Laowang)ObjectFactory.getBean("laowang");
laowang.sing();
}
}
- 运行一下,结果是:
Meili is singing.
- 在上例的代码中,可以看到我们完全把创建对象的工作交给了工厂类来做
- 这个就叫做:IOC(控制反转)
- IOC是spring的核心概念之一
- 讲到这里,大家可能隐隐约约猜到spring的本质了
- spring就是一个帮我们创建对象的工厂
- 是这样吗?
2.2 工厂与spring
- 在上面举的例子里,不知道大家有没有发现一个问题
- 工厂类中,getBean这个方法,调用一次,就创建一个独立的对象
- 我们可以在main中把做一个无限的for循环,很快就会内存溢出导致程序挂掉
- 而实际上,在Meili类和Laowang类里没有其他有线程安全问题的成员变量的情况下,其实我们只需要工厂类创建一个对象,就可以反复循环使用了
- 顺着这个思路,也就是说
- 配置文件中的每条配置,也就说每个数据类型,都只创建一个具体变量
- 变量创建出来后,需要找个容器保存下来,以备后续调用
- 当外部再调用获取变量的方法时,直接去容器里查找
- 我们再修改一下工厂类
public class ObjectFactory {
// 定义一个properties对象
private static Properties props;
// 定义一个Map,用于存放我们要创建的对象。我们把它称之为容器
private static Map<String, Object> beans;
// 使用静态代码块为props对象赋值并填充容器
static {
try {
// 1.实例化props,读取配置文件
props = new Properties();
props.load(ObjectFactory.class.getClassLoader().getResourceAsStream("party.properties"));
// 2.填充容器
beans = new HashMap<String, Object>();
// 2.1取出配置文件中所有的keys
Enumeration keys = props.keys();
// 2.2遍历枚举
while (keys.hasMoreElements()) {
// 2.3取出每个key
String key = keys.nextElement().toString();
// 2.4根据key获取value
String beanPath = props.getProperty(key);
// 2.5反射创建对象
Object value = Class.forName(beanPath).newInstance();
// 2.6把key和value存入容器之中
beans.put(key, value);
}
} catch (Exception e) {
e.printStackTrace();
}
}
/**
* 根据bean的名称获取bean对象
* @param beanName
* @return
*/
public static Object getBean(String beanName) {
return beans.get(beanName);
}
}
- 继续开party
public class Party {
public static void main(String[] args) {
// 通过对象工厂类获取laowang对象
Agent laowang = (Laowang)ObjectFactory.getBean("laowang");
laowang.sing();
System.out.println(laowang);
Agent laowang1 = (Laowang)ObjectFactory.getBean("laowang");
System.out.println(laowang1);
}
}
- 注意在main中,我们二次调用getBean去获取laowang对象。运行得到:
Meili is singing.
com.agent.impl.Laowang@74a14482
com.agent.impl.Laowang@74a14482
- laowang和laowang1这两个变量的地址一模一样,可见此时我们获得的都是同一个对象
- 通过这个例子,联想到spring,大家应该可以猜得到:
- 在某方面来说,spring就是一个帮我们创建对象并放入容器中保管的工厂
3、小结
- 由以上例子可知,我们把创建对象的权力交给了工厂
- 而当spring出现成为这个工厂时,我们就把创建对象的权力交给了spring
- 这就是spring中的IOC,控制反转
- 我们把对对象的控制,反转交给了spring来负责,而不是我们自己
- 拥有了spring这个工具,我们就可以专注于系统逻辑的编写
- 高效简洁的编写出低耦合的优质代码
- 当然又有人要提问了
- 配置中的每个数据类型都只建了一份,那我们就是想要建多份怎么办?
- 可能还有别的问题等等
- 但是,都不用担心!这些要求spring都能满足你!
- 本篇就不扩展讲了,能大概理解到spring的作用和优势就OK