1、策咯模式
在面试字节跳动的时候,面试官就让我用代码实现一下策略模式,说实话,当时我是很慌张的。在面试深信服的时候面试官也问了一下设计模式的单例模式,那时候我还不是很懂设计模式,然后我就把《First Head设计模式》给啃完了,但是就是简单过了一遍。这次面试字节跳动的时候又来,干…(省略内心1000字的问候)但是还是要好好写。
首先我们要知道策略模式是干了一件什么事情?
- 策咯模式定义了算法族,分别封装起来,让他们之间可以相互替换,此模式让算法的变化独立于使用算法的客户
简单来说就是,我针对某一个事件,定义了很多解决方案,当面对不同的事件的时候能选取不同的方案去解决这个事情。那么问题来了,很多的不同方案要方便管理啊,也就是说我要他们都满足于同一个规则,这样方便我管理和替换啊。(你可以理解为这个事件为一台PC机,而解决方案为U盘,那么U盘要使用的话,必须是USB接口)
理解了这个的话就很容易了,接下来就是实现代码了:
interface USB_Interface
{
void event();
}
class PC
{
USB_Interface usb;
//构造方法,传进来到底要使用哪一个U盘
public PC(USB_Interface usb)
{
this.usb = usb;
}
void use()
{
//使用U盘里面的事件,这里要去找具体传进来的类实现的event方法
//(也就是这个设计让策略可以实现替换)
usb.event();
}
}
/**
创建一个U盘,这个U盘必须实现USB接口,否则就不能被PC机使用
*/
class UP implments USB_Interface
{
//具体的实现方法
void event()
{
System.out.println("我是第一个U盘,我里面存了一个txt文档");
}
}
class Test
{
public static void main(String[] args)
{
//首先创建一个U盘
UP up = new UP();
//实例化一个PC机,并把U盘传进去
PC pc = new PC(up);
pc.use();
//当然了,你也可以不在初始化PC机的时候就把U盘插进去,你可以设置U盘的
//这样做的话,就写一个默认的构造函数,另外写一个set函数,先调用set函数在调用use
/**
PC pc = new PC();
pc.setUP(up);
pc.use();
*/
}
}
2、单例模式
这个应该是面试中最经常考的一个模式了,我层在深信服的面试中被问到了单例模式能用在什么场景,奈何那个时候我刚刚午睡醒来,脑子都是懵的。其实我也是对这个模式一知半解,所以有了上面的一幕,我把First Head设计模式看完了,然后发现到现在还是一知半解,所以这东西还是要用自己的话语说一遍才能,不理解的知识永远就不是自己的
同样的,我们也要了解一下单例模式是用来干嘛的?
- 单例模式是指在内存中只会创建出而且仅创建一次的设计模式
那么单例模式在什么情况下使用呢?
- 有频繁实例化然后销毁的情况,也就是频繁的 new 对象,可以考虑单例模式;
- 创建对象时耗时过多或者耗资源过多,但又经常用到的对象;
- 频繁访问 IO 资源的对象,例如数据库连接池或访问本地文件;
单例模式有什么实际应用场景?
- 数据库连接池,创建数据库连接时一个很费时间很费操作的事情,我们通过创建很多数据库连接的线程放在数据库连接池里面,当我们要用的时候去线程池里面取。如果我们数据库连接池不是使用单例模式的话,就会有很多线程池,这样线程池的作用就体现不出来了
- 还有就是各种准确的计数器了,什么程序计数器,网站在线人数的计数器,如果有多个计数器的话就没意义了
单例模式还分两种模式
- 懒汉式:在需要用到的时候才创建给你
- 饿汉式:类加载的时候就给你创建好了,不管你会不会使用
接下来就懒汉式和饿汉式的单例模式来分别讲解一下
(1)懒汉式
我们上面讲了,懒汉式就是这个人很懒,只有当我们需要使用的时候才给我们创建对象,那么根据这个思想,我们可以写出这个代码
public class Singleton
{
private static Singleton singleton;
//单例模式的构造器一定要设置为私有,只有自己能创建对象,外面的人别想创建对象
//那我们怎么提供一个对象呢?
//我们可以提供一个函数接口啊,想要对象?你必须通过我的接口
//(你个程序员,想要对象?来相亲网站吧,不然我就不给你)
private Singleton ()
{
//....
}
public static Singleton getInstance()
{
//我们给出一个对象的时候,我们先判断一个实例到底有没有创建啊
if(null == singleton)
{
//不存在的话,我就要给你整一个对象
//1
singleton = new Singleton();//2
return singleton;
}
//如果存在了,我就直接把对象给你
return singleton;
}
}
这就是一个最简单的单例模式了,但是这个单例模式存在一些问题。试想一下,如果我有两个人同时请求获取这个对象的话,会发生什么事情?
张三 | 李四 |
---|---|
我是张三,我想要一个对象,所以我来到相亲网站去getInstance()获取一个对象 | |
当程序执行到2之前,也就是1的位置的时候,张三因为一点事情耽误了一下,所以就中断了一小会,此时李四就像一个程咬金一样,半路杀出来,说我也要一个对象 | 我是李四,我想要一个对象,所以我来到相亲网站去getInstance()获取一个对象 |
此时张三的对象还没有创建出来,李四在条件判断的时候,判断对象没有啊,那我就要创建一个给他 | |
于是程序创建一个对象A给李四 | |
张三此时忙完了自己的事情,继续索要对象,于是从1处继续执行 | |
然后程序又创建了一个对象B给张三 |
咦,是不是有点不对劲啊?我明明就是单例模式啊,怎么会创建出两个对象来呢?
这不行啊,我要查一下课本,然后发现了一个同步锁synchronized,这样我就可以大大方方的创建了吧,毕竟这是同步锁啊,只能又一个人进去创建对象,于是我改…
package c_Singleton;
/**
*@time 2020年8月19日:上午10:52:27
*@author Weirdo
*@version 1.0
**/
public class Sington_L {
private static Sington_L singleton;
//单例模式的构造器一定要设置为私有,只有自己能创建对象,外面的人别想创建对象
//那我们怎么提供一个对象呢?
//我们可以提供一个函数接口啊,想要对象?你必须通过我的接口
//(你个程序员,想要对象?来相亲网站吧,不然我就不给你)
private Sington_L ()
{
//....
}
public static Sington_L getInstance()
{
//我们给出一个对象的时候,我们先判断一个实例到底有没有创建啊
//我在外面加一个锁
synchronized(Sington_L.class)
{
if(null == singleton)
{
//不存在的话,我就要给你整一个对象
//1
singleton = new Sington_L();//2
return singleton;
}
}
//如果存在了,我就直接把对象给你
return singleton;
}
}
package c_Singleton;
/**
*@time 2020年8月19日:上午10:49:23
*@author Weirdo
*@version 1.0
**/
public class Main {
public static void main(String[] args) {
Sington_L a = Sington_L.getInstance();
Sington_L b = Sington_L.getInstance();
System.out.println(a==b);
//true
}
}
对了,这样就万无一失了,一次只能有一个人进去创建对象,等你创建出来的时候,另外一个人再去请求对象,是不是很完美?
但是你有没有发现,这样子用同步锁会发生一个很大的问题,就是我们要限制的只是创建对象这个操作,而我却把这个判断的动作都锁住了,。试着想一下,我是要去相亲的,那么大的一家相亲网站,为了不出错,把当一个人在相亲的时候,网站卡住了,不能用了要等这个人相亲完出来才能用,然后用户找到你说:“什么垃圾玩意,卡死了”。然后你就收拾收拾回家放牛去吧。
那有没有解决方案?
- 既然我这样子做会锁住整个过程,那我把锁细化一下行不行?我就仅仅锁住创建对象这个操作,如果判断对象已经存在的话,直接给你返回一个;不存在的话,对不起没得商量,你还是要乖乖排队
好,既然这样子的话,我又改改改…一顿操作
public class Singleton
{
private static Singleton singleton;
//单例模式的构造器一定要设置为私有,只有自己能创建对象,外面的人别想创建对象
//那我们怎么提供一个对象呢?
//我们可以提供一个函数接口啊,想要对象?你必须通过我的接口
//(你个程序员,想要对象?来相亲网站吧,不然我就不给你)
private Singleton ()
{
//....
}
public static Singleton getInstance()
{
//我们给出一个对象的时候,我们先判断一个实例到底有没有创建啊
if(null == singleton)
{
//1
synchronized(singleton)
{
//不存在的话,我就要给你整一个对象
//2
singleton = new Singleton();//3
return singleton;
}
//4
}
//如果存在了,我就直接把对象给你
return singleton;
}
}
好的,这时又来了两个人张三和李四
张三 | 李四 |
---|---|
我是张三,我想要一个对象,所以我来到相亲网站去getInstance()获取一个对象 | |
于是张三进来了1这个,发现有一个门,这个门有个限制,仅仅能进去一个人,张三发现里面没人,我可以进去 | |
于是张三来到了2,然后张三又是屁事多,有什么事情耽误了一下,没有创建出对象来,此时李四又来了 | 我是李四,我想要一个对象,所以我来到相亲网站去getInstance()获取一个对象 |
在判断的时候发现没有对象存在,那我就进来1,但是发现有一个门限制,仅仅能进去一个人,里面已经有人了,那我就等吧 | |
此时张三的屁事搞完了,继续创建对象,张三于是拿到了一个对象A,屁颠屁颠走了,此时门限制解除了 | 李四看到门限制解除了,那我就进去呗 |
然后李四又新建了一个对象B,对着新的对象跑了 |
此时你又发现了,不对啊,怎么我这个锁加了和没加一样啊,加在外面整个过程锁住了,老板炒我鱿鱼,不满足单例模式又炒鱿鱼,我真的是太难了。
单例模式是不可能放弃的,性能也是需要的,那我怎么权衡呢?你想着想着的时候迷迷糊糊的睡着了
- 晚上周公不忍看你失业,于是托梦给你。在梦里大骂你一顿说:小孩子才做选择,单例和性能我全都要,你在锁里面再判断一次对象有没有存在不就行了?我们称之为双重检查锁机制
突然的惊醒,赶紧把周公的教诲记下来,于是又一顿改…
public class Singleton
{
//volatile关键字
private static volatile Singleton singleton;
//单例模式的构造器一定要设置为私有,只有自己能创建对象,外面的人别想创建对象
//那我们怎么提供一个对象呢?
//我们可以提供一个函数接口啊,想要对象?你必须通过我的接口
//(你个程序员,想要对象?来相亲网站吧,不然我就不给你)
private Singleton ()
{
//....
}
public static Singleton getInstance()
{
//我们给出一个对象的时候,我们先判断一个实例到底有没有创建啊
if(null == singleton)
{
//确保只有一个人能进去
synchronized(singleton)
{
//进来之后再判断,有没有对象
//不存在的话,我就要给你整一个对象
if(null == singleton)
{
singleton = new Singleton();//3
return singleton;
}
//如果存在的话,我直接返回
return singleton;
}
//4
}
//如果存在了,我就直接把对象给你
return singleton;
}
}
这个方案已经很接近正确的解决方案了,还差最后一个,volatile关键字。
volatile关键字
- 能够禁止指令重排
- 能使得修改的内容马上让别的在使用这个变量的人感知(这里涉及到JVM的知识,往后会慢慢展开,现在记得这个就好)
(2)饿汉式
类加载的时候就给你创建好了,不管你会不会使用
package c_Singleton;
/**
*@time 2020年8月19日:上午10:48:38
*@author Weirdo
*@version 1.0
**/
public class Sington_E {
//我在初始化的时候就给你来个对象,你要我就返回,就算你不要,我也创建出来,简单粗暴
private final static Sington_E singleton = new Sington_E();
private Sington_E ()
{
//....
}
public static Sington_E getInstance()
{
return singleton;
}
}
package c_Singleton;
/**
*@time 2020年8月19日:上午10:49:23
*@author Weirdo
*@version 1.0
**/
public class Main {
public static void main(String[] args) {
Sington_E a = Sington_E.getInstance();
Sington_E b = Sington_E.getInstance();
System.out.println(a==b);
//true
}
}
3、观察者模式
观察者模式的概念?
- 观察者模式定义了对象之间的一多依赖,这样一来,当一个对象改变状态时,它的所有依赖着都会收到通知并自动更新
观察者模式一般使用于什么场景?
- 在我的理解中,观察者模式就像微信用户和公众号的关系,是一种订阅与被订阅的关系。当我订阅一个公众号的时候,公众号会把推文更新给订阅的人,当然我也可以取消订阅,这样我就可以不接受推文信息。
那我们想一下,怎么设计一个观察者模式,来让我们的系统变得容易拓展呢?也就是低耦合
- 首先设计肯定是面向接口的,我们需要设计一个被订阅的接口(公众号)和一个订阅者的接口(用户)
- 这样我们就可以通过实现接口来做出多个公众号还有用户
- 还需要考虑的问题就是,我怎么让公众号也用户之间产生关联?
综合以上,我们可以得出,必须设计一个被订阅的接口(公众号),他必须有三个方法
- 订阅的接口
- 取消订阅的接口
- 发布信息的接口
设计一个订阅者的接口(用户),他必须有一个方法
- 接收信息的接口
public interface PublicAble
{
//接收一个用户参数
void register(UserAble user);
//接收一个用户参数
void unRegister(UserAble user);
void notify(String info);
}
public interface UserAble
{
void receive();
}
那现在我们有一个规范了,我们尝试来实现一下观察者模式吧(公众号—用户)
//公众号
public interface PublicAble
{
//接收一个用户参数
void register(UserAble user);
//接收一个用户参数
void unRegister(UserAble user);
//接收要发送的信息
void notify(String info);
}
//用户
public interface UserAble
{
void receive(String info);
}
public class Public implements PublicAble
{
//使用list集合来存储订阅的用户
List<UserAble> users;
public Public()
{
users = new ArraysList<>();
}
//接收一个用户参数
void register(UserAble user)
{
users.add(user);
}
//接收一个用户参数
void unRegister(UserAble user)
{
users.remove(users.indexOf(user));
}
void notify(String info)
{
for(int i=0;i<users.length;i++)
{
users.get(i).receive(info);
}
}
}
public class User implements UserAble
{
String name;
String ID;
public User()
{
//..
}
void receive(String info)
{
System.out.println("我是"+ID+"号"+name+",我接受到信息:"+info);
}
}
public class Main
{
public static void main(String[] args)
{
//新建一个公众号实例
Public publicer = new public();
//新建两个用户
User user1 = new User();
User user2 = new User();
//两个用户订阅公众号
publicer.register(user1);
publicer.register(user2);
//通过公众号发布信息
publicer.notify("推文信息")
//用户2取消订阅
publicer.remove(user2);
//通过公众号发布信息
publicer.notify("推文信息")
}
}
其实我感觉观察者模式有点像策略模式的,只不过策略模式是一个对象属性(当然了,也可以是多个对象属性),这里的是对象的集合,其实原理都差不多
4、装饰者模式
按照惯例,我们先来了解一下装饰着模式干了一件什么事情?
- 动态的将责任附加到对象上,若要拓展功能,装饰着提供了比继承更有弹性的替代方案
- 说白了,就是在给一个东西装上了一个外壳,穿了一件衣服,在原有的基础之上有了更多的功能,我也将这个理解为组合
这里使用同样使用First Head里的例子,奶茶店加料。首先抽象一个奶茶和要加的材料之间有什么共同点。首先肯定是名字,也就是描述description,然后就是价格price
那么:
package d_decoration;
/**
*@time 2020年8月19日:下午3:40:23
*@author Weirdo
*@version 1.0
**/
public abstract class Common {
String description = "unknow";
public String getDescriprion() {
return description;
}
public abstract double cost();
}
对于调味料,我们可以更加具体一点,让我们知道编写的类是调料
package d_decoration;
/**
*@time 2020年8月19日:下午3:46:40
*@author Weirdo
*@version 1.0
**/
public abstract class CondimentDecorator extends Common{
public abstract String getDescriprion();
}
以上工作准备好了之后,我们要想一下,装饰着模式的核心思想是什么?
- 我的理解就是,用一个对象封装另外一个对象,从而达到装饰的效果
所以,我们开始编写咖啡类,注意:这里的咖啡是没有添加任何东西的,最原生的咖啡
package d_decoration;
/**
*@time 2020年8月19日:下午3:48:30
*@author Weirdo
*@version 1.0
**/
public class Espresso extends Common{
public Espresso() {
description = "Espresso";
}
@Override
public double cost() {
// TODO Auto-generated method stub
return 1.99;
}
}
package d_decoration;
/**
*@time 2020年8月19日:下午3:51:55
*@author Weirdo
*@version 1.0
**/
public class HouseBlend extends Common{
public HouseBlend() {
// TODO Auto-generated constructor stub
description = "HouseBlend";
}
@Override
public double cost() {
// TODO Auto-generated method stub
return 0.89;
}
}
然后我们再来编写调料类,记得我们上面将的,装饰着模式就是通过一个对象把另外一个对象封装起来
那么我们已经封装好了一个咖啡的类了,我们要做的就是是用调料的类来封装一下咖啡的类
package d_decoration;
/**
*@time 2020年8月19日:下午3:54:34
*@author Weirdo
*@version 1.0
**/
public class Mocha extends CondimentDecorator{
public Common common;
public Mocha(Common common) {
this.common = common;
}
@Override
public String getDescriprion() {
// TODO Auto-generated method stub
return common.getDescriprion()+", Mocha";
}
@Override
public double cost() {
// TODO Auto-generated method stub
return common.cost()+.20;
}
}
package d_decoration;
/**
*@time 2020年8月19日:下午3:58:24
*@author Weirdo
*@version 1.0
**/
public class Pearl extends CondimentDecorator{
public Common common;
public Pearl(Common common) {
this.common = common;
}
@Override
public String getDescriprion() {
// TODO Auto-generated method stub
return common.getDescriprion()+", Pearl";
}
@Override
public double cost() {
// TODO Auto-generated method stub
return common.cost()+.80;
}
}
不要忘记了一个事实,我们所有的编码其实都有一个最原始的父类,也就是Common抽象类,也就是说,不管是咖啡还是调味料,其本质都是Common
接下来我们测试一下代码
package d_decoration;
/**
*@time 2020年8月19日:下午3:59:05
*@author Weirdo
*@version 1.0
**/
public class Main {
public static void main(String[] args) {
//点一杯咖啡
Common espresso = new Espresso();
//加一次Mocha
Common addMocha1 = new Mocha(espresso);
//再加一次Mocha
Common addMocha2 = new Mocha(addMocha1);
//输出
System.out.println(addMocha2.getDescriprion());
System.out.println(addMocha2.cost());
//点一杯原生的
espresso = new Espresso();
//输出
System.out.println(addMocha1.getDescriprion());
System.out.println(addMocha1.cost());
}
}