戲說領域驅動設計(廿五)——領域事件

来源:https://www.cnblogs.com/skevin/archive/2022/05/06/16188097.html
-Advertisement-
Play Games

SaaS賽道是一個超大賽道,足夠容納上萬家服務商,不太可能有哪個服務商能滿足所有場景,大部分SaaS服務商在某個垂直領域,提供差異化的產品和服務。SaaS產品大部分都是面向B端客戶,少部分面向C端客戶,本文主要講的B端SaaS產品。 B端SaaS產品的挑戰 B端SaaS產品為企業提供協同辦公的工具, ...


  任何事物都在變化著包括領域驅動設計這門學問。Evans在首次提到DDD概念後,後來出現了陸續又出現了很多的專家與學者對其理論進行了擴充比如:“領域事件”、“事件源”、“命令查詢責任分離”等。也正是由於這些補充,不僅讓DDD的適用範圍變得更大也讓後來出現的微服務架構系統受益良多,為系統落地提供了非常優秀的理論指導。這節我們主要討論領域事件,不誇張的說,在現代化的業務系統中它的應用普度度非常高,將其看成一種事實上的標準也並不為過。尤其在使用基於Saga的分散式事務時,領域事件完全是不能少的。此外,DDD中不推薦一個事務更新多個聚合,那如果有這種需要的時候要怎麼做呢?答案還是“領域事件”,所以讓我們開始今天的學習之旅。

一、概覽

  主流的基於事件的業務處理流程大概如下圖所示。為什麼說是主流呢?有些特殊情況下可能會使用多線程+遠程服務調用的方式進行事件的投遞,但這種情況大多都發生在遺留的系統中。很多系統中早已經引入了消息隊列中間件或者一些消息隊列組件,使用它們作為消息的載體已經是主流。所以後續的內容中一旦涉及到消息的投遞我們預設就是指使用消息隊列 。

 

  單體時代,想要實現模塊間的交流最簡單的方式是通過進程內函數調用,比較直觀,程式員用起來也更方便。到了微服務的時代,由於業務被劃分到多個獨立部署的服務中,想要實現業務串聯方式之一是使用進程間通訊技術比如RPC或基於HTTP調用。但使用遠程調用的方式所帶來的隱患比較多,一是由於同步的調用會產生性能瓶頸,其實基於進行內調用也是一樣,單線程情況之下整個業務執行的時間等於其所調用的所有方法的執行時間之和; 二是分散式部署的服務需要通過網路連接進行協作,你不能假設網路是穩定的,而不穩定的網路所帶來的隱患也很多,比如性能、後期運維等。所以使用消息及消息隊列中間件作為服務間的信息交換方式成為另外一種主流,不論是在微服務的內部還是在微服務之間。而且呢,由於各服務都是與消息中間件進行交互也不用知道其它服務的地址,能大大減少服務間的相互依賴(即使引入了服務治理工具也不代表沒有依賴,而是服務的客戶端不再像過去一樣需要瞭解服務端的IP地址和埠等信息)。引入領域事件的另一個優勢就是系統的擴展性被增強:在使用基於遠程調用的方式實現某個業務時,當業務需要進行擴展時很多時候你需要增加對另外的服務的調用;而使用事件的機制,您只需要再引入一個事件的監聽者即可,成本非常低,也符合了我們所追求的“開閉原則”。雖然消息這種方式看起來要美好很多,但需要額外引入新的消息中間鍵,必然會加大學習與運營的成本。不過這個賬得看你怎麼算,通過硬體與人員的投入雖然有額外的支出,但能讓系統更加穩定,吞吐量更高,實際上又節約了成本。再說了,為了應對請求的高峰有的時候你必須要引入消息隊列進行緩衝以實現削峰填谷。事件本質上不就一種消息嗎?大部分情況下可以復用系統中的基礎設施,反正一個羊是趕,兩個羊也是放,也不差領域事件那點消耗。

領域事件的提出其實是在Evans那本書之後,有的時候我在想:在沒有領域事件的情況下,他是如何處理多聚合的協作呢?猜測的結果有兩個:一是和當時的時代背景有關,03或04年他提出這個概念,當時單體是主流並不會有那麼多的子服務存在,因此在實踐中應該是允許一個事務更新多個聚合的,也就是通過應用服務完成聚合的協作。二是當時EJB比較流行,裡面有企業消息匯流排的使用,可以通過它實現聚合間的協作,但作者並未給消息賦予領域事件之名。具體原因不可考,總得來說領域事件的使用的確讓哪怕技術一般的團隊也能開發出較高吞吐量的系統。

