又大又粗又猛免费视频久久_国产理论在线播放_久久男人av资源网站免费软件_99国产精品无码

后端開發(fā)就是CRUD?沒那么簡單?。ê蠖碎_發(fā)是啥意思)

作為一個后端開發(fā)者,不時都能聽到這么一種論調(diào):后端開發(fā)沒什么技術(shù)含量,就是CRUD而已。此時,我一般會嘴角抿抿,心里呵呵。

后端開發(fā)就是CRUD?沒那么簡單?。ê蠖碎_發(fā)是啥意思)

事實上,從某種程度上說這種說法并沒錯,我們甚至還可以進一步去挖掘一下其背后更深層次的本質(zhì):軟件就是一個I/O系統(tǒng),后端開發(fā)就是對數(shù)據(jù)的I/O處理而已,只需能把數(shù)據(jù)存起來再放出去即可,的確說不上什么高端可言。此外,在國內(nèi)的大多數(shù)程序員所從事的細分行業(yè)只能說是“應用軟件開發(fā)”或者“業(yè)務軟件開發(fā)”,說白了這些成天處理業(yè)務邏輯的軟件都沒什么難的,就是一些低級邏輯而已,這也是為什么很多非計算機專業(yè)的學生都可以成功轉(zhuǎn)行為程序員的原因(之一)。

然而,同樣一個業(yè)務功能,分別讓兩個工作經(jīng)驗不同的程序員去實現(xiàn),他們的代碼可能完全不一樣。有時,經(jīng)驗少的程序員寫100行代碼就能實現(xiàn)的一個功能,老程序員卻需要寫500行,因為后者考慮到了對各種邊界條件的處理,緩存的使用以及對性能的顧及等。又有時,經(jīng)驗少的程序員寫了500行代碼實現(xiàn)的一個功能,老程序員只花了100行就實現(xiàn)了,因為后者使用了更加優(yōu)秀的算法或者采用了能使代碼變得更加簡潔的工具和原則等。

李書福說:“造車就是一個沙發(fā)加四個車輪”。他說的沒錯,因為這是汽車的某種本質(zhì)。然而,真正要造好一臺汽車,卻需要考慮舒適性、加速性、NVH、操控性、通過性等諸多方面的因素。軟件也一樣,簡單的CRUD操作縱然能夠滿足基本的I/O需求,但是在具體落地時我們還要考慮很多原則和因素以讓人能夠更好地掌控軟件系統(tǒng),其中包含但不限于:高內(nèi)聚低耦合、關(guān)注點分離、依賴倒置、非功能性需求等等。這里所涉及到的一個基本命題是:軟件代碼首先是給人腦看的,其次才是給電腦執(zhí)行的。

在本文中,我們將以一個真實的軟件項目 —— 碼如云為例,系統(tǒng)性的講解后端在處理請求的過程中所需要顧及的方方面面,你會發(fā)現(xiàn)后端開發(fā)絕非單純的CRUD這么簡單。

碼如云(https://www.mryqr.com)是一個基于二維碼的一物一碼管理軟件,技術(shù)上是一個無代碼平臺,全程采用DDD思想進行開發(fā),對DDD感興趣的讀者可以參考我們的DDD系列文章。

接下來,我們將圍繞以下業(yè)務用例展開討論:在碼如云中,成員(Member)可以更新自己的手機號碼,但如果所使用的手機號已經(jīng)被他人占用,則禁止更新。

整個請求處理的流程如下圖所示:

后端開發(fā)就是CRUD?沒那么簡單?。ê蠖碎_發(fā)是啥意思)

概括來看,整個請求處理流程和我們通常的實踐并沒有太大的區(qū)別。首先,請求到達MemberController,這是Spring MVC處理請求的第一站;然后MemberController調(diào)用MemberCommandService完成該業(yè)務用例,調(diào)用時傳入請求數(shù)據(jù)對象ChangeMyMobileCommand,這里的MemberCommandService在DDD中被稱為應用服務;MemberCommandService通過MemberRepository獲取到對應的Member對象,再通過MemberDomainService(在DDD中被稱為領(lǐng)域服務)完成對Member的手機號更新;最后MemberCommandService 調(diào)用MemberRepository.save()將更新后的Member對象保存到數(shù)據(jù)庫。

MemberController

在整個請求處理的過程中,首先通過MemberController接收請求:

@PutMapping(value = "/me/mobile")@ResponseStatus(OK)public void changeMyMobile(@RequestBody @Valid ChangeMyMobileCommand command, @AuthenticationPrincipal User user) { memberCommandService.changeMyMobile(command, user);}

