有限状态机的4种Java实现对比

写在前面:2020年面试必备的Java后端进阶面试题总结了一份复习指南在Github上,内容详细,图文并茂,有需要学习的朋友可以Star一下!
GitHub地址:https://github.com/abel-max/Java-Study-Note/tree/master

在日常工作过程中,我们经常会遇到状态的变化场景,例如订单状态发生变化,商品状态的变化。这些状态的变化,我们称为有限状态机,缩写为FSM( F State Machine).。之所以称其为有限,是因为这些场景中的状态往往是可以枚举出来的有限个的,所以称其为有限状态机。下面我们来看一个具体的场景例子。

简单场景:

地铁进站闸口的状态有两个:已经关闭、已经开启两个状态。刷卡后闸口从已关闭变为已开启,人通过后闸口状态从已开启变为已关闭。

01 遇到这类问题,在编码时我们应该如何处理呢?

  • 基于Switch
  • 基于状态集合
  • 基于State模式
  • 基于枚举的实现

下面我们针对每一种实现方式进行分析。场景分解后会有一下2种状态4种情况出现:

image.png

针对以上4种请求,共拆分了5个Test Case

T01

Given:一个Locked的进站闸口
When: 投入硬币
Then:打开闸口

T02

Given:一个Locked的进站闸口
When: 通过闸口
Then:警告提示

T03

Given:一个Unocked的进站闸口
When: 通过闸口
Then:闸口关闭

T04

Given:一个Unlocked的进站闸口
When: 投入硬币
Then:退还硬币

T05

Given:一个闸机口
When: 非法操作
Then:操作失败

项目中共有4中状态机的实现方式。

  • 基于Switch语句实现的有限状态机,代码在 master 分支

  • 基于State模式实现的有限状态机。代码在 state-pattern 分支

  • 基于状态集合实现的有限状态机。代码在 collection-state 分支

  • 基于枚举实现的状态机。代码在 enum-state 分支

01.01 使用Switch来实现有限状态机

这种方式只需要懂得Java语法及可以实现出来。先看代码,然后我们在讨论这种实现方式是否好。

EntranceMachineTest.java

package com.page.java.fsm;

import com.page.java.fsm.exception.InvalidActionException;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.BDDAssertions.then;

class EntranceMachineTest {

    @Test
    void should_be_unlocked_when_insert_coin_given_a_entrance_machine_with_locked_state() {
        EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);

        String result = entranceMachine.execute(Action.INSERT_COIN);

        then(result).isEqualTo("opened");
        then(entranceMachine.getState()).isEqualTo(EntranceMachineState.UNLOCKED);
    }

    @Test
    void should_be_locked_and_alarm_when_pass_given_a_entrance_machine_with_locked_state() {
        EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);

        String result = entranceMachine.execute(Action.PASS);

        then(result).isEqualTo("alarm");
        then(entranceMachine.getState()).isEqualTo(EntranceMachineState.LOCKED);
    }

    @Test
    void should_fail_when_execute_invalid_action_given_a_entrance_with_locked_state() {
        EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);

        assertThatThrownBy(() -> entranceMachine.execute(null))
                .isInstanceOf(InvalidActionException.class);
    }

    @Test
    void should_locked_when_pass_given_a_entrance_machine_with_unlocked_state() {
        EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.UNLOCKED);

        String result = entranceMachine.execute(Action.PASS);

        then(result).isEqualTo("closed");
        then(entranceMachine.getState()).isEqualTo(EntranceMachineState.LOCKED);
    }

    @Test
    void should_refund_and_unlocked_when_insert_coin_given_a_entrance_machine_with_unlocked_state() {
        EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.UNLOCKED);

        String result = entranceMachine.execute(Action.INSERT_COIN);

        then(result).isEqualTo("refund");
        then(entranceMachine.getState()).isEqualTo(EntranceMachineState.UNLOCKED);
    }
}

Action.java

public enum Action {
    INSERT_COIN,
    PASS
}

EntranceMachineState.java

public enum EntranceMachineState {
    UNLOCKED,
    LOCKED
}

InvalidActionException.java

package com.page.java.fsm.exception;
public class InvalidActionException extends RuntimeException {
}

EntranceMachine.java

package com.page.java.fsm;
import com.page.java.fsm.exception.InvalidActionException;
import lombok.Data;
import java.util.Objects;

@Data
public class EntranceMachine {

    private EntranceMachineState state;

    public EntranceMachine(EntranceMachineState state) {
        this.state = state;
    }