二、領域事件本質

  領域事件的本質需要從兩個維度進行說明:業務與技術。在業務方面,領域事件表達了在領域中發生的某些事件,為了表達這個事件我們對其進行了建模並使其成為通用語言的一部分。單純的構建一個領域事件其實沒什麼作用,在業務中由於某個領域對象的動作被觸發會引發與之關聯的另外的領域對象也受到影響,那麼我們要怎麼通知受波及的對象呢?答:領域事件。通過領域事件我們可以驅動業務的流向。其實您仔細想一想會發現很多的業務都是由於某個事件的發生而推動其流程前進的,所以我有的時候在想“基於事件的架構”是不是更符合業務本質或者說更有助於系統的實現。此外,在領域驅動設計中還有一種架構風格叫“事件溯源(ES)”,其也使用領域事件,雖然在架構風格和開發風格上有別於我們傳統的模式,但其本質上也是由事件進行驅動的,只不於更註重於實體驅動實體屬性的變更。

  有這樣的一個需求:“訂單支付後需要給其所屬賬戶增加10點成就值”。在使用微服務架構的系統下,您可以很明顯的看出來系統中應該包含兩個服務:“訂單服務”用於處理訂單相關的業務; “賬戶服務”用於處理成就值業務。這段需求中您也可以發現一個明顯的領域事件“訂單支付後”。在引入了領域事件後這個業務的處理流程可分解為:訂單服務在訂單支付後產生“訂單支付”事件;賬戶服務可以根據事件觸發積分邏輯。此處,為了實現事件在服務間的投遞通常會引入事件發佈與訂閱組件,具體細節後面說明。因為領域事件的引入,您可以讓微服務系統發揮出最大的效能,每個系統都專註於完成各自的責任;從技術的角度來看由於使用了消息隊列,整個業務的執行也會由原來的同步變為非同步,性能更高。代碼案例如下所示。

public class OrderService {
    public void pay(Long orderId, Money cost) {
        Order order = this.orderRepository.findBy(orderId);
        OrderPaid orderPaid = order.pay(cost);
        this.eventBus.post(orderPaid);
    }
}
public class AccountService {
    public void handle(OrderPaid orderPaid) {
        Account account = this.accountRepository.findBy(orderPaid.getAccountId());
        account.increaseRewardPoints();
    }
}

  讓我們再進行一個反推,如果沒有領域事件要如何處理示例業務呢?您需要在應用服務中在執行訂單的支付業務後再通過遠程調用的方式讓賬戶服務執行積分的增加,大致的代碼如下所示。

