DCI in C++

来源:http://www.cnblogs.com/magicbowen/archive/2016/06/06/5563268.html
-Advertisement-
Play Games

DCI in C++ 本文講解的C++的DCI編程框架,目前作為 "ccinfra" 的一個組件提供,可訪問 "https://github.com/MagicBowen/ccinfra" 獲取具體源碼。ccinfra中的DCI框架原創者是袁英傑先生(Thoughtworks),我們在兩個大型電信系 ...


DCI in C++

本文講解的C++的DCI編程框架,目前作為ccinfra的一個組件提供,可訪問https://github.com/MagicBowen/ccinfra獲取具體源碼。ccinfra中的DCI框架原創者是袁英傑先生(Thoughtworks),我們在兩個大型電信系統的重構過程中大面積地使用了該技術,取得了非常好的效果,在此我將其整理出來。由於文筆有限,拙於表達,希望不足之處英傑見諒!


DCI是一種面向對象軟體架構模式,它可以讓面向對象更好地對數據和行為之間的關係進行建模從而更容易被人理解。DCI目前廣泛被作為對DDD(領域驅動開發)的一種發展和補充,用於基於面向對象的領域建模。DCI建議將軟體的領域核心代碼分為Context、Interactive和Data層。Context層用於處理由外部UI或者消息觸發業務場景,每個場景都能找對一個對應的context,其作為理解系統如何處理業務流程的起點。Data層用來描述系統是什麼(What the system is?),在該層中採用領域驅動開發中描述的建模技術,識別系統中應該有哪些領域對象以及這些對象的生命周期和關係。而DCI最大的發展則在於Interactive層,DCI認為應該顯示地對領域對象在每個context中所扮演的角色role進行建模,role代表了領域對象服務於context時應該具有的業務行為。正是因為領域對象的業務行為只有在去服務於某一context時才會具有意義,DCI認為對role的建模應該是面向context的,屬於role的方法不應該強塞給領域對象,否則領域對象就會隨著其支持的業務場景(context)越來越多而變成上帝類。但是role最終還是要操作數據,那麼role和領域對象之間應該存在一種註入(cast)關係。當context被觸發的時候,context串聯起一系列的role進行交互完成一個特定的業務流程。Context應該決定在當前業務場景下每個role的扮演者(領域對象),context中僅完成領域對象到role的註入或者cast,然後讓role互動以完成對應業務邏輯。基於上述DCI的特點,DCI架構使得軟體具有如下好處:

  • 清晰的進行了分層使得軟體更容易被理解。
    1. Context是儘可能薄的一層。Context往往被實現得無狀態,只是找到合適的role,讓role交互起來完成業務邏輯即可。但是簡單並不代表不重要,顯示化context層正是為人去理解軟體業務流程提供切入點和主線。
    2. Data層描述系統有哪些領域概念及其之間的關係,該層專註於領域對象和之間關係的確立,讓程式員站在對象的角度思考系統,從而讓系統是什麼更容易被理解。
    3. Interactive層主要體現在對role的建模,role是每個context中複雜的業務邏輯的真正執行者。Role所做的是對行為進行建模,它聯接了context和領域對象!由於系統的行為是複雜且多變的,role使得系統將穩定的領域模型層和多變的系統行為層進行了分離,由role專註於對系統行為進行建模。該層往往關註於系統的可擴展性,更加貼近於軟體工程實踐,在面向對象中更多的是以類的視角進行思考設計。
  • 顯示的對role進行建模,解決了面向對象建模中充血和貧血模型之爭。DCI通過顯示的用role對行為進行建模,同時讓role在context中可以和對應的領域對象進行綁定(cast),從而既解決了數據邊界和行為邊界不一致的問題,也解決了領域對象中數據和行為高內聚低耦合的問題。

