Python設(shè)計模式之MVC模式

模型-視圖-控制器模式

關(guān)注點分離(Separation of Concerns,SoC)原則是軟件工程相關(guān)的設(shè)計原則之一铅辞。SoC原則背后的思想是將一個應(yīng)用切分成不同的部分鸠澈,每個部分解決一個單獨的關(guān)注點歇父。分層設(shè)計中的層次(數(shù)據(jù)訪問層派草、業(yè)務(wù)邏輯層和表示層等)即是關(guān)注點的例子舱馅。使用SoC原則能簡化軟件應(yīng)用的開發(fā)和維護(請參考網(wǎng)頁[t.cn/RqrjewK])赘方。

模型—視圖—控制器(Model-View-Controller烧颖,MVC)模式是應(yīng)用到面向?qū)ο缶幊痰腟oc原則。模式的名稱來自用來切分軟件應(yīng)用的三個主要部分窄陡,即模型部分炕淮、視圖部分和控制器部分。MVC被認(rèn)為是一種架構(gòu)模式而不是一種設(shè)計模式跳夭。架構(gòu)模式與設(shè)計模式之間的區(qū)別在于前者比后者的范疇更廣涂圆。然而们镜,MVC太重要了,不能僅因為這個原因就跳過不說润歉。即使我們從不需要從頭實現(xiàn)它模狭,也需要熟悉它,因為所有常見框架都使用了MVC或者是其略微不同的版本(之后會詳述)踩衩。

模型是核心的部分嚼鹉,代表著應(yīng)用的信息本源,包含和管理(業(yè)務(wù))邏輯驱富、數(shù)據(jù)锚赤、狀態(tài)以及應(yīng)用的規(guī)則。視圖是模型的可視化表現(xiàn)褐鸥。視圖的例子有线脚,計算機圖形用戶界面、計算機終端的文本輸出叫榕、智能手機的應(yīng)用圖形界面浑侥、PDF文檔、餅圖和柱狀圖等翠霍。視圖只是展示數(shù)據(jù)锭吨,并不處理數(shù)據(jù)『祝控制器是模型與視圖之間的鏈接/粘附零如。模型與視圖之間的所有通信都通過控制器進(jìn)行(請參考[GOF95,第14頁]、網(wǎng)頁[t.cn/RqrjF4G]和網(wǎng)頁[t.cn/RPrOUPr])锄弱。

對于將初始屏幕渲染給用戶之后使用MVC的應(yīng)用考蕾,其典型使用方式如下所示:

  • 用戶通過單擊(鍵入、觸摸等)某個按鈕觸發(fā)一個視圖
  • 視圖把用戶操作告知控制器
  • 控制器處理用戶輸入会宪,并與模型交互
  • 模型執(zhí)行所有必要的校驗和狀態(tài)改變,并通知控制器應(yīng)該做什么
  • 控制器按照模型給出的指令肖卧,指導(dǎo)視圖適當(dāng)?shù)馗潞惋@示輸出
## 如下所示為來自于github的示例代碼:

#!/usr/bin/env python
# -*- coding: utf-8 -*-

class Model(object):
    def __iter__(self):
        raise NotImplementedError

    def get(self, item):
        """Returns an object with a .items() call method
        that iterates over key,value pairs of its information."""
        raise NotImplementedError

    @property
    def item_type(self):
        raise NotImplementedError


class ProductModel(Model):

    class Price(float):
        """A polymorphic way to pass a float with a particular __str__ functionality."""
        def __str__(self):
            first_digits_str = str(round(self,2))
            try:
                dot_location = first_digits_str.index('.')
            except ValueError:
                return (first_digits_str + '.00')
            else:
                return (first_digits_str +
                               '0'*(3 + dot_location - len(first_digits_str)))

    products = {
        'milk': {'price': Price(1.50), 'quantity': 10},
        'eggs': {'price': Price(0.20), 'quantity': 100},
        'cheese': {'price': Price(2.00), 'quantity': 10}
    }

    item_type = 'product'

    def __iter__(self):
        for item in self.products:
            yield item

    def get(self, product):
        try:
            return self.products[product]
        except KeyError as e:
            raise KeyError((str(e) + " not in the model's item list."))

class View(object):
    def show_item_list(self, item_type, item_list):
        raise NotImplementedError

    def show_item_information(self, item_type, item_name, item_info):
        """Will look for item information by iterating over key,value pairs
        yielded by item_info.items()"""
        raise NotImplementedError

    def item_not_found(self, item_type, item_name):
        raise NotImplementedError