public class OrderService {
    public void pay(Long orderId, Money cost) {
        Order order = this.orderRepository.findBy(orderId);
        order.pay(cost);
        this.remoteAccountService.increaseRewardPoints(10L);
    }
}

  哪種代碼更好一點?目測還是使用領域事件的方案更優秀:非同步操作,性能是杠杠的。遠程調用的方式就差了點意思,案例中只展示了基本的邏輯,如果想要確保“訂單支付後需要給其所屬賬戶增加10點成就值”這個業務能夠順利完成,你還得加上一個分散式事務,這可就複雜了。當然了,使用了領域事件的方式你也得做一些工作來保證消息不丟失。但總得來看方案二要複雜一點,如果一個業務涉及到多個服務共同參與才能完成,那這個性能低得可就不是一點半點了。是不是在您的心裡已經首先把方案二給否了?我這性子已經夠急了,您這比我還急。先彆著急下結論,親!具體使用哪種方案還得看需求呢,請聽我慢慢道來。

  首要的一點,您心裡得有一個譜,咱們這個案例是基於微服務風格的,那考慮問題的時候就得站在微服務的角度而不能仍然使用單體的思維來看待問題,說白了就是需要把眼光放寬一點。分散式系統有一個重要的特性您時刻都不能忘掉的即“CAP”,大師已經證明瞭您只能選擇一種,要不是“AP”要不就是“CP”。不僅是那些我們常用的中間件如此,您所做的業務系統也需要一同考慮。為什麼很多人會忽略這一點?因為我們使用的這些中間件也好,工具也好,人家已經幫你決定了到底“AP”或“CP”。比如Zookeeper,雅虎幫您確認這個就是“CP”的,用戶不用操心這些事情,直接使用即可。這種問題造成了很多的軟體工程師在建設分散式系統的時候時常忽略“CAP”這個東西,也就造成了對於上述的案例先入為主的認為方案一比較好。那為什麼我說評估方案的好壞要看業務需求呢?假如業務強烈要求你必須要保證賬戶的積分必須與訂單支付保持同步,那方案二才是首選。當然,這裡所謂的“強烈要求”需要工程師做好判斷,從用戶的角度來看他們肯定要求數據需要時刻保持同步尤其是不懂技術的客戶,可是大多數的時候其實他們是容忍這種同步存在著延遲的。可以假想一下,如果沒有系統的支撐,通過手工來實現業務是不是也存在不一致呢?說到這裡您應該知道為什麼DDD強調最終一致性了吧?因為的確是大多數情況下不需要嚴格保持數據的強一致性的。我在前面的文章中曾強調過在微服務風格系統中使用Saga代替強分散式事務是一種事實上的標準,也是由於業務的特性造成的,也就是說大多數業務其實只要實現AP就足夠了。不過話又得說回來了,假如你做的系統出現長時間的數據不一致比如一天,那您也別怪用戶懟你,誰也不能容忍如此誇張的延遲,我們所說最終一致性雖然沒有一個標準規定這個最終要經歷多久,那也不能幾小時、幾天都不一致吧?

  以DDD的眼光來看,其實方案二的問題是在建模上,沒有對於需求中的“訂單支付後”這個動作進行建模,不夠純粹。而領域事件的好處是其能夠更加精確的表達通用語言。使用了領域事件後,您可以在需求中提煉出很多的領域模型,這樣會使得建模的工作做得很細緻,十分有利於挖掘到業務的本質。當然,這話就有點虛了,具體的好處是你對業務本質認識的越清楚做出的系統就會更加健壯,可擴展性也更強。寫了這麼多東西,其實雖然只有這一句話“領域事件能夠更加精確的表達通用語言”對應了標題,不過那些陪襯的內容也是精華,加緊找個小本本兒記下來。

三、領域事件與領域命令

  領域事件從技術的角度來看其實就是消息,類似的還包括領域命令,說白了就是給消息一個業務術語(使用消息表示兩者是比較普遍的情況,我們此處只談主流的使用方式)。可就是這些術語才能對應我們的主題“領域驅動設計”,叫“消息驅動”總是差點意思。讓我們先解釋一下這兩者的異同。

  相同方面:1)兩者都需要使用通用語言來命名;2)都是對動作的建模,只不過一個表示已經發生,一個表示未發生;3)一般都以消息的方式來實現;4)都需要遵從相同的使用約束比如都應該放到BO層中;不應當在其中放入領域實體;5)一般都會觸發額外的業務動作;6)針對兩者的投遞方式,主流方式是使用消息隊列。

  不同方面:1)從業務上來看兩者所表達的含義完全不同。領域事件表示某個已經發生的業務動作,是對於發生後的事件的建模;而領域命令所表示的動作還尚未發生;2)語義不同,事件所觸發的動作具備被動色彩:某些業務動作被引發是由於某個事件發生了。您稍微註意一下會發現我這裡使用了“某些業務動作”,說明一個事件可能觸發多個業務行為。此外,事件的發佈方在生成事件後並不期待事件的訂閱方給出響應。領域命令在業務上表示主動的含義。命令產生方主動的發起某個動作,它十分期待收到命令的那個接收者給出響應,比如通過消息隊列給出一個響應事件。這裡還是需要註意一下命令的接收者數量:只能有一個。

  使用領域命令的場景以我個人的經歷沒法概括出全部,但在此列出有代表性的且經過個人實踐過的兩點:1)CQRS架構的應用,一般C端面使用非同步的領域命令。因為使用了這種架構一般是由於高併發的需要,使用非同步的消息模式能更好的應對;2)Saga,Saga的使用模式是接收事件併發送命令。使用事件的場景相對就會普遍很多,我覺得在使用DDD的戰術方式進行系統建設的時候幾乎多多少少的都會涉及到 ,最起碼在有事務需求的時候少不了。

  理論說得天花亂墜,那麼領域事件到底如何產生呢?咱們這不是嚴謹的學術型文章,所以我基於日常的實踐總結出兩種方式:1)領域模型或服務在做出某個動作後,將事件以返回值的形式生成;2)領域事件的組成需要的信息相對複雜,需要在應用服務中進行構建。方式一我在前面展示過代碼此處便不再重覆說明,方式二如下列代碼所示。“(1)”部分所使用的“ApplyFormTerminated”事件需要“OperatorInfo”信息,而這個信息並不參與業務邏輯,所以我們直接使用事件的構造函數在應用服務中創建。