面向對象建模面臨的一個棘手問題是數據邊界和行為邊界往往不一致。遵循模塊化的思想,我們通過類將行為和其緊密耦合的數據封裝在一起。但是在複雜的業務場景下,行為往往跨越多個領域對象,這樣的行為放在某一個對象中必然導致別的對象需要向該對象暴漏其內部狀態。所以面向對象發展的後來,領域建模出現兩種派別之爭,一種傾向於將跨越多個領域對象的行為建模在所謂的service中(見DDD中所描述的service建模元素)。這種做法使用過度經常導致領域對象變成只提供一堆get方法的啞對象,這種建模導致的結果被稱之為貧血模型。而另一派則堅定的認為方法應該屬於領域對象,所以所有的業務行為仍然被放在領域對象中,這樣導致領域對象隨著支持的業務場景變多而變成上帝類,而且類內部方法的抽象層次很難一致。另外由於行為邊界很難恰當,導致對象之間數據訪問關係也比較複雜。這種建模導致的結果被稱之為充血模型。

在DCI架構中,如何將role和領域對象進行綁定,根據語言特點做法不同。對於動態語言,可以在運行時進行綁定。而對於靜態語言,領域對象和role的關係在編譯階段就得確定。DCI的論文《www.artima.com/articles/dci_vision.html》中介紹了C++採用模板Trait的技巧進行role和領域對象的綁定。但是由於在複雜的業務場景下role之間會存在大量的行為依賴關係,如果採用模板技術會產生複雜的模板交織代碼從而讓工程層面變得難以實施。正如我們前面所講,role主要對複雜多變的業務行為進行建模,所以role需要更加關註於系統的可擴展性,更加貼近軟體工程,對role的建模應該更多地站在類的視角,而面向對象的多態和依賴註入則可以相對更輕鬆地解決此類問題。另外,由於一個領域對象可能會在不同的context下扮演多種角色,這時領域對象要能夠和多種不同類型的role進行綁定。對於所有這些問題,ccinfra提供的DCI框架採用了多重繼承來描述領域對象和其支持的role之間的綁定關係,同時採用了在多重繼承樹內進行關係交織來進行role之間的依賴關係描述。這種方式在C++中比採用傳統的依賴註入的方式更加簡單高效。

對於DCI的理論介紹,以及如何利用DCI框架進行領域建模,本文就介紹這些。後面主要介紹如何利用ccinfra中的DCI框架來實現和拼裝role以完成這種組合式編程。

下麵假設一種場景:模擬人和機器人製造產品。人製造產品會消耗吃飯得到的能量,缺乏能量後需要再吃飯補充;而機器人製造產品會消耗電能,缺乏能量後需要再充電。這裡人和機器人在工作時都是一名worker(扮演的角色),工作的流程是一樣的,但是區別在於依賴的能量消耗和獲取方式不同。

DEFINE_ROLE(Energy)
{
    ABSTRACT(void consume());
    ABSTRACT(bool isExhausted() const);
};

struct HumanEnergy : Energy
{
    HumanEnergy()
    : isHungry(false), consumeTimes(0)
    {
    }

private:
    OVERRIDE(void consume())
    {
        consumeTimes++;

        if(consumeTimes >= MAX_CONSUME_TIME)
        {
            isHungry = true;
        }
    }

    OVERRIDE(bool isExhausted() const)
    {
        return isHungry;
    }

private:
    enum
    {
        MAX_CONSUME_TIME = 10,
    };

    bool isHungry;
    U8 consumeTimes;
};

struct ChargeEnergy : Energy
{
    ChargeEnergy() : percent(0)
    {
    }

    void charge()
    {
        percent = FULL_PERCENT;
    }

private:
    OVERRIDE(void consume())
    {
        if(percent > 0)
            percent -= CONSUME_PERCENT;
    }

    OVERRIDE(bool isExhausted() const)
    {
        return percent == 0;
    }

private:
    enum
    {
        FULL_PERCENT = 100,
        CONSUME_PERCENT = 1
    };

    U8 percent;
};

DEFINE_ROLE(Worker)
{
    Worker() : produceNum(0)
    {
    }

    void produce()
    {
        if(ROLE(Energy).isExhausted()) return;

        produceNum++;

        ROLE(Energy).consume();
    }

    U32 getProduceNum() const
    {
        return produceNum;
    }

private:
    U32 produceNum;

private:
    USE_ROLE(Energy);
};