class ConsoleView(View):

    def show_item_list(self, item_type, item_list):
        print(item_type.upper() + ' LIST:')
        for item in item_list:
            print(item)
        print('')

    @staticmethod
    def capitalizer(string):
        return string[0].upper() + string[1:].lower()

    def show_item_information(self, item_type, item_name, item_info):
        print(item_type.upper() + ' INFORMATION:')
        printout = 'Name: %s' % item_name
        for key, value in item_info.items():
            printout += (', ' + self.capitalizer(str(key)) + ': ' + str(value))
        printout += '\n'
        print(printout)

    def item_not_found(self, item_type, item_name):
        print('That %s "%s" does not exist in the records' % (item_type, item_name))


class Controller(object):

    def __init__(self, model, view):
        self.model = model
        self.view = view

    def show_items(self):
        items = list(self.model)
        item_type = self.model.item_type
        self.view.show_item_list(item_type, items)

    def show_item_information(self, item_name):
        try:
            item_info = self.model.get(item_name)
        except:
            item_type = self.model.item_type
            self.view.item_not_found(item_type, item_name)
        else:
            item_type = self.model.item_type
            self.view.show_item_information(item_type, item_name, item_info)


if __name__ == '__main__':
    model = ProductModel()
    view = ConsoleView()
    controller = Controller(model, view)
    controller.show_items()
    controller.show_item_information('cheese')
    controller.show_item_information('eggs')
    controller.show_item_information('milk')
    controller.show_item_information('arepas')

### OUTPUT ###
# PRODUCT LIST:
# cheese
# eggs
# milk
#
# PRODUCT INFORMATION:
# Name: Cheese, Price: 2.00, Quantity: 10
#
# PRODUCT INFORMATION:
# Name: Eggs, Price: 0.20, Quantity: 100
#
# PRODUCT INFORMATION:
# Name: Milk, Price: 1.50, Quantity: 10
#
# That product "arepas" does not exist in the records
PRODUCT LIST:
cheese
milk
eggs

PRODUCT INFORMATION:
Name: cheese, Quantity: 10, Price: 2.00

PRODUCT INFORMATION:
Name: eggs, Quantity: 100, Price: 0.20

PRODUCT INFORMATION:
Name: milk, Quantity: 10, Price: 1.50

That product "arepas" does not exist in the records

你可能想知道為什么控制器部分是必要的?我們能跳過它嗎?能掸鹅,但那樣我們將失去MVC提供的一大優(yōu)勢:無需修改模型就能使用多個視圖的能力(甚至可以根據(jù)需要同時使用多個視圖)塞帐。為了實現(xiàn)模型與其表現(xiàn)之間的解耦,每個視圖通常都需要屬于它的控制器巍沙。如果模型直接與特定視圖通信葵姥,我們將無法對同一個模型使用多個視圖(或者至少無法以簡潔模塊化的方式實現(xiàn))。

現(xiàn)實生活的例子

MVC是應(yīng)用于面向?qū)ο缶幊痰腟oC原則句携。SoC原則在現(xiàn)實生活中的應(yīng)用有很多榔幸。例如,如果你造一棟新房子,通常會請不同的專業(yè)人員來完成以下工作削咆。

  • 安裝管道和電路
  • 粉刷房子

另一個例子是餐館牍疏。在一個餐館中,服務(wù)員接收點菜單并為顧客上菜拨齐,但是飯菜由廚師烹飪(請參考網(wǎng)頁[t.cn/RqrYh1I])鳞陨。

軟件的例子

Web框架web2py(請參考網(wǎng)頁[t.cn/RqrYZwy])是一個支持MVC模式的輕量級Python框架。若你還未嘗試過web2py奏黑,我推薦你試用一下炊邦,安裝過程極其簡單编矾,你要做的就是下載安裝包并執(zhí)行一個Python文件(web2py.py)熟史。在該項目的網(wǎng)頁上有很多例子演示了在web2py中如何使用MVC(請參考網(wǎng)頁[t.cn/RqrYADU])。

Django也是一個MVC框架窄俏,但是它使用了不同的命名約定蹂匹。在此約定下,控制器被稱為視圖凹蜈,視圖被稱為模板限寞。Django使用名稱模型—模板—視圖(Model-Template-View,MTV)來替代MVC。依據(jù)Django的設(shè)計者所言仰坦,視圖是描述哪些數(shù)據(jù)對用戶可見履植。因此,Django把對應(yīng)一個特定URL的Python回調(diào)函數(shù)稱為視圖悄晃。Django中的“模板”用于把內(nèi)容與其展現(xiàn)分開玫霎,其描述的是用戶看到數(shù)據(jù)的方式而不是哪些數(shù)據(jù)可見(請參考網(wǎng)頁[t.cn/RwRJZ87])。