    public String execute(Action action) {
        if (Objects.isNull(action)) {
            throw new InvalidActionException();
        }

        if (EntranceMachineState.LOCKED.equals(state)) {
            switch (action) {
                case INSERT_COIN:
                    setState(EntranceMachineState.UNLOCKED);
                    return open();
                case PASS:
                    return alarm();
            }
        }

        if (EntranceMachineState.UNLOCKED.equals(state)) {
            switch (action) {
                case PASS:
                    setState(EntranceMachineState.LOCKED);
                    return close();
                case INSERT_COIN:
                    return refund();
            }
        }
        return null;
    }

    private String refund() {
        return "refund";
    }

    private String close() {
        return "closed";
    }

    private String alarm() {
        return "alarm";
    }

    private String open() {
        return "opened";
    }
}

if(), swich语句都是switch语句,但是 Switch是一种Code Bad Smell ,因为它本质上一种重复。当代码中有多处相同的switch时,会让系统变得晦涩难懂,脆弱,不易修改。

上面的代码虽然出现了多层嵌套但是还算是结构简单,不过想通过并不能很清楚闸机口的逻辑还是化点时间。如果闸机口的状态等多一些,那就阅读、理解起来也就更加困难。

所以在日常工作,我遵循“事不过三,三则重构”的原则:

事不过三:

当只有一两个状态(或者重复)时,那么先用最简单的实现实现。

一旦出现三种以及以上的状态(或者重复),立即重构。

01.02 State模式

EntranceMachineTest.java

package com.page.java.fsm;

import com.page.java.fsm.exception.InvalidActionException;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.BDDAssertions.then;

class EntranceMachineTest {

    @Test
    void should_be_unlocked_when_insert_coin_given_a_entrance_machine_with_locked_state() {
        EntranceMachine entranceMachine = new EntranceMachine(new LockedEntranceMachineState());

        String result = entranceMachine.execute(Action.INSERT_COIN);

        then(result).isEqualTo("opened");
        then(entranceMachine.isUnlocked()).isTrue();
    }

    @Test
    void should_be_locked_and_alarm_when_pass_given_a_entrance_machine_with_locked_state() {
        EntranceMachine entranceMachine = new EntranceMachine(new LockedEntranceMachineState());

        String result = entranceMachine.execute(Action.PASS);

        then(result).isEqualTo("alarm");
        then(entranceMachine.isLocked()).isTrue();
    }

    @Test
    void should_fail_when_execute_invalid_action_given_a_entrance_with_locked_state() {
        EntranceMachine entranceMachine = new EntranceMachine(new LockedEntranceMachineState());

        assertThatThrownBy(() -> entranceMachine.execute(null))
                .isInstanceOf(InvalidActionException.class);
    }

    @Test
    void should_locked_when_pass_given_a_entrance_machine_with_unlocked_state() {
        EntranceMachine entranceMachine = new EntranceMachine(new UnlockedEntranceMachineState());

        String result = entranceMachine.execute(Action.PASS);

        then(result).isEqualTo("closed");
        then(entranceMachine.isLocked()).isTrue();
    }

    @Test
    void should_refund_and_unlocked_when_insert_coin_given_a_entrance_machine_with_unlocked_state() {
        EntranceMachine entranceMachine = new EntranceMachine(new UnlockedEntranceMachineState());

        String result = entranceMachine.execute(Action.INSERT_COIN);

        then(result).isEqualTo("refund");
        then(entranceMachine.isUnlocked()).isTrue();
    }
}
EntranceMachineState.java
package com.page.java.fsm;

public interface EntranceMachineState {

    String insertCoin(EntranceMachine entranceMachine);

    String pass(EntranceMachine entranceMachine);
}

LockedEntranceMachineState.java

package com.page.java.fsm;

public class LockedEntranceMachineState implements EntranceMachineState {

    @Override
    public String insertCoin(EntranceMachine entranceMachine) {
        return entranceMachine.open();
    }

    @Override
    public String pass(EntranceMachine entranceMachine) {
        return entranceMachine.alarm();
    }
}

UnlockedEntranceMachineState.java

package com.page.java.fsm;

public class UnlockedEntranceMachineState implements EntranceMachineState {

    @Override
    public String insertCoin(EntranceMachine entranceMachine) {
        return entranceMachine.refund();
    }

    @Override
    public String pass(EntranceMachine entranceMachine) {
        return entranceMachine.close();
    }
}

Action.java