上面代碼中使用了DCI框架中三個主要的語法糖:

  • DEFINE_ROLE:用於定義role。DEFINE_ROLE的本質是創建一個包含了虛析構的抽象類,但是在DCI框架裡面使用這個命名更具有語義。DEFINE_ROLE定義的類中需要至少包含一個虛方法或者使用了USE_ROLE聲明依賴另外一個role。

  • USE_ROLE:在一個類裡面聲明自己的實現依賴另外一個role。

  • ROLE:當一個類聲明中使用了USE_ROLE聲明依賴另外一個類XXX後,則在類的實現代碼裡面就可以調用 ROLE(XXX)來引用這個類去調用它的成員方法。

上面的例子中用DEFINE_ROLE定義了一個名為Worker的role(本質上是一個類),WorkerUSE_ROLE聲明它的實現需要依賴於另一個role:EnergyWorker在它的實現中調用ROLE(Energy)訪問它提供的介面方法。Energy是一個抽象類,有兩個子類HumanEnergyChargeEnergy分別對應於人和機器人的能量特征。上面是以類的形式定義的各種role,下麵我們需要將role和領域對象關聯並將role之間的依賴關係在領域對象內完成正確的交織。

struct Human : Worker
             , private HumanEnergy
{
private:
    IMPL_ROLE(Energy);
};

struct Robot : Worker
             , ChargeEnergy
{
private:
    IMPL_ROLE(Energy);
};