應(yīng)用案例

MVC是一個非常通用且大有用處的設(shè)計模式妈橄。實際上,所有流行的Web框架(Django庶近、Rails 和Yii)和應(yīng)用框架(iPhone SDK、Android和QT)都使用了MVC或者其變種眷蚓,其變種包括模式—視圖—適配器(Model-View-Adapter,MVA)鼻种、模型—視圖—演示者(Model-View-Presenter,MVP) 等。然而沙热,即使我們不使用這些框架叉钥,憑自己實現(xiàn)這一模式也是有意義的,因為這一模式提供了以下這些好處篙贸。

  • 視圖與模型的分離允許美工一心搞UI部分投队,程序員一心搞開發(fā),不會相互干擾歉秫。
  • 由于視圖與模型之間的松耦合,每個部分可以單獨修改/擴展蛾洛,不會相互影響。例如,添加一個新視圖的成本很小轧膘,只要為其實現(xiàn)一個控制器就可以了钞螟。
  • 因為職責(zé)明晰,維護每個部分也更簡單谎碍。

在從頭開始實現(xiàn)MVC時鳞滨,請確保創(chuàng)建的模型很智能,控制器很瘦蟆淀,視圖很傻瓜(請參考[Zlobin13,第9頁])拯啦。

可以將具有以下功能的模型視為智能模型。

  • 包含所有的校驗/業(yè)務(wù)規(guī)則/邏輯
  • 處理應(yīng)用的狀態(tài)
  • 訪問應(yīng)用數(shù)據(jù)(數(shù)據(jù)庫熔任、云或其他)
  • 不依賴UI

可以將符合以下條件的控制器視為瘦控制器褒链。

  • 在用戶與視圖交互時,更新模型
  • 在模型改變時,更新視圖
  • 如果需要疑苔,在數(shù)據(jù)傳遞給模型/視圖之前進(jìn)行處理不展示數(shù)據(jù)
  • 不直接訪問應(yīng)用數(shù)據(jù)
  • 不包含校驗/業(yè)務(wù)規(guī)則/邏輯

可以將符合以下條件的視圖視為傻瓜視圖甫匹。

  • 展示數(shù)據(jù)
  • 允許用戶與其交互
  • 僅做最小的數(shù)據(jù)處理,通常由一種模板語言提供處理能力(例如惦费,使用簡單的變量和循環(huán)控制)
  • 不存儲任何數(shù)據(jù)
  • 不直接訪問應(yīng)用數(shù)據(jù)
  • 不包含校驗/業(yè)務(wù)規(guī)則/邏輯

如果你正在從頭實現(xiàn)MVC兵迅,并且想弄清自己做得對不對,可以嘗試回答以下兩個關(guān)鍵問題薪贫。

  • 如果你的應(yīng)用有GUI恍箭,那它可以換膚嗎?易于改變它的皮膚/外觀以及給人的感受嗎瞧省?可以為用戶提供運行期間改變應(yīng)用皮膚的能力嗎扯夭?如果這做起來并不簡單,那就意味著你的MVC實現(xiàn)在某些地方存在問題(請參考網(wǎng)頁[t.cn/RqrjF4G])臀突。
  • 如果你的應(yīng)用沒有GUI(例如勉抓,是一個終端應(yīng)用),為其添加GUI支持有多難?或者候学,如果添加GUI沒什么用藕筋,那么是否易于添加視圖從而以圖表(餅圖、柱狀圖等)或文檔(PDF梳码、電子表格等)形式展示結(jié)果隐圾?如果因此而作出的變更不小(小的變更是,在不變更模型的情況下創(chuàng)建控制器并綁定到視圖)掰茶,那你的MVC實現(xiàn)就有些不對了暇藏。

如果你確信這兩個條件都已滿足,那么與未使用MVC模式的應(yīng)用相比濒蒋,你的應(yīng)用會更靈活盐碱、更好維護把兔。

實現(xiàn)