package com.page.java.fsm;

public enum Action {
    PASS,
    INSERT_COIN
}
EntranceMachine.java
package com.page.java.fsm;

import com.page.java.fsm.exception.InvalidActionException;

import java.util.Objects;

public class EntranceMachine {

    private EntranceMachineState locked = new LockedEntranceMachineState();

    private EntranceMachineState unlocked = new UnlockedEntranceMachineState();

    private EntranceMachineState state;

    public EntranceMachine(EntranceMachineState state) {
        this.state = state;
    }

    public String execute(Action action) {
        if (Objects.isNull(action)) {
            throw new InvalidActionException();
        }

        if (Action.PASS.equals(action)) {
            return state.pass(this);
        }

        return state.insertCoin(this);
    }

    public boolean isUnlocked() {
        return state == unlocked;
    }

    public boolean isLocked() {
        return state == locked;
    }

    public String open() {
        setState(unlocked);
        return "opened";
    }

    public String alarm() {
        setState(locked);
        return "alarm";
    }

    public String refund() {
        setState(unlocked);
        return "refund";
    }

    public String close() {
        setState(locked);
        return "closed";
    }

    private void setState(EntranceMachineState state) {
        this.state = state;
    }
}

State模式和Proxy模式类似,但是在State模式中EntranceMachineState持有EntranceMachine实例的引用。

我们发现EntranceMachine的execute()方法的逻辑变的简单,但是代码复杂度升高了。因为每个state实例都提供了两个动作实现insertCoin()和pass()。这个地方本人认为并不够表意,因为作出的动作被添加到两个状态上,虽然能够实现业务业务,但是并不利于理解清楚业务意思。

State模式,虽然能够将逻辑进行拆分,但是那些状态的顺序,以及有几种状态,都不是很直观的观察到。

不过在实际业务中,State模式也是一种很好的实现方式,毕竟他避免了switch的堆积问题。

01.03 使用状态集合

状态集合是将一组描述状态变化的事务元素组成的集合。

集合中的每一个元素包含4个属性:当前的状态,事件,下一个状态,触发的动作。

使用时遍历集合根据动作找到特定的元素,并更具元素上的属性和事件来完成业务逻辑。

具体代码如下:

EntranceMachineTest.java

package com.page.java.fsm;

import com.page.java.fsm.exception.InvalidActionException;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.BDDAssertions.then;

class EntranceMachineTest {

    @Test
    void should_be_unlocked_when_insert_coin_given_a_entrance_machine_with_locked_state() {
        EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);

        String result = entranceMachine.execute(Action.INSERT_COIN);

        then(result).isEqualTo("opened");
        then(entranceMachine.getState()).isEqualTo(EntranceMachineState.UNLOCKED);
    }

    @Test
    void should_be_alarm_when_pass_given_a_entrance_machine_with_locked_state() {
        EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);

        String result = entranceMachine.execute(Action.PASS);

        then(result).isEqualTo("alarm");
        then(entranceMachine.getState()).isEqualTo(EntranceMachineState.LOCKED);
    }

    @Test
    void should_fail_when_execute_invalid_action_given_a_entrance_machine() {
        EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);

        assertThatThrownBy(() -> entranceMachine.execute(null))
                .isInstanceOf(InvalidActionException.class);

    }

    @Test
    void should_closed_when_pass_given_a_entrance_machine_with_unlocked() {
        EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.UNLOCKED);

        String result = entranceMachine.execute(Action.PASS);

        then(result).isEqualTo("closed");
        then(entranceMachine.getState()).isEqualTo(EntranceMachineState.LOCKED);
    }

    @Test
    void should_refund_when_insert_coin_given_a_entrance_machine_with_unlocked() {
        EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.UNLOCKED);

        String result = entranceMachine.execute(Action.INSERT_COIN);

        then(result).isEqualTo("refund");
        then(entranceMachine.getState()).isEqualTo(EntranceMachineState.UNLOCKED);
    }
}
Action.java
package com.page.java.fsm;

public enum Action {
    PASS,
    INSERT_COIN
}

EntranceMachineState.java

package com.page.java.fsm;

public enum EntranceMachineState {
    LOCKED,
    UNLOCKED
}

EntranceMachine.java

package com.page.java.fsm;

import com.page.java.fsm.events.AlarmEvent;
import com.page.java.fsm.events.CloseEvent;
import com.page.java.fsm.events.OpenEvent;
import com.page.java.fsm.events.RefundEvent;
import com.page.java.fsm.exception.InvalidActionException;
import lombok.Data;