public CommandHandlingResult terminate(Long id, OperatorInfo operatorInfo) {    
    OprApplyForm oprApplyForm = this.oprApplyFormRepository.findBy(id);
    if (oprApplyForm == null) {
        throw new InvalidOperationException(OperationMessages.APPLY_FORM_NOT_EXIST);
    }

    oprApplyForm.terminate();

    TransactionScope tScope = TransactionScope.create(UnitOfWorkFactory.INSTANCE, oprApplyFormRepository);
    this.oprApplyFormRepository.update(oprApplyForm);
    CommitHandlingResult commitResult = tScope.commit();
    if (commitResult.isSucceed()) {
        this.localEventBus.post(new ApplyFormTerminated(operatorInfo, oprApplyForm.getId())); // (1)
    }
}

四、事件的組成

  事件本質上是一個實體對象,正常情況下不會在裡面加入業務方法,即便有也不能修改其內部的屬性。我個人在用的時候還會將其當作DTO一般來看待並讓其具備值對象的不變特性,不會將事件作為某個實體的屬性,也不會在其中嵌入任何的實體或值對象,所有的屬性皆使用基本類型。實踐中,我們一般會給事件一些公共屬性如事件源即由誰來觸發的事件、事件產生的日期、事件ID等、請參看如下示例。

public class DomainEventBase {
    private String sourceService;
private Object sourceAggreateId;
private String id; private Date occurredOn; }

  此處我多廢話兩句。針對事件的來源“sourceService”,我一般情況下會把產生事件的類的全名+服務名賦給它。有的時候我們在應用中會發佈各種各樣的事件,在排查問題的時候你都不知道這個事件到底是誰發出來的,又沒有文檔來作為指導,項目著急上線也沒人寫那個東西。大多數文檔都是系統上線後、驗收前後補的,做過開發的人你懂的……。這個欄位可以很有效的幫助排查問題。“sourceAggreateId”表示產生這個事件的聚合的ID。註意一點,我們這裡把事件稱之為“領域事件”,表示其作用範圍在整個領域內。比較現實的情況是並不是所有的限界上下文的實現都使用對象驅動的方式,存在著大比例數量的服務使用了事件腳本。在這種情況下雖然沒有聚合的概念但不代表不能產生事件,所以我一般也會把某個數據實體的ID賦給“sourceAggreateId”。最後要說的是“id”這個屬性,表示事件的ID,建議把它加到事件中。因為對於事件的冪等性處理幾乎是一種事實上的標準,您可以使用一些業務信息作為冪等的判斷標準,也可以使用事件ID,比如把它放到Redis中。收到事件後可以判斷ID是否在Redis中存在來決策是否要正常的處理這個事件。