我可以使用任意常見框架來演示如何使用MVC,但覺得那樣的話,讀者對MVC的理解會不完整。因此我決定使用一個非常簡單的示例來展示如何從頭實現(xiàn)MVC狼讨,這個示例是名人名言打印機乒躺。想法極其簡單:用戶輸入一個數(shù)字轮洋,然后就能看到與這個數(shù)字相關(guān)的名人名言。名人名言存儲在一個quotes元組中。這種數(shù)據(jù)通常是存儲在數(shù)據(jù)庫、文件或其他地方晾咪,只有模型能夠直接訪問它。

我們從下面的代碼開始考慮這個例子贮配。

quotes = ('A man is not complete until he is married. Then he is finished.',
              'As I said before, I never repeat myself.',
              'Behind a successful man is an exhausted woman.',
              'Black holes really suck...', 'Facts are stubborn things.')

模型極為簡約谍倦,只有一個get_quote()方法,基于索引n從quotes元組中返回對應(yīng)的名人名言(字符串)牧嫉。注意剂跟,n可以小于等于0,因為這種索引方式在Python中是有效的酣藻。本節(jié)末尾準(zhǔn)備了練習(xí),供你改進(jìn)這個方法的行為鳍置。

class QuoteModel:
    def get_quote(self, n):
        try:
            value = quotes[n]
        except IndexError as err:
            value = 'Not found!'
        return value

視圖有三個方法辽剧,分別是show()、error()和select_quote()税产。show()用于在屏幕上輸出一旬名人名言(或者輸出提示信息Not found!)怕轿;error()用于在屏幕上輸出一條錯誤消息;select_quote()用于讀取用戶的選擇辟拷,如以下代碼所示撞羽。

class QuoteTerminalView:
    def show(self, quote):
        print('And the quote is: "{}"'.format(quote))
    def error(self, msg):
        print('Error: {}'.format(msg))
    def select_quote(self):
        return input('Which quote number would you like to see? ')

控制器負(fù)責(zé)協(xié)調(diào)。init()方法初始化模型和視圖衫冻。run()方法校驗用戶提供的名言索引诀紊,然后從模型中獲取名言,并返回給視圖展示隅俘,如以下代碼所示邻奠。

class QuoteTerminalController:
    def init (self):
        self.model = QuoteModel()
        self.view = QuoteTerminalView()

    def run(self):
        valid_input = False
        while not valid_input:
            n = self.view.select_quote()
            try:
                n = int(n)
            except ValueError as err:
                self.view.error("Incorrect index '{}'".format(n)) else:
            valid_input = True
        quote = self.model.get_quote(n)
        self.view.show(quote)

最后,但同樣重要的是为居,main()函數(shù)初始化并觸發(fā)控制器碌宴,如以下代碼所示。

def main():
    controller = QuoteTerminalController()
    while True:
        controller.run()

以下是該示例的完整代碼(文件mvc.py)蒙畴。

quotes = ('A man is not complete until he is married. Then he is finished.',
          'As I said before, I never repeat myself.',
          'Behind a successful man is an exhausted woman.',
          'Black holes really suck...',
          'Facts are stubborn things.')

class QuoteModel:
    def get_quote(self, n):
        try:
            value = quotes[n]
        except IndexError as err:
            value = 'Not found!'
        return value

class QuoteTerminalView:
    def show(self, quote):
        print('And the quote is: "{}"'.format(quote))

    def error(self, msg):
        print('Error: {}'.format(msg))

    def select_quote(self):
        return input('Which quote number would you like to see? ')

class QuoteTerminalController:
    def __init__(self):
        self.model = QuoteModel()
        self.view = QuoteTerminalView()

    def run(self):
        valid_input = False
        while not valid_input:
            n = self.view.select_quote()
            try:
                n = int(n)
            except ValueError as err:
                self.view.error("Incorrect index '{}'".format(n))
            else:
                valid_input = True
        quote = self.model.get_quote(n)
        self.view.show(quote)

def main():
    controller = QuoteTerminalController()
    while True:
        controller.run()

if __name__ == '__main__':
    main()