這里,MemberController.changeMyMobile()方法一共只有5行代碼,可不要小瞧這5行代碼,在實際編碼時我們卻需要考慮多個方面的因素:

  1. Spring MVC的Controller是框架直接相關(guān)的,DDD講求業(yè)務復雜度與技術(shù)復雜度的分離,我們希望自己的代碼實現(xiàn)能夠盡快的脫離技術(shù)框架,因此MemberController只起到了簡單的代理作用,也即把請求代理給應用服務MemberCommandService。
  2. 對URL的設(shè)計是有講究的,MemberController采用了REST風格的URL,通過HTTP的PUT方法完成對mobile資源(me/mobile)的更新,更多關(guān)于REST URL的內(nèi)容,請參考這里。
  3. 同樣基于REST原則,更新資源后應該返回HTTP的200狀態(tài)碼,這里通過@ResponseStatus(OK)完成(Spring MVC默認返回的即是200)。
  4. 對于接收到的數(shù)據(jù)請求對象ChangeMyMobileCommand需要加上@Valid以做數(shù)據(jù)驗證,否則后續(xù)對ChangeMyMobileCommand中的各種JSR-303驗證將失效。
  5. MemberController需要返回void,也即不返回任何數(shù)據(jù),這是因為基于CQRS的原則,任何寫數(shù)據(jù)的操作不能同時查詢數(shù)據(jù),反之亦然。

ChangeMyMobileCommand

命令對象ChangeMyMobileCommand用于封裝請求數(shù)據(jù),之所以稱之為命令(Command)是因為一個請求就像外界向軟件系統(tǒng)發(fā)起了一次命令一樣,這里的Command正是來自于CQRS中的“C”。

@Value@Builder@AllArgsConstructor(access = private)public class ChangeMyMobileCommand implements Command { @Mobile @NotBlank private final String mobile; @NotBlank @VerificationCode private final String verification; @NotBlank @Password private final String password; @Override public void correctAndValidate() { //用于JSR-303無法完成的驗證邏輯,但是又不能包含業(yè)務邏輯 }}

ChangeMyMobileCommand 對象主要充當數(shù)據(jù)容器的作用,其中一個比較重要的任務是完成數(shù)據(jù)的初步驗證。具體實踐時需要考慮以下幾個方面:

  1. Command對象通常是不變的(Immutable),在編碼時應將建模為一個值對象,為此我們使用了Lombok中的@Value、@Builder和@AllArgsConstructor(access = PRIVATE)達到此目的。
  2. 對Command對象中的每一個字段,都需要判斷是否需要做驗證,有些字段可以通過簡單的JSR-303內(nèi)建注解完成驗證,比如mobile字段中的@NotBlank,而更復雜的驗證則需要自行實現(xiàn)JSR-303的ConstraintValidator接口,比如mobile字段的@Mobile注解。
  3. 對于Command對象,還需要特別注意其中的容器類字段,比如List和Set等,需要對這些字段做非null檢查(@NotNull),以消除后續(xù)代碼在引用這些字段時有可能的空指針異常NullPointerException。
  4. 對于更加復雜的驗證,比如需要對多個字段進行關(guān)聯(lián)性驗證,通過自定義JSR-303可能比較麻煩,此時可以自定義Command接口,通過實現(xiàn)該接口的correctAndValidate()方法完成驗證目的。
  5. 對于字符串類字段來說,任何時候都需要通過@Size注解對其長度進行限制,除非其他注解中已經(jīng)包含了此限制。

MemberCommandService

應用服務(ApplicationService或者CommandService)是領(lǐng)域模型的門面,任何對領(lǐng)域模型的請求都需要通過應用服務中的公有方法完成。更多關(guān)于應用服務的講解,請參考我們DDD文章系列中的這一篇。

@Transactionalpublic void changeMyMobile(ChangeMyMobileCommand command, User user) { mryRateLimiter.applyFor(user.getTenantId(), "Member:ChangeMyMobile", 5); String mobile = command.getMobile(); verificationCodeChecker.check(mobile, command.getVerification(), CHANGE_MOBILE); Member member = memberRepository.byId(user.getMemberId()); memberDomainService.changeMyMobile(member, mobile, command.getPassword()); memberRepository.save(member); log.info("Mobile changed by member[{}].", member.getId());}

在DDD中,應用服務應該是很薄的一層,因為它不能包含業(yè)務邏輯,而主要是起協(xié)調(diào)的作用,另外事務邊界、鑒權(quán)等操作也會放在應用服務中。在實現(xiàn)時,應該考慮以下幾個方面:

  1. 應用服務不能包含業(yè)務邏輯,這也是很多CRUD程序員經(jīng)常犯的一個錯誤。舉個例子,在本例中,如果成員的手機號已經(jīng)被占用,則禁止更新手機號,這是一個典型的業(yè)務邏輯,因此不應該在MemberCommandService 中完成,而應該放到領(lǐng)域模型中。通常來說,應用服務遵循請求處理“三部曲”原則:(1)獲取需要處理的領(lǐng)域?qū)ο螅ū纠械腗ember),(2)對領(lǐng)域?qū)ο筮M行處理(memberDomainService.changeMyMobile()),(3)將更新后的領(lǐng)域?qū)ο蟊4婊財?shù)據(jù)庫(memberRepository.save())。
  2. 應用服務中的公共方法應該與業(yè)務用例一一對應,而每個業(yè)務用例又對應一個數(shù)據(jù)庫事務,因此應用服務應該是事務的邊界,也即Spring的@Transactional注解應該打在應用服務的公用方法上。
  3. 與Controller一樣,應用服務中負責寫操作的方法不能返回查詢數(shù)據(jù),而負責查詢的方法不能更改數(shù)據(jù)。
  4. 應用服務應該是獨立于技術(shù)框架(本例的Spring)的,如果把領(lǐng)域模型比作CPU中的芯片,那么應用服務便是CPU引腳,整個CPU放到不同的電腦主板(類比到技術(shù)框架)中均能正常使用。不過,在實際的編碼過程中,我們做了一些妥協(xié),比如在本例中,@Transactional 則是來自于Spring的,不過總的原則是不變的,即應用服務(以及其所包圍著的領(lǐng)域模型)盡量少地依賴于技術(shù)框架。
  5. 一些非業(yè)務性的功能也應該在應用服務中完成,比如對請求的限流(本例中的mryRateLimiter ),限流處理原本可以放到技術(shù)框架中統(tǒng)一處理的,不過由于碼如云是一個SaaS軟件,需要對不同的租戶單獨限流,因此我們將其放在了應用服務這一層。
  6. 一般來講,對權(quán)限的檢查也可以放在應用服務中;不過不同的人對此有不同的看法,有人認為權(quán)限也屬于業(yè)務邏輯,因此應該放到領(lǐng)域模型中,而另外有人認為權(quán)限不是業(yè)務邏輯,應該被當做一個單獨的關(guān)注點來處理。在碼如云,我們選擇了后者,并且將對權(quán)限的處理放到了應用服務中。