五、事件的載體

  前面我們說過事件在技術上可以等同於消息,不過並不是一個嚴格的定義。你當然可以使用比如REST進行事件的傳輸,這種方式雖然能滿足通用語言的需要但不能享受事件所帶來的性能上的提升。既然主流的使用方式是消息隊列 ,那我們在實踐其實有很多的選擇。可以使用基於記憶體的BlockingQueue、Guava EventBus,也可以使用大型的分散式消息隊列如Kafka、RabbitMQ等。涉及消息中間件的部署與結構不是本文的重點,所以我們只談應用。這兩種方式在實踐中我都使用過,基於記憶體的自治性很好,也就是說你不需要依賴於外部的消息隊列,不會因為隊列出現問題而導致應用不可用。基於記憶體的優勢還在於你通常情況下只需引用一個Jar包即可,拎包入住,在不怕消息丟失的場景這是一個很好的選擇。所以您在使用前要評估一下是否可以容忍消息的丟失,畢竟應用一重啟消息也就丟了。但無論如何最好別自己寫一套新的,好多的現成工具可用何必重新造輪子,你能保證你寫得一定比Guava EvenBus好?

  另外一點就是消息隊列的可靠性需要多加思考,比如如何避免消息的丟失就是一個很值得投入精力的地方。當然,想保障消息不丟失,首先在消息隊列中間件的選擇上就不能隨意了。你整個記憶體型的消息隊列還要要求消息處理的可靠性基本上沒戲。我個人經歷的項目中使用過兩種分散式MQ:RabbitMQ和Kafka,在此我們只以前者為例介紹一下如何保障消息的不丟失。通常下我們可選擇三種方式來進行保障:1)生產者使用Confirm機制,出現投遞問題後將消息寫入到資料庫以用於重試;2)配置消息隊列的時候開啟“Durable”模式並將消息在伺服器端進行存儲(註意:此處使用的是消息隊列集群,單實例無論你怎麼折騰都沒戲);3)消費者開啟ACK機制。這裡面的前兩點消息隊列都可以幫忙實現,而在消費端的消息不丟除了ACk能起到部分作用外,還需要消費者進行保障,簡單來說只要消息到達消費者就必須保障其成功的處理,類似於“TCC”事務中的“Confirm”處理。這一點不僅是針對RabbitMQ,包括Kafka、RocketMQ等都是一樣的要求。

  還有一點需要著重說明:在消息的發送端僅使用“Confirm”機制是不能保障消息完全不丟失的。比如下列代碼。“(1)”處的代碼提交了一個資料庫的事務,假如此刻系統掛掉,事件也就一併丟失了。這種情況比較極端但不代表不發生。據小道消息說“本地消息表”方案可以解決這個問題,但到底要不要真的引入還請慎重。我們在生產者、消費者和消息隊列配置上下得功夫已經不少了,已經能大大的保障消息不丟。而引入本地消息表又要做很多的工作。所以在考慮人工的介入還是嚴格的系統約束間要找到平衡,儘管作為一個技術人員我不應該說這種不負責任的話,但實現本來與理想就是存在差距的。

public class orderService {
    public void pay1(Long orderId, Money cost) {
        Order order = this.orderRepository.findBy(orderId);
        OrderPaid orderPaid = order.pay(cost);
        
        this.orderRepository.update(order);
        this.uniteOfWork.commit(); // (1)
        
        this.eventBus.post(orderPaid);
    }
}

  其實我個人也經常在項目中使用記憶體型的消息隊列Guava EvenBus,當時的使用場景是對業務告警進行接收並用於後續的處理。雖然可能面臨消息丟失風險,但偶然丟個一條兩條其實也不會造成多大的影響。因為業務異常有一個特性:其往往是重覆錯誤,丟失部分消息並不會有多大的問題。之所要提到這個事情其實就是想提醒讀者在項目建設的時候要一定要考慮系統建設的成本,原則上我們肯定要求不能有任何消息的丟失,但這個事情得從兩個方面看而且絕對不可以上綱上線,極左或極右都不可能把事情做好。

六、事件處理

  我們已經說過,一個事件會有多個訂閱者。 在六邊型架構中,事件的“Adapter”處在架構的左側作為事件的輸入,但您不應該在Adapter中完成事件的處理而是應該和一般的REST調用一樣使用應用程式服務進行業務的協調處理。這裡有一點需要特別的註意即事件的“冪等性”,實際上在基於消息的業務場景中大部分情況下都需要考這個事情 。可能由於網路、消息組件和消費者處理異常等原因需要進行消息的重發;當事件有多個訂閱方的時候,如果有一個訂閱方出現失敗可能也需要進行業務補償,而最簡單的補償方式就是把事件重發一次。總之呢,同一個消息被重覆的收到多次是非常常見的場景,那您在使用的時候就必須要投入精力做好保障。前面我們曾經說過,您可以給事件一個唯一ID比如“UUID”併在消費端把ID進行存儲以達到排重的目的;您也可以通過使用業務標記進行排除,這種方式在使用Saga的時候會經常被使用以達到事務的隔離效果。下麵代碼片段來自於我曾經做過的一個項目,此處使用業務信息來決策某個事件是否被收到過如“(1)”處。