---------------------------------------------------------------------------
    KeyboardInterrupt                         Traceback (most recent call last)

    /Users/hanlei/anaconda/lib/python3.5/site-packages/ipykernel/kernelbase.py in _input_request(self, prompt, ident, parent, password)
        713             try:
    --> 714                 ident, reply = self.session.recv(self.stdin_socket, 0)
        715             except Exception:


    /Users/hanlei/anaconda/lib/python3.5/site-packages/jupyter_client/session.py in recv(self, socket, mode, content, copy)
        738         try:
    --> 739             msg_list = socket.recv_multipart(mode, copy=copy)
        740         except zmq.ZMQError as e:

    /Users/hanlei/anaconda/lib/python3.5/site-packages/zmq/sugar/socket.py in recv_multipart(self, flags, copy, track)

    --> 358         parts = [self.recv(flags, copy=copy, track=track)]
        359         # have first part already, only loop while more to receive


    zmq/backend/cython/socket.pyx in zmq.backend.cython.socket.Socket.recv (zmq/backend/cython/socket.c:6971)()


    zmq/backend/cython/socket.pyx in zmq.backend.cython.socket.Socket.recv (zmq/backend/cython/socket.c:6763)()


    zmq/backend/cython/socket.pyx in zmq.backend.cython.socket._recv_copy (zmq/backend/cython/socket.c:1931)()


    /Users/hanlei/anaconda/lib/python3.5/site-packages/zmq/backend/cython/checkrc.pxd in zmq.backend.cython.checkrc._check_rc (zmq/backend/cython/socket.c:7222)()


    KeyboardInterrupt:


    During handling of the above exception, another exception occurred:


    KeyboardInterrupt                         Traceback (most recent call last)

    <ipython-input-11-ea7696f29195> in <module>()
         47
         48 if __name__ == '__main__':
    ---> 49     main()


    <ipython-input-11-ea7696f29195> in main()
         44     controller = QuoteTerminalController()
         45     while True:
    ---> 46         controller.run()
         47
         48 if __name__ == '__main__':


    <ipython-input-11-ea7696f29195> in run(self)
         31         valid_input = False
         32         while not valid_input:
    ---> 33             n = self.view.select_quote()
         34             try:
         35                 n = int(n)


    <ipython-input-11-ea7696f29195> in select_quote(self)
         21
         22     def select_quote(self):
    ---> 23         return input('Which quote number would you like to see? ')
         24
         25 class QuoteTerminalController:


    /Users/hanlei/anaconda/lib/python3.5/site-packages/ipykernel/kernelbase.py in raw_input(self, prompt)
        687             self._parent_ident,
        688             self._parent_header,
    --> 689             password=False,
        690         )
        691


    /Users/hanlei/anaconda/lib/python3.5/site-packages/ipykernel/kernelbase.py in _input_request(self, prompt, ident, parent, password)
        717             except KeyboardInterrupt:
        718                 # re-raise KeyboardInterrupt, to truncate traceback
    --> 719                 raise KeyboardInterrupt
        720             else:
        721                 break


    KeyboardInterrupt:

當(dāng)然贰镣,你不會(也不應(yīng)該)就此止步。堅持多寫代碼,還有很多有意思的想法可以試驗碑隆,比如以下這些董朝。

  • 僅允許用戶使用大于或等于1的索引,程序會顯得更加友好干跛。為此子姜,你也需要修改get_quote()。
  • 使用Tkinter楼入、Pygame或Kivy之類的GUI框架來添加一個圖形化視圖哥捕。程序如何模塊化?可以在程序運行期間決定使用哪個視圖嗎嘉熊?
  • 讓用戶可以選擇鍵入某個鍵(例如遥赚,r鍵)隨機地看一旬名言。
  • 索引校驗H前是在控制器中完成的阐肤。這個方式好嗎凫佛?如果你編寫了另一個視圖,需要它自己的控制器孕惜,那又該怎么辦呢愧薛?試想一下,為了讓索引校驗的代碼被所有控制/視圖復(fù)用衫画,將索引校驗移到模型中進(jìn)行毫炉,需要做哪些變更?
  • 對這個例子進(jìn)行擴展削罩,使其變得像一個創(chuàng)建瞄勾、讀取、更新弥激、刪除(Create, Read, Update, Delete进陡,CURD)應(yīng)用。你應(yīng)該能夠輸入新的名言微服,刪除已有的名言趾疚,以及修改名言。

小結(jié)

本章中职辨,我們學(xué)習(xí)了MVC模式盗蟆。MVC是一個非常重要的設(shè)計模式,用于將應(yīng)用組織成三個部分:模型舒裤、視圖和控制器喳资。

每個部分都有明確的職責(zé)。模型負(fù)責(zé)訪問數(shù)據(jù)腾供,管理應(yīng)用的狀態(tài)仆邓。視圖是模型的外在表現(xiàn)鲜滩。視圖并非必須是圖形化的;文本輸出也是一種好視圖节值♂愎瑁控制器是模型與視圖之間的連接。MVC的恰當(dāng)使用能確保最終產(chǎn)出的應(yīng)用易于維護搞疗、易于擴展嗓蘑。