import java.util.Arrays;
import java.util.List;
import java.util.Optional;

@Data
public class EntranceMachine {

    List<EntranceMachineTransaction> entranceMachineTransactionList = Arrays.asList(
            EntranceMachineTransaction.builder()
                    .currentState(EntranceMachineState.LOCKED)
                    .action(Action.INSERT_COIN)
                    .nextState(EntranceMachineState.UNLOCKED)
                    .event(new OpenEvent())
                    .build(),
            EntranceMachineTransaction.builder()
                    .currentState(EntranceMachineState.LOCKED)
                    .action(Action.PASS)
                    .nextState(EntranceMachineState.LOCKED)
                    .event(new AlarmEvent())
                    .build(),
            EntranceMachineTransaction.builder()
                    .currentState(EntranceMachineState.UNLOCKED)
                    .action(Action.PASS)
                    .nextState(EntranceMachineState.LOCKED)
                    .event(new CloseEvent())
                    .build(),
            EntranceMachineTransaction.builder()
                    .currentState(EntranceMachineState.UNLOCKED)
                    .action(Action.INSERT_COIN)
                    .nextState(EntranceMachineState.UNLOCKED)
                    .event(new RefundEvent())
                    .build()
    );

    private EntranceMachineState state;

    public EntranceMachine(EntranceMachineState state) {
        setState(state);
    }

    public String execute(Action action) {
        Optional<EntranceMachineTransaction> transactionOptional = entranceMachineTransactionList
                .stream()
                .filter(transaction ->
                        transaction.getAction().equals(action) && transaction.getCurrentState().equals(state))
                .findFirst();

        if (!transactionOptional.isPresent()) {
            throw new InvalidActionException();
        }

        EntranceMachineTransaction transaction = transactionOptional.get();
        setState(transaction.getNextState());
        return transaction.getEvent().execute();
    }
}

EntranceMachineTransaction.java

package com.page.java.fsm;

import com.page.java.fsm.events.Event;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class EntranceMachineTransaction {

    private EntranceMachineState currentState;

    private Action action;

    private EntranceMachineState nextState;

    private Event event;
}

Event.java

package com.page.java.fsm.events;

public interface Event {

    String execute();
}

OpenEvent.java

package com.page.java.fsm.events;

public class OpenEvent implements Event {
    @Override
    public String execute() {
        return "opened";
    }
}

AlarmEvent.java

package com.page.java.fsm.events;

public class AlarmEvent implements Event {
    @Override
    public String execute() {
        return "alarm";
    }
}

CloseEvent.java

package com.page.java.fsm.events;

public class CloseEvent implements Event {
    @Override
    public String execute() {
        return "closed";
    }
}

RefundEvent.java

package com.page.java.fsm.events;

public class RefundEvent implements Event {
    @Override
    public String execute() {
        return "refund";
    }
}

InvalidActionException.java

package com.page.java.fsm.exception;

public class InvalidActionException extends RuntimeException {
}

相比于Switch的实现方式,状态集合的实现方式对状态规则的描述更加直观。且扩展性更强,不需求修改实现路基,只需要添加相关的状态描述即可。

我们知道日常工作中读代码和写代码比例在10:1,有些场景下甚至到了20:1。Switch需要我们每次在脑子中组织一次状态的顺序和规则,而集合能够很直观的表达出这个规则。

01.04 使用Enum的来实现状态机

EntranceMachineTest.java

package com.page.java.fsm;

import com.page.java.fsm.exception.InvalidActionException;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.assertj.core.api.BDDAssertions.then;

class EntranceMachineTest {

    @Test
    void should_unlocked_when_insert_coin_given_a_entrance_machine_with_locked_state() {
        EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);

        String result = entranceMachine.execute(Action.INSERT_COIN);