public void handle(WorkOrderAccepted workOrderAccepted) {
    if (this.status == ResourceBuildStatusEnum.UN_START) { // (1)
        this.status = ResourceBuildStatusEnum.SAVING_WORK_ORDER;
        this.updatedDate = new Date();
        this.message = this.status.getDescription();

        SaveWorkOrder saveWorkOrder = new SaveWorkOrder();
        saveWorkOrder.processManagerId = this.getId();
        this.commands.add(saveWorkOrder);
    }
}

  針對事件的存儲,這個其實要看具體的需要。如果不是使用ES架構的服務,至少要對核心的事件進行持久化,十分有利於後續系統的運維。由於事件是只讀的,其存儲的記錄也不會進行更改。所以不論是使用MySQL這種關係型數據還是使用MongoDB這種NoSQL,並沒有太大的限制,主要看您的系統現狀。不過在運維工作中有一點請務必要註意:請對事件記錄進行周期性轉存。一是可以方便後續的安全審計,二是可以減少其數據占用量以避免與其它業務數據發生空間爭搶。我個人在使用的時候直接存到了MySQL中,和業務數據進行了分離,每隔一個月備份一次數據。其實也只起到了備份的作用,平常幾乎不查。對了,最好在事件生產側進行存儲,萬一丟了呢。

 七、反思

  微服務架構下的事件使用,存在這樣一個場景,我們還是以本章中的“訂單支付後需要給其所屬賬戶增加10點成就值”這個需求為例。假如訂單服務發佈了一個“OrderPaid”事件,在賬戶服務中要如何進行處理呢?我們是否需要設計一個和“OrderPaid”結構一模一樣的類且保持“OrderPaid”命名不變,簡單來說就是把這個事件的代碼複製到賬戶服務中。另外一個選擇是我們在賬戶服務中建立一個和“OrderPaid”結構一樣但叫做“ChangeRewardPoint”的領域命令,使用命令代替原來的事件來處理“積分變更”這個業務。請發揮您的聰明才智,也期待您的回覆。

總結

  本節講解了領域事件的使用,在實踐中請您結合自身的業務需求尤其是基於“CAP”理論來決策是否應該使用,不要被先入為主的想法矇蔽雙眼。我們還講解了事件的通常結構、事件的載體和事件的存儲。您別一時用得痛快結果由於不能全面考慮造成後續運維成本的加大。我個人的工作經歷中有一段時間是作為運營運維的角色存在,相信您在我的文章中總會看到我會提及系統的運維。個人其實更中意軟體設計與研發的工作,可也正是因為這段運維經歷讓自己在考慮事情的時候不會那麼局限,能夠站在不同的維度去思考。

  客觀來講,基於事件驅動的服務用起來的確很痛快。一是建模的粒度比較細,讓系統的擴展點增加了很多。很多的時候加個功能不過是增加一個事件的消費者而矣,並不會因為新加入的邏輯引發全局BUG或性能損耗。二是系統的性能會有很多的提升,服務解耦處理做得也比較優雅。然而事情有利也有弊,請客觀的、務實的、謹慎的進行選擇。


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

-Advertisement-
Play Games
更多相關文章
  • 技術大咖們從開源實戰項目總結經驗,利用真實場景的應用案例分享前沿技術,引導開發者從零參與 OpenHarmony 開源貢獻,提升代碼效率,培養開發者成為開源社區的貢獻者。 ...
  • 1. 準備階段 關於該功能的實現我們需要學習以下的資料: 1.1 【ARKUI】ets怎麼實現文件操作 1.2 文件管理 1.3 Ability上下文 2. demo 實現 2.1 文件路徑讀取 參考 context.getFilesDir 來進行獲取文件路徑,代碼如下 private getCac ...
  • 今天做了一個案例,可以好好做做能夠將之前的內容結合起來,最主要的是能對組件化編碼流程有一個大概的清晰認知,這一套做下來,明天自己再做一遍複習一下,其實組件化流程倒是基本上沒什麼問題了,主要是很多vue的方法需要多熟悉一下,畢竟打破了之前的一些對於傳統js的認知,還需要多熟悉一下。 這兩天可能內容不是 ...
  • 大家好,我是半夏👴,一個剛剛開始寫文的沙雕程式員.如果喜歡我的文章,可以關註➕ 點贊 👍 加我微信:frontendpicker,一起學習交流前端,成為更優秀的工程師~關註公眾號:搞前端的半夏,瞭解更多前端知識! 點我探索新世界! 原文鏈接 ==>http://sylblog.xin/archi ...
  • 一、主要區別 1、{} 和 new Object() 除了本身創建的對象,都繼承了 Object 原型鏈上(Object.prototype)的屬性或者方法,eg:toString();當創建的對象相同時,可以說 {} 等價於 new Object() 。2、Object.create() 是將創建 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 1. 非同步編程的實現方式? JavaScript中的非同步機制可以分為以下幾種: 回調函數 的方式,使用回調函數的方式有一個缺點是,多個回調函數嵌套的時候會造成回調函數地獄,上下兩層的回調函數間的代碼耦合度太高,不利於代碼的可維護。 Pro ...
  • DOM 事件是處理 Web 頁面交互的基礎,是掌握前端開發技術的基礎。 W3C協會早在1988年就開始了DOM標準的制定,W3C DOM標準可以分為DOM1,DOM2,DOM3三個版本。 1.Html事件處理 原始事件模型,事件處理程式被設置為html控制項的性質值,一般是html控制項的 onclic ...
  • 背景 在我們日常工作中,代碼寫著寫著就出現下列的一些臭味。但是還好我們有SOLID這把‘尺子’, 可以拿著它不斷去衡量我們寫的代碼,除去代碼臭味。這就是我們要學習SOLID原則的原因所在。 設計的臭味 僵化性 具有聯動性,動一處,會牽連到其他地方 脆弱性 不敢改動,動一處,全局癱瘓 頑固性 不易改動 ...