MVC模式是應(yīng)用到面向?qū)ο缶幊痰腟oC原則。這一原則類似于一棟新房子如何建造匿乃,或一個餐館如何運營桩皿。

Python框架web2py使用MVC作為核心架構(gòu)理念。即使是最簡單的web2py例子也使用了MVC來實現(xiàn)模塊化和可維護性幢炸。Django也是一個MVC框架泄隔,但它使用的名稱是MTV。

使用MVC時宛徊,請確保創(chuàng)建智能的模型(核心功能)佛嬉、瘦控制器(實現(xiàn)視圖與模型之間通信的能力)以及傻瓜式的視圖(外在表現(xiàn),最小化邏輯處理)闸天。

在8.4節(jié)中暖呕,我們學(xué)習(xí)了如何從零開始實現(xiàn)MVC,為用戶展示有趣的名人名言号枕。這與羅列一個RSS源的所有文章所要求的功能沒什么兩樣缰揪,如果你對其他推薦練習(xí)不感興趣,可以練習(xí)實現(xiàn)這個葱淳。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市抛姑,隨后出現(xiàn)的幾起案子赞厕,更是在濱河造成了極大的恐慌,老刑警劉巖定硝,帶你破解...
    沈念sama閱讀 210,914評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件皿桑,死亡現(xiàn)場離奇詭異,居然都是意外死亡蔬啡,警方通過查閱死者的電腦和手機诲侮,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 89,935評論 2 383
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來箱蟆,“玉大人沟绪,你說我怎么就攤上這事】詹拢” “怎么了绽慈?”我有些...
    開封第一講書人閱讀 156,531評論 0 345
  • 文/不壞的土叔 我叫張陵恨旱,是天一觀的道長。 經(jīng)常有香客問我坝疼,道長搜贤,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,309評論 1 282
  • 正文 為了忘掉前任钝凶,我火速辦了婚禮仪芒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘耕陷。我一直安慰自己掂名,他們只是感情好,可當(dāng)我...
    茶點故事閱讀 65,381評論 5 384
  • 文/花漫 我一把揭開白布啃炸。 她就那樣靜靜地躺著铆隘,像睡著了一般。 火紅的嫁衣襯著肌膚如雪南用。 梳的紋絲不亂的頭發(fā)上膀钠,一...
    開封第一講書人閱讀 49,730評論 1 289
  • 那天,我揣著相機與錄音裹虫,去河邊找鬼肿嘲。 笑死,一個胖子當(dāng)著我的面吹牛筑公,可吹牛的內(nèi)容都是我干的雳窟。 我是一名探鬼主播,決...
    沈念sama閱讀 38,882評論 3 404
  • 文/蒼蘭香墨 我猛地睜開眼匣屡,長吁一口氣:“原來是場噩夢啊……” “哼封救!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起捣作,我...
    開封第一講書人閱讀 37,643評論 0 266
  • 序言:老撾萬榮一對情侶失蹤誉结,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后券躁,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體惩坑,經(jīng)...
    沈念sama閱讀 44,095評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,448評論 2 325
  • 正文 我和宋清朗相戀三年也拜,在試婚紗的時候發(fā)現(xiàn)自己被綠了以舒。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,566評論 1 339
  • 序言:一個原本活蹦亂跳的男人離奇死亡慢哈,死狀恐怖蔓钟,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情岸军,我是刑警寧澤奋刽,帶...
    沈念sama閱讀 34,253評論 4 328
  • 正文 年R本政府宣布瓦侮,位于F島的核電站,受9級特大地震影響佣谐,放射性物質(zhì)發(fā)生泄漏肚吏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,829評論 3 312
  • 文/蒙蒙 一狭魂、第九天 我趴在偏房一處隱蔽的房頂上張望罚攀。 院中可真熱鬧,春花似錦雌澄、人聲如沸斋泄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,715評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽炫掐。三九已至,卻和暖如春睬涧,著一層夾襖步出監(jiān)牢的瞬間募胃,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,945評論 1 264
  • 我被黑心中介騙來泰國打工畦浓, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留痹束,地道東北人。 一個月前我還...
    沈念sama閱讀 46,248評論 2 360
  • 正文 我出身青樓讶请,卻偏偏與公主長得像祷嘶,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子夺溢,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,440評論 2 348

推薦閱讀更多精彩內(nèi)容