MemberRepository

資源庫(Repository)的、可以認為是對數(shù)據(jù)庫的封裝和抽象,有些類似于DAO(Data Access Object),不過它們最大的區(qū)別是資源庫是與DDD中的聚合根一一對應的,只有聚合根對象才“配得上”擁有資源庫,而DAO則沒有此限制。更多關(guān)于資源庫的內(nèi)容,可以參考這里。

public interface MemberRepository { boolean existsByMobile(String mobile); Member byId(String id); Optional<Member> byIdOptional(String id); Member byIdAndCheckTenantShip(String id, User user); boolean exists(String arId); void save(Member member); void delete(Member member);}

在實現(xiàn)資源庫時,應該考慮以下幾個方面:

  1. 只對聚合根對象創(chuàng)建相應的資源庫,并且其操作的對象是以聚合根為單位的。
  2. 資源庫不能包含太多的查詢方法,大量的查詢操作可能意味著對領(lǐng)域模型的污染,此時可以考慮通過CQRS將查詢操作繞過資源庫單獨處理。
  3. 資源庫通常分為接口類和實現(xiàn)類,接口類是屬于領(lǐng)域模型的一部分,而實現(xiàn)類則應該放到基礎(chǔ)設(shè)施中,落地時接口類應該放到domain分包下,而實現(xiàn)類應該放到infrastructure分包下,這也意味著,資源庫的實現(xiàn)是“可插拔”的,即如果將來要從MySQL遷移到MongoDB,那么只需要新添加一個基于MongoDB的資源庫實現(xiàn)類即可,其他地方可以不變。
  4. 資源庫中不能包含業(yè)務邏輯,其完成的功能只限于將數(shù)據(jù)從內(nèi)存同步到數(shù)據(jù)庫,或者反之。

MemberDomainService

與應用服務不同的是,領(lǐng)域服務(DomainService)屬于領(lǐng)域模型的一部分,專門用于處理業(yè)務邏輯,通常被應用服務所調(diào)用。在本例中,我們使用MemberDomainService 對“手機號是否已經(jīng)被占用”進行檢查:

public void changeMyMobile(Member member, String newMobile, String password) { if (!mryPasswordEncoder.matches(password, member.getPassword())) { throw new MryException(PASSWORD_NOT_MATCH, "修改手機號失敗,密碼不正確。", "memberId", member.getId()); } if (Objects.equals(member.getMobile(), newMobile)) { return; } if (memberRepository.existsByMobile(newMobile)) { throw new MryException(MEMBER_WITH_MOBILE_ALREADY_EXISTS, "修改手機號失敗,手機號對應成員已存在。", mapOf("mobile", newMobile, "memberId", member.getId())); } member.changeMobile(newMobile, member.toUser());}