上面的代碼使用多重繼承完成了領域對象對role的組合。在上例中Human組合了WorkerHumanEnergy,而Robot組合了WorkerChargeEnergy。最後在領域對象的類內還需要完成role之間的關係交織。由於Worker中聲明瞭USE_ROLE(Energy),所以當HumanRobot繼承了Worker之後就需要顯示化Energy從哪裡來。有如下幾種主要的交織方式:

  • IMPL_ROLE: 對上例,如果Energy的某一個子類也被繼承的話,那麼就直接在交織類中聲明IMPL_ROLE(Energy)。於是當Worker工作時所找到的ROLE(Energy)就是在交織類中所繼承的具體Energy子類。

  • IMPL_ROLE_WITH_OBJ: 當持有被依賴role的一個引用或者成員的時候,使用IMPL_ROLE_WITH_OBJ進行關係交織。假如上例中Human類中有一個成員:HumanEnergy energy,那麼就可以用IMPL_ROLE_WITH_OBJ(Energy, energy)來聲明交織關係。該場景同樣適用於類內持有的是被依賴role的指針、引用的場景。

  • DECL_ROLE : 自定義交織關係。例如對上例在Human中定義一個方法DECL_ROLE(Energy){ // function implementation},自定義Energy的來源,完成交織。

當正確完成role的依賴交織工作後,領域對象類就可以被實例化了。如果沒有交織正確,一般會出現編譯錯誤。

TEST(...)
{
    Human human;
    SELF(human, Worker).produce();
    ASSERT_EQ(1, SELF(human, Worker).getProduceNum());

    Robot robot;
    SELF(robot, ChargeEnergy).charge();
    while(!SELF(robot, Energy).isExhausted())
    {
        SELF(robot, Worker).produce();
    }
    ASSERT_EQ(100, SELF(robot, Worker).getProduceNum());
}

如上使用SELF將領域對象cast到對應的role上訪問其介面方法。註意只有被public繼承的role才可以從領域對象上cast過去,private繼承的role往往是作為領域對象的內部依賴(上例中human不能做SELF(human, Energy)轉換,會編譯錯誤)。

通過對上面例子中使用DCI的方式進行分析,我們可以看到ccinfra提供的DCI實現方式具有如下特點:

  • 通過多重繼承的方式,同時完成了類的組合以及依賴註入。被繼承在同一顆繼承樹上的類天然被組合在一起,同時通過USE_ROLEIMPL_ROLE的這種編織虛函數表的方式完成了這些類之間的互相依賴引用,相當於完成了依賴註入,只不過這種依賴註入成本更低,表現在C++上來說就是避免了在類中去定義依賴註入的指針以及通過構造函數進行註入操作,而且同一個領域對象類的所有對象共用類的虛表,所以更加節省記憶體。

  • 提供一種組合式編程風格。USE_ROLE可以聲明依賴一個具體類或者抽象類。當一個類的一部分有復用價值的時候就可以將其拆分出來,然後讓原有的類USE_ROLE它,最後通過繼承再組合在一起。當一個類出現新的變化方向時,就可以讓當前類USE_ROLE一個抽象類,最後通過繼承抽象類的不同子類來完成對變化方向的選擇。最後如果站在類的視圖上看,我們得到的是一系列可被覆用的類代碼素材庫;站在領域對象的角度上來看,所謂領域對象只是選擇合適自己的類素材,最後完成組合拼裝而已(見下麵的類視圖和DCI視圖)。

    類視圖:

    DCI視圖:

  • 每個領域對象的結構類似一顆向上生長的樹(見上DCI視圖)。Role作為這顆樹的葉子,實際上並不區分是行為類還是數據類,都儘量設計得高內聚低耦合,採用USE_ROLE的方式聲明互相之間的依賴關係。領域對象作為樹根採用多重繼承完成對role的組合和依賴關係交織,可以被外部使用的role被public繼承,我們叫做“public role”(上圖中空心圓圈表示),而只在樹的內部被調用的role則被private繼承,叫做“private role”(上圖中實心圓圈表示)。當context需要調用某一領域對象時,必須從領域對象cast到對應的public role上去調用,不會出現傳統教科書上所說的多重繼承帶來的二義性問題。

  • 採用這種多重繼承的方式組織代碼,我們會得到一種小類大對象的結構。所謂小類,指的是每個role的代碼是為了完成組合和擴展性,是站在類的角度去解決工程性問題(面向對象),一般都相對較小。而當不同的role組合到一起形成大領域對象後,它卻可以讓我們站在領域的角度去思考問題,關註領域對象整體的領域概念、關係和生命周期(基於對象)。大對象的特點同時極大的簡化了領域對象工廠的成本,避免了繁瑣的依賴註入,並使得記憶體規劃和管理變得簡單;程式員只用考慮領域對象整體的記憶體規劃,對領域對象上的所有role整體記憶體申請和釋放,避免了對一堆小的拼裝類對象的記憶體管理,這點對於嵌入式開發非常關鍵。

  • 多重繼承關係讓一個領域對象可以支持哪些角色(role),以及一個角色可由哪些領域對象扮演變得顯示化。這種顯示化關係對於理解代碼和靜態檢查都非常有幫助。

上述在C++中通過多重繼承來實現DCI架構的方式,是一種幾近完美的一種方式(到目前為止的個人經驗)。如果非要說缺點,只有一個,就是多重繼承造成的物理依賴污染問題。由於C++中要求一個類如果繼承了另一個類,當前類的文件里必須包含被繼承類的頭文件。這就導致了領域對象類的聲明文件裡面事實上包含了所有它繼承下來的role的頭文件。在context中使用某一個role需用領域對象做cast,所以需要包含領域對象類的頭文件。那麼當領域對象上的任何一個role的頭文件發生了修改,所有包含該領域對象頭文件的context都得要重新編譯,無關該context是否真的使用了被修改的role。解決該問題的一個方法就是再建立一個抽象層專門來做物理依賴隔離。例如對上例中的Human,可以修改如下:

DEFINE_ROLE(Human)
{
    HAS_ROLE(Worker);
};

struct HumanObject : Human
                   , private Worker
                   , private HumanEnergy
{
private:
    IMPL_ROLE(Worker);
    IMPL_ROLE(Energy);
};

struct HumanFactory
{
    static Human* create()
    {
        return new HumanObject;
    }
};

TEST(...)
{
    Human* human = HumanFactory::create();

    human->ROLE(Worker).produce();

    ASSERT_EQ(1, human->ROLE(Worker).getProduceNum());

    delete human;
}

為了屏蔽物理依賴,我們把Human變成了一個純介面類,它裡面聲明瞭該領域對象可被context訪問的所有public role,由於在這裡只用前置聲明,所以無需包含任何role的頭文件。而對真正繼承了所有role的領域對象HumanObject的構造隱藏在工廠裡面。Context中持有從工廠中創建返回的Human指針,於是context中只用包含Human的頭文件和它實際要使用的role的頭文件,這樣和它無關的role的修改不會引起該context的重新編譯。

事實上C++語言的RTTI特性同樣可以解決上述問題。該方法需要領域對象額外繼承一個公共的虛介面類。Context持有這個公共的介面,利用dynamic_cast從公共介面往自己想要使用的role上去嘗試cast。這時context只用包含該公共介面以及它僅使用的role的頭文件即可。修改後的代碼如下:

DEFINE_ROLE(Actor)
{
};

struct HumanObject : Actor
                   , Worker
                   , private HumanEnergy
{
private:
    IMPL_ROLE(Energy);
};

struct HumanFactory
{
    static Actor* create()
    {
        return new HumanObject;
    }
};

TEST(...)
{
    Actor* actor = HumanFactory::create();

    Worker* worker = dynamic_cast<Worker*>(actor);

    ASSERT_TRUE(__notnull__(worker));

    worker->produce();

    ASSERT_EQ(1, worker->getProduceNum());

    delete actor;
}

上例中我們定義了一個公共類Actor,它沒有任何代碼,但是至少得有一個虛函數(RTTI要求),使用DEFINE_ROLE定義的類會自動為其增加一個虛析構函數,所以Actor滿足要求。最終領域對象繼承Actor,而context僅需持有領域對象工廠返回的Actor的指針。Context中通過dynamic_castactor指針轉型成領域對象身上其它有效的public role,dynamic_cast會自動識別這種轉換是否可以完成,如果在當前Actor的指針對應的對象的繼承樹上找不到目標類,dynamic_cast會返回空指針。上例中為了簡單把所有代碼寫到了一起。真實場景下,使用ActorWorker的context的實現文件中僅需要包含ActorWorker的頭文件即可,不會被HumanObject繼承的其它role物理依賴污染。

通過上例可以看到使用RTTI的解決方法是比較簡單的,可是這種簡單是有成本的。首先編譯器需要在虛表中增加很多類型信息,以便可以完成轉換,這會增加目標版本的大小。其次dynamic_cast會隨著對象繼承關係的複雜變得性能底下。所以C++編譯器對於是否開啟RTTI有專門的編譯選項開關,由程式員自行進行取捨。

最後我們介紹ccinfra的DCI框架中提供的一種RTTI的替代工具,它可以模仿完成類似dynamic_cast的功能,但是無需在編譯選項中開啟RTTI功能。這樣當我們想要在代碼中小範圍使用該特性的時候,就不用承擔整個版本都因RTTI帶來的性能損耗。利用這種替代技術,可以讓程式員精確地在開發效率和運行效率上進行控制和平衡。

UNKNOWN_INTERFACE(Worker, 0x1234)
{
// Original implementation codes of Worker!
};

struct HumanObject : dci::Unknown
                   , Worker
                   , private HumanEnergy
{
    BEGIN_INTERFACE_TABLE()
        __HAS_INTERFACE(Worker)
    END_INTERFACE_TABLE()

private:
    IMPL_ROLE(Energy);
};

struct HumanFactory
{
    static dci::Unknown* create()
    {
        return new HumanObject;
    }
};

TEST(...)
{
    dci::Unknown* unknown = HumanFactory::create();

    Worker* worker = dci::unknown_cast<Worker>(unknown);

    ASSERT_TRUE(__notnull__(worker));

    worker->produce();

    ASSERT_EQ(1, worker->getProduceNum());

    delete unknown;
}

通過上面的代碼,可以看到ccinfra的dci框架中提供了一個公共的介面類dci::Unknown,該介面需要被領域對象public繼承。能夠從dci::Unknown被轉化到的目標role需要用UNKNOWN_INTERFACE來定義,參數是類名以及一個32位的隨機數。這個隨機數需要程式員自行提供,保證全局不重覆(可以寫一個腳本自動產生不重覆的隨機數,同樣可以用腳本自動校驗代碼中已有的是否存在重覆,可以把校驗腳本作為版本編譯檢查的一部分)。領域對象類繼承的所有由UNKNOWN_INTERFACE定義的role都需要在BEGIN_INTERFACE_TABLE()END_INTERFACE_TABLE()中由__HAS_INTERFACE顯示註冊一下(參考上面代碼中HumanObject的寫法)。最後,context持有領域對象工廠返回的dci::Unknown指針,通過dci::unknown_cast將其轉化目標role使用,至此這種機制和dynamic_cast的用法基本一致,在無法完成轉化的情況下會返回空指針,所以安全起見需要對返回的指針進行校驗。

上述提供的RTTI替代手段,雖然比直接使用RTTI略顯複雜,但是增加的手工編碼成本並不大,帶來的好處卻是明顯的。例如對嵌入式開發,這種機制相比RTTI來說對程式員是可控的,可以選擇在僅需要該特性的範圍內使用,避免無謂的記憶體和性能消耗。

作者:MagicBowen, Email:[email protected],轉載請註明作者信息,謝謝!


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 某個子站是php寫的,訪問的時候nginx時不時會冒出現502錯誤,高峰時更頻繁,檢查php-fpm的日誌發現大量的 child exited on signal 7 (SIGBUS),並且和accesslog里的502時間完全吻合,排除了php進程過載的可能,然後又排除了apc的嫌疑。 既然php ...
  • ...
  • 第2天 棧和寄存器 多文件編程 筆者在私下和很多C語言的愛好者和初學者交流的過程中發現,大家已經能夠使用C語言做出來很出色的程式了。但是這些出色的程式中的一部分竟然只有一個源文件。所以,筆者決定要介紹一下如何使用多個源文件進行編程。不得不說,多文件編程有非常多的優勢。比如在維護上非常方便,同時也給多 ...
  • 上一篇文章主要是講了mybatis-generator-core-1.3.2.jar的使用,這一篇我要介紹的是,修改jar包代碼,實現生成自定義模板。 1.我們從這裡可以下載mybatis-generator-core-1.3.2.jar項目源碼 http://maven.outofmemory.c ...
  • 看了網上一些文章,做了點總結,順便再加點自己的東西,簡單的說下。 1.利用瀏覽器的304緩存,但是304叫協商緩存,還是需要與伺服器通信一次 2.強制使用瀏覽器使用本地緩存(cache-control/expires),但是問題來了,不讓瀏覽器發資源請求,資源怎麼更新。 3.使用版本號,類似於a.c ...
  • 一、 java的基本程式設計結構 (一) java共有8種基本類型:4種整型,2種浮點類型,1種char,1種boolean。 1) 4種整型:byte(1)、short(2)、int(4)、long(8)。 2) int最常用,byte和short常用在底層的文件處理或者需要控制占用存儲空間量的大 ...
  • 每次回到宿舍想看部電影才發現很長時間沒有去bt站淘種子了, 然而天天去站上找適合自己類型的電影又是一件費時又費力的事兒, 所以周末花時間寫了一個可配置的爬子, 能夠根據不同人的不同需求去自動下載種子文件, 並且能夠避免不同分類中的重覆電影 後期還會加入下載隊列的功能, 在檢測宿舍無人用網的時候開啟b ...
  • 前幾天在學習js的時候,碰到了這樣一道面試題,要求計算出給你一個隨機亂敲的一個字元串,要求在其中找出那個字元出現的次數最多,以及出現的個數。 這你有兩種方案,請大家仔細閱讀,有可能在你將來的面試中會碰到。 一: 二: 謝!轉:http://www.cnblogs.com/heyongjun1997/ ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...