一周排行
    -Advertisement-
    Play Games
  • 分組和樹形結構是不一樣的。 樹形結構是以遞歸形式存在。分組是以鍵值對存在的形式,類似於GroupBy這樣的形式。 舉個例子 ID NAME SEX Class 1 張三 男 1 2 李四 女 2 3 王二 男 1 當以Sex為分組依據時則是 Key Value 男 1 張三 男 1 3 王二 男 1 ...
  • NetCore中將SQLServer資料庫備份為Sql腳本 描述: 最近寫項目收到了一個需求, 就是將SQL Server資料庫備份為Sql腳本, 如果是My Sql之類的還好說, 但是在網上搜了一大堆, 全是教你怎麼操作SSMS的, 就很d疼! 解決方案: 通過各種查找資料, 還有一些老哥的幫助, ...
  • 我的Notion Clowd.Squirrel Squirrel.Windows 是一組工具和適用於.Net的庫,用於管理 Desktop Windows 應用程式的安裝和更新。 Squirrel.Windows 對 Windows 應用程式的實現語言沒有任何要求,甚至無需服務端即可完成增量更新。 ...
  • 轉載請註明來源 https://www.cnblogs.com/brucejiao/p/16188865.html 謝謝! 轉載請註明來源 https://www.cnblogs.com/brucejiao/p/16188865.html 謝謝! 轉載請註明來源 https://www.cnblog ...
  • 1. Netty源碼研究筆記(3)——Channel系列 依舊是通過先縱向再橫向的研究方法,在開篇中,我們發現不管是Sever還是Client,最終的啟動是通過調用channel的對應方法來完成的,而這個動作實際在channel綁定的eventLoop中執行。 接下來,我們繼續EchoSever、E ...
  • 大家好,今天給大家介紹一款輕量、快速、穩定可編排的組件式規則引擎框架LiteFlow。 一、LiteFlow的介紹 LiteFlow官方網站和代碼倉庫地址 官方網站:https://yomahub.com/liteflow Gitee托管倉庫:https://gitee.com/dromara/li ...
  • 我使用Spring AOP實現了用戶操作日誌功能 今天答辯完了,復盤了一下系統,發現還是有一些東西值得拿出來和大家分享一下。 需求分析 系統需要對用戶的操作進行記錄,方便未來溯源 首先想到的就是在每個方法中,去實現記錄的邏輯,但是這樣做肯定是不現實的,首先工作量大,其次違背了軟體工程設計原則(開閉原 ...
  • 《零基礎學Java》 繪製幾何圖形 Java可以分別使用 Graphics 和 Graphics2D 繪製圖形,Graphics類 使用不同的方法繪製不同的圖形(drawLine()方法可f以繪製線、drawRect()方法用於繪製矩形、drawOval()方法用於繪製橢圓形)。 Graphics類 ...
  • 本期教程人臉識別第三方平臺為虹軟科技,本文章講解的是人臉識別RGB活體追蹤技術,免費的功能很多可以自行搭配,希望在你看完本章課程有所收穫。 ...
  • 很多人都喜歡使用黑色的主題樣式,包括我自己,使用了差不多三年的黑色主題,但是個人覺得在進行視窗轉換的時候很廢眼睛。 比如IDEA是全黑的,然後需要看PDF或者WORD又變成白色的了,這樣來回切換導致眼睛很累,畢竟現在網頁以及大部分軟體的界面都是白色的。那麼還是老老實實的使用原來比較順眼的模式吧。 1 ...