在實踐時,使用領(lǐng)域服務應該考慮到以下幾個方面:

  1. 領(lǐng)域服務不是必須有的,而是只有當領(lǐng)域模型(準確的講是聚合根)無法完成某些業(yè)務邏輯時才出現(xiàn)的,是“不得已而為之”的結(jié)果。在本例中,檢查“手機號是否被占用”需要進行跨聚合(Member)的操作,光憑當事的Member是無法做到這一點的,此外這種檢查有屬于業(yè)務邏輯的一部分,因此我們創(chuàng)建一種可以處理業(yè)務邏輯的服務(Service)類來解決,這個服務類即是領(lǐng)域服務。在很多項中,應用服務和領(lǐng)域服務揉雜在一起,功能倒是實現(xiàn)了,但是各組件之間的耦合也加深了,導致的結(jié)果是軟件在未來的演進中將變得越來越復雜,越來越困難。
  2. 領(lǐng)域服務的職責最多只到更新領(lǐng)域模型在內(nèi)存中的狀態(tài),而不包含保存領(lǐng)域模型的職責,比如在本例中,MemberDomainService 并不調(diào)用memberRepository.save(member)來保存Member,而是由應用服務MemberCommandService負責完成。這樣做的好處是將領(lǐng)域服務建模為一個僅僅操作領(lǐng)域模型的“存在”,使其職責更加的單一化。

Member

領(lǐng)域?qū)ο?/span>(Domain Object)是業(yè)務邏輯的主要載體,同時包含了業(yè)務數(shù)據(jù)和業(yè)務行為。在本例中,Member對象則是一個典型的領(lǐng)域?qū)ο螅贒DD中,Member也被稱為聚合根對象。Member對象實現(xiàn)修改手機號的代碼如下:

public void changeMobile(String mobile, User user) { if (Objects.equals(this.mobile, mobile)) { return; } this.mobile = mobile; this.mobileIdentified = true; raiseEvent(new MobileChangedEvent(this.getId(), mobile));}

在實現(xiàn)領(lǐng)域?qū)ο髸r,應該考慮以下幾個方面:

  1. 忘掉數(shù)據(jù)庫,不要預設(shè)性地將領(lǐng)域模型中的字段與數(shù)據(jù)庫中的字段對應起來,只有這樣才能夠做到架構(gòu)的整潔性以及基礎(chǔ)設(shè)施中立性,正如Bob大叔所說,數(shù)據(jù)庫是一個細節(jié)。
  2. 領(lǐng)域模型應該保證數(shù)據(jù)一致性,比如在修改訂單項時,訂單的價格也應該相應的變化,那么此時所有相關(guān)的處理邏輯均應該在同一個方法中完成。在本例中,手機號修改了之后,應該同時將Member標記為“手機號已記錄”狀態(tài)(mobileIdentified ),因此對mobileIdentified 的修改應該與對mobile的修改放在同一個chagneMyMobile()方法中。在DDD中,這也稱為不變條件(Invariants)。
  3. 在實現(xiàn)領(lǐng)域邏輯的過程中,還會隨之產(chǎn)生領(lǐng)域事件(Domain Event),由于領(lǐng)域事件也是領(lǐng)域模型的一部分,因此一種做法是領(lǐng)域?qū)ο笤谕瓿蓸I(yè)務操作之后,還應發(fā)出領(lǐng)域事件,即本例中的raiseEvent(new MobileChangedEvent(this.getId(), mobile));更多關(guān)于領(lǐng)域事件的內(nèi)容,請參考這里。
  4. 領(lǐng)域?qū)ο蟛荒艹钟谢蛞闷渌愋偷膶ο?,包括應用服務,領(lǐng)域服務,資源庫等,因為領(lǐng)域?qū)ο笾皇歉鶕?jù)業(yè)務邏輯的運算完成對業(yè)務數(shù)據(jù)的更新,也即領(lǐng)域?qū)ο髴摻?span id="kjtcpxx" class="candidate-entity-word" data-gid="8948897">POJO(Plain Old Java Object)。
  5. 同理于應用服務,Member.changeMobile()方法是個寫操作,不能返回任何數(shù)據(jù)。

總結(jié)

在文本中我們看到,哪怕是一個諸如“用戶修改手機號”這樣簡單的需求,在整個實現(xiàn)過程中需要考慮的點也達到了將近30個,真實情況只會多不會少,比如我們可能還需要考慮性能、緩存和認證等眾多非功能性需求等。因此,后端開發(fā)絕非CRUD這么簡單,而是需要將諸多因素考慮在內(nèi)的一個系統(tǒng)性工程,還是那句話,有講究的編程并不是一件易事。

相關(guān)新聞

聯(lián)系我們
聯(lián)系我們
在線咨詢
分享本頁
返回頂部