        then(result).isEqualTo("opened");
        then(entranceMachine.getState()).isEqualTo(EntranceMachineState.UNLOCKED);
    }

    @Test
    void should_alarm_when_pass_given_a_entrance_machine_with_locked_state() {
        EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);

        String result = entranceMachine.execute(Action.PASS);

        then(result).isEqualTo("alarm");
        then(entranceMachine.getState()).isEqualTo(EntranceMachineState.LOCKED);
    }

    @Test
    void should_fail_when_execute_invalid_action_given_a_entrance_machine() {
        EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.LOCKED);

        assertThatThrownBy(() -> entranceMachine.execute(null))
                .isInstanceOf(InvalidActionException.class);
    }

    @Test
    void should_refund_when_insert_coin_given_a_entrance_machine_with_unlocked_state() {
        EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.UNLOCKED);

        String result = entranceMachine.execute(Action.INSERT_COIN);

        then(result).isEqualTo("refund");
        then(entranceMachine.getState()).isEqualTo(EntranceMachineState.UNLOCKED);
    }

    @Test
    void should_closed_when_pass_given_a_entrance_machine_with_unlocked_state() {
        EntranceMachine entranceMachine = new EntranceMachine(EntranceMachineState.UNLOCKED);

        String result = entranceMachine.execute(Action.PASS);

        then(result).isEqualTo("closed");
        then(entranceMachine.getState()).isEqualTo(EntranceMachineState.LOCKED);

    }

}

EntraceMachine.java

package com.page.java.fsm;

import com.page.java.fsm.exception.InvalidActionException;
import lombok.Data;

import java.util.Objects;

@Data
public class EntranceMachine {

    private EntranceMachineState state;

    public EntranceMachine(EntranceMachineState state) {
        setState(state);
    }

    public String execute(Action action) {
        if (Objects.isNull(action)) {
            throw new InvalidActionException();
        }

        return action.execute(this, state);
    }

    public String open() {
        return "opened";
    }

    public String alarm() {
        return "alarm";
    }

    public String refund() {
        return "refund";
    }

    public String close() {
        return "closed";
    }
}

Action.java

package com.page.java.fsm;

public enum Action {
    PASS {
        @Override
        public String execute(EntranceMachine entranceMachine, EntranceMachineState state) {
            return state.pass(entranceMachine);
        }
    },
    INSERT_COIN {
        @Override
        public String execute(EntranceMachine entranceMachine, EntranceMachineState state) {
            return state.insertCoin(entranceMachine);
        }
    };

    public abstract String execute(EntranceMachine entranceMachine, EntranceMachineState state);
}

EntranceMachineState.java

package com.page.java.fsm;

public enum EntranceMachineState {
    LOCKED {
        @Override
        public String insertCoin(EntranceMachine entranceMachine) {
            entranceMachine.setState(UNLOCKED);
            return entranceMachine.open();
        }

        @Override
        public String pass(EntranceMachine entranceMachine) {
            entranceMachine.setState(this);
            return entranceMachine.alarm();
        }
    },
    UNLOCKED {
        @Override
        public String insertCoin(EntranceMachine entranceMachine) {
            entranceMachine.setState(this);
            return entranceMachine.refund();
        }

        @Override
        public String pass(EntranceMachine entranceMachine) {
            entranceMachine.setState(LOCKED);
            return entranceMachine.close();
        }
    };

    public abstract String insertCoin(EntranceMachine entranceMachine);

    public abstract String pass(EntranceMachine entranceMachine);
}

InvalidActionException.java

package com.page.java.fsm.exception;

public class InvalidActionException extends RuntimeException {
}

通过上面的代码,可以发现Action、EntranceMachineState两个枚举的复杂度都提升了。不单单是定义了常量那么简单。还提供了相应的逻辑处理。

在EntranceMachineState.java的提交记录中,对进行了一次重构,将具体业务逻辑执行移动到EntranceMachine中,EntranceMachineState内每种状态的方法中只负责调度。这样能够通过EntranceMachineState相对直观的看清楚做了什么,状态变成了什么。

缺陷就是,EntranceMachine 对外提供了public的setState方法,这也就意味着调用者在将来维护是,很有可能滥用setState方法。

02 总结

通过上面4中对FSM的实现,我们看到每一种是实现都有优点和它的不足。那么在日常工作中,如何选择呢,我个人认为可以遵循一下两个建议:

  1. 遵循Simple Design。如果没有一个外部参考,那么用哪一种都不为过。所以引入一个原则作为参考,可以更好的帮助我们做决定。这里日常工作中我们经常使用Simple Design:通过测试、揭示意图、消除重复、最少元素。并在实现过程中不断重构,代码是重构出来的,而不是一次性的设计出来的。

  2. 在状态机的实现上多做尝试。例子只是一个简单的场景,所以只能看到简单场景下的实现效果,实际业务线上的状态会非常丰富,而且每种状态中可真行的动作也是不同的。所以针对特定场景遇到的问题,多尝试练习思考,练习思考后的经验才是最重要的。

来源: https://juejin.im/post/5dff7595f265da33d645bc63

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343