情侣网名,如何克服软件开发的复杂性?-安博电竞 官网_安博电竞入口_安博电竞网页版

西甲联赛 288℃ 0

【CSDN编者按】在开发软件的进程中,咱们会遇到许多困难,例如需求不明确、交流不畅、开发进程不顺利等等。此外,咱们还面临一些技能难题,比方留传代码拖后腿、扎手的规划扩展、遇到一些以往的过错决议。全部这些问题都能够得到处理或削减,可是有一个咱们力不从心的底子问题:体系的杂乱性。在本文中,咱们将以构建一个办理信用卡的 Web 运用程序为例,手把手教你怎样编写一套更经用且易于重构的代码。

作者 | Anish Karandikar、Roman

译者 | 弯月,责编 | 屠敏

在参加了多起项目之后,我留意到每个项目都存在一些共通的问题,不管项意图范畴、架构、代码约好等等。尽管这些问题并不具有挑战性,仅仅一些庸俗的例行程序,但意图是为了保证你不会犯任何显着的过错。我不想日复一日重复这些例行程序,我却是很期望找到处理方案:一些开发办法、代码约好或其他任何能够协助我避免这些问题发作的办法,这样在规划项意图时分,我就能够专心做自己感兴趣的作业了。这便是本文的方针:描绘这些问题,并向你展现我处理这些问题的东西和办法。

咱们面临的问题

在开发软件的进程中,咱们会遇到许多困难,例如需求不明确、交流不畅、开发进程不顺利等等。此外,咱们还面临一些技能难题,比方留传代码拖后腿、扎手的规划扩展、遇到一些以往的过错决议。全部这些问题都能够得到处理或削减,可是有一个咱们力不从心的底子问题:体系的杂乱性。不管你了解与否,你正在开发的体系总是很杂乱。即便你仅仅在做一个烂大街的CRUD运用程序,也总是会遇到一些极点状况,一些扎手的作业,并且还有人不时地问:“假如我在这种状况下这样做会怎样样?”你或许会说:“嗯,这是一个好问题。容我想想……”你都需求细心考虑那些扎手的事例,含糊不清的逻辑、验证和拜访办理等等。咱们常常遇到某个主意过于庞大,无法一个人独自策划,而这种状况往往会衍生出误解等问题。可是,假定这个范畴专家和事务分析师团队明晰地交流并发作了哆嗦功教育视频一同的需求。现在咱们有必要完结这些需求,经过代码表达这个杂乱的主意。而代码是另一个体系,比咱们原始的主意更杂乱。怎样会这样?这便是实践:技能上的局限性迫使你在完结事务逻辑的根底之上,还要处理处理高负载、数据一同性和可用性。如你所见,这项使命的难度十分大,现在咱们需求恰当的东西来处理。编程言语仅仅一种东西,就像其他东西相同,你不只需求考虑编程言语自身的质量,并且还要挑选合适作业的东西。工欲善其事必先利其器!

技能介绍

现在,面向方针的编程言语很流电讯数码行。当有人介绍OOP时,一般他们会运用这样一个比方:假定有一辆来自实践国际的轿车,轿车有品牌、分量、色彩、最大速度、当时速度等各项特点。为了在咱们的程序中反映这个方针,咱们需求树立一个类搜集这些特点。特点能够是永久的或可变的,全部特点一同构成该方针的当时状况和改变的一些鸿沟。可是,仅组合这些特点还不行,咱们有必要查看当时状况是否有意义,例如当时速度有没有超越最大速度。为了向这个类增加一些逻辑,咱们需求情侣网名,怎样战胜软件开发的杂乱性?-安博电竞 官网_安博电竞进口_安博电竞网页版将特点标记为私有以避免其他人创立不合法状况。可见,方针的中心便是它们的内部状况和生命周期。因而,在这种状况下,OOP的三个支柱彻底合理:运用承继来重用某些状况操作;经过封装保护状况;以相同办法处理相似方针的多态性。默许可修正也很合理,由于在这种状况下,不行变方针无法具有生命周期,并且一向处于同一个状况,这并非是常态。

可是,现在常见的Web运用程序并不处理方针。咱们代码中的全部东西都有永久的生命或底子没有生命。两种最常见的“方针”类型是某种服务,比方UserService、EmployeeRepository、某些模型/实体/ DTO或任何服务。服务的内部没有逻辑状况,它们每次发作和消亡进程彻底相同,咱们只需树立新的数据库衔接偏从头创立依靠联系图。实体和模型没有任何附加行为,它们仅仅数据绑缚,它们的可修正性无关紧要。因而,在开发这种运用程序时,OOP的要害特性并没有用武之地。

常见的Web运用程序中需求处理的是数据流:验证、转化、评价等。合适这种作业的编程言语类型为函数式编程。事实证明,现在盛行言语中的全部现代功用都来自:async / await、lambdas和delegates、呼应式编程、可辨认联合(swift或rust中的枚举,不要与java或.net中的枚举混杂)、元组——全部这些都来自函数式编程。可是,这些仅仅其间一部分,除此之外还有许多许多。

在深化评论之前,还需求告知一点。切换到新言语,尤其是新编程范式,是对开发人员的出资,也是对事务的出资。即便出资过错也不会带来任何费事,但合理的出资能够让你获益无量。

东西介绍

许多人喜爱静态类型的编程言语。原因很简略:编译器担任繁琐的查看,例如将正确的参数传递给函数,正确地结构实体等等。这些查看都是自带的。至于编译器无法查看的东西,咱们能够挑选听其自然,或编写测验。编写测验意味着本钱,并且每项测验都不是一次性的,你还需求付出保护的本钱。此外,人们都比较粗枝大叶,所以每隔一段时刻咱们就会遇到假阳性和假阴性的测验成果。你编写的测验越多,测验的均匀质量就越高。还有另一个问题:为了某些测验,你有必要记住测验哪些功用,体系越大越简略漏掉某些功用。

可是,编译器只能发挥与言语的类型体系相同的功用。假如它不承受静态办法的表达,则有必要在运转时履行此操作。这种状况下你也需求测验。这不只仅与类型体系有关,语法和语法糖特征也十分重要,由于咱们必定期望编写尽或许少的代码,所以假如某些办法需求编写10倍的代码,那么就没人用。这便是为什么你需求挑选具有合适的特征和技巧的言语。不然的话,你不只无法运用言语的功用来对立原有的困难(比方体系的杂乱性和不断改变的要求),你还需求与言语自身作斗争。由于你需求为开发人员花费的时刻付出费用,所以这全部归根结底都是白花花的银子。开发人员需求处理的问题越多,他们需求的时刻就越多,你需求的开发人员就越多。

最终,让咱们来看看实践的代码。我是一名.NET开发人员,因而以下代码示例将用C#和F#编写,但即运用其他盛行的OOP和函数式编程言语编写,状况也差不多。

下面开端写代码

咱们将构建一个办理信用卡的Web运用程序。底子需求如下:

  • 创立/读取用户

  • 创立/读取信用卡

  • 激活/停用信用卡

  • 设置信用卡的每日限额

  • 充值余额

  • 处理付款(考虑余额、信用卡到期时刻、活动/停用状况和每日限额)

创立/读取用户

创立/读取信用卡

激活/停用信用卡

设置信用卡的每日限额

充值余额

处理付款(考虑余额、信用卡到期时刻、活动/停用状况和每日限额)

为了简略起见,咱们假定每个账户仅有一张信用卡,咱们将疏忽授权功用。除了这一点之外,咱们将构建一个具有验证、过错处理、数据库和web api等强壮功用的运用程序。下面让咱们开端动手做第一个使命:规划信用卡。首要,让咱们看看C#的代码:

这些还不行,咱们还有必要增加验证,一般咱们会经过一些Validator完结,就像FluentValidation相同。规矩很简略:

  • 卡号是必输项,有必要是16位数字符串。

  • 用户名是必输项,只能包含字母,并且中心能够包含空格。

  • 月和年有必要满意鸿沟。

  • 当卡处于活动状况时,账号信息有必要存在;而当卡被停用时,账号信息不存在。原因很简略:当卡被停用时,余额或每日限额不该该答应更改。

卡号是必输项,有必要是16位数字符串。

用户名是必输项,只能包含字母,并且中心能够包含空格。

月和年有必要满意鸿沟。

当卡处于活动状况时,账号信息有必要存在;而当卡被停用时,账号信息不存在。原因很简略:当卡被停用时,余额或每日限额不该该答应更改。

这种办法存在一些问题:

  • 验证与类型声明别离,这意味着假如想查看信用卡完好的信息,咱们有必要查看全部代码,并在脑海中幻想完好的信用卡。假如这种作业只需求做一次的话就还好,可是在大项目中,咱们有必要为每个实体重复这种动作,所以十分耗时。

  • 上述验证不是强制性的,咱们有必要紧记在全部当地调用。咱们能够经过测验来保证不会有遗失,可是请不要忘掉编写测验的开支。

  • 当咱们想要在其他当地验证卡号时,咱们有必要从头编写相同的代码。当然,咱们能够将正则表达式保持在一个一同的当地,但咱们仍然需求在每个验证中调用。

验证与类型声明别离,这意味着假如想查看信用卡完好的信息,咱们有必要查看全部代码,并在脑海中幻想完好的信用卡。假如这种作业只需求做一次的话就还好,可是在大项目中,咱们有必要为每个实体重复这种动作,所以十分耗时。

上述验证不是强制性的,咱们有必要紧记在全部当地调用。咱们能够经过测验来保证不会有遗失,可是请不要忘掉编写测验的开支。

当咱们想要在其他当地验证卡号时,咱们有必要从头编写相同的代码。当然,咱们能够将正则表达式保持在一个一同的当地,但咱们仍然需求在每个验证中调用。

在F#中,咱们能够选用不同的办法:

// First we define a type for CardNumber with private constructor

// and public factory which receives string and returns `Result<CardNumber, string>`.

// Normally we would use `Validati` instead, but string is good enough for example

type CardNumber = private CardNumber ofstring

with

member this.Value = match thiswithCardNumber s -> s

staticmember create str =

match str with

| ( null| "") -> Error"card number can't be empty"

| str ->

ifcardNumberRegex.IsMatch(str) then CardNumber str |> Ok

elseError"Card number must be a 16 digits string"

// Then in here we express this logic "when card is deactivated, balance and daily limit manipulations aren't available`.

// Note that this is way easier to grasp that reading `RuleFor` in validators.

type CardAccountInfo =

| Active ofAccountInfo

| Deactivated

// And then that's it. The whole set of rules is here, and it's described in a static way.

// We don't need tests for that, the compiler is our test. And we can't accidentally miss this validation.

type Card =

{ CardNumber: CardNumber

Name: LetterString // LetterString is another type with built-in validation

HolderId: UserId

Expiration: (Month * Year)

AccountDetails: CardAccountInfo }

当然,在C#中咱们也能够做一些改善。咱们能够创立CardNumber类,它也会抛出ValidationException。可是咱们无法在C#中运用CardAccountInfo的技巧。别的,C#十分依靠反常。这会引发几个问题:

  • 反常有“go to”的语义。前一秒你还在这个办法中,下一秒就在大局反常处理程序中了。

  • 这些反常不会呈现在办法签名中。像ValidationException或InvalidUserOperationException这种反常都归于调用协议的一部分,但你只要阅览了完结之后才干得知这一点。这个问题很严重,由于你会常常运用其他人编写的代码,你不能仅仅阅览签名,所以不得不花费许多时刻,一向阅览到调用栈的最底部。

反常有“go to”的语义。超级赛亚人前一秒你还在这个办法中,下一秒就在大局反常处理程序中了。

这些反常不会呈现在办法签名中。像ValidationException或InvalidUserOperationException这种反常都归于调用协议的一部分,但你只要阅览了完结之后才干得知这一点。这个问题很严重,由于你会常常运用其他人编写的代码,你不能仅仅阅览签名,所以不得不花费许多时刻,一向阅览到调用栈的最底部。

这个问题让我很烦恼:每逢我完结一些新功用时,完结进程自身并不需求花费太多时刻,大部分时刻都花费在了两件事上:

  • 阅览其他人的代码,并找出事务逻辑规矩。

  • 保证没有损坏任何东情侣网名,怎样战胜软件开发的杂乱性?-安博电竞 官网_安博电竞进口_安博电竞网页版西。

阅览其他人的代码,并找出事务逻辑规矩。

保证没有损坏任何东西。

听起来如同是由于代码规划不合理,但即便是编写杰出的项目也会发作相同的作业。下面让咱们测验在C#中运用相同的Result。最简略的完结如下所示:

可是,这段代码纯属废物,它不会阻挠咱们一同设置Ok和Error,并且还答应咱们彻底疏忽过错。正确的编写办法如下:

这段代码很繁琐吧?并且我还没完结void版别的Map和MapError。调用办法大致如下:

voidTest(Result<int, string> result)

{

varsquareResult = result.Map(x => x * x);

}

还行吧?现在幻想你有三个result,假如它们都是ok,你期望做一些后续动作。头大了吧?所以这种完结底子上不用考虑。下面是用F#编写的版别:

// this type is in standard library, but declaration looks like this:

type Result< 'ok, 'error> =

| Ok of 'ok

| Error of 'error

// and usage:

let test res1 res2 res3 =

match res1, res2, res3 with

| Ok ok1, Ok ok2, Ok ok3 -> printfn "1: %A 2: %A 3: %A"ok1 ok2 ok3

| _ -> printfn "fail"

底子上,你要么挑选代码量合理,可是代码很含糊、依靠于反常、反射、表达式和其他“技巧”的编程言语,要么挑选需求编写许多代码,很难阅览,可是代码自身开门见山的言语。一旦遇到大型项目,你就无法运用相似C#类型体系的言语。让咱们考虑一个简略的状况:你的代码库中有一些实体现已存在一段时刻了。现在,你想增加一个新的必填字段。当然,你需求在创立此实体的当地初始化该字段,但编译器底子帮不上忙,由于类是可变的且null是有用值。遇上AutoMapper这有氧运动和无氧运动的区异样的库状况只会更糟。这种可变性答应咱们在某个当地部分初始化方针,然后将其推送到其他当地并持续初始化。这是bug的多发地段。

当然,咱们能够比较言语的功用,但这超出了本文的评论规模。可是,言语功用自身不该成为替换技能的理由。

所以,咱们需求面临以下两个问题:

为什么咱们的确需求放弃现代OOP?

为什么咱们需求切换到函数式编程?

第一个问题的答案是,在现代运用程序中运用通用的OOP言语会给你带来许多费事,由于这些言语的规划意图不同。挑选这些言语,你不只需求花费时刻和金钱应对言语自身,还需求处理运用程序的杂乱性。

第二个问题的答案是,函数式编程言语供给了一种简略的办法来规划功用,它们就像时钟相同,假如新功用损坏现有逻辑,代码遭到损坏,你会马上知晓。

可是,这些答案还不行充沛。正如我的朋友在一次评论中指出的那样,假如你没有把握最佳实践,那么切换到函数式编程也毫无用处。咱们身处的大职业出产了许多关于规划OOP运用程序的文章、书本和教程,并且咱们具有丰厚OOP的出产经历,因而咱们知道不同办法的预期成果。不幸的是,函数式编程并非如此,所以即便你切换到函数式编程,在第一次测验中也或许遭受为难,当然也不或许快速而轻松地开发杂乱的体系。

本文便是想评论这一点。咱们能够经过构建相似出产的运用程序看看其间的差异。

怎样规划运用程序?

在规划进程中,我学习了许多来自《Domain Modeling Made Functional》一书的主意,所以我强烈建议你阅览这本书。

点击这儿获取完好的源代码(https://github.com/atsapura/CardManagement)。在本文中,我只会介绍一些要害点。

咱们有4个首要项目:事务层,数据拜访层,根底设施,当然还有(每个处理方案都有的)通用处理。咱们从范畴中的建模开端。此刻咱们不了解也不用理睬数据库。由于假如我脑海中想着某个特广东信华电器有限公司定的数据库,我就会倾向于依据这个数据库做范畴规划,然后导致将实体-表的联系带入事务层,到后边就会出问题。咱们需求做的仅仅完结范畴->DAL的映射,而过错的规划会不断给咱们找费事,直到咱们改掉唐辛肖这个过错。接下来咱们的作业是:创立一个名为CardManagement的项目,然后当即翻开项目文件中的<TreatWarning泱泱sAsErrors>true</TreatWarningsAsErrors>设置。为什么咱们需求这个?由于咱们将许多运用可辨认联合,而在匹配办法时,假如咱们没有包含全部或许的状况,编译器就会给咱们一个正告:

letfail result =

match result with

| Ok v -> printfn "%A"v

// warning: Incomplete pattern matches on this expression. For example, the value 'Error' may indicate a case not covered by the pattern(s).

启用这个设置后,当咱们扩展现有功用并期望其他全部当地也跟着调整,那么代码就无法经过编译,这正是咱们所需求的。接下来,咱们需求创立模块(它在静态类中编译)CardDomain。咱们在这个文件中只描绘了域类型。请记住,在F#中代码和文件的次序很重要:默许状况下,你只能运用之前声明过的内容。

范畴的类型

首要,咱们运用之前显现的CardNumber界说咱们的类型,但咱们需求更实践的Error,而不只仅是一个字符串,所以咱们将运用Validati。

type Validati =

{ FieldPath: string

Message: string}

letvalidati field message = { FieldPath = field; Message = message }

// Actually we should use here Luhn's algorithm, but I leave it to you as an exercise,

// so you can see for yourself how easy is updating code to new requirements.

letprivatecardNumberRegex = newRegex( "^[0-9]{16}$", RegexOptions.Compiled)

type CardNumber = privateCardNumber of string

with

member this.Value = match thiswith CardNumber s -> s

staticmember create fieldName str =

match str with

| ( null| "") -> validati fieldName "card number can't be empty"

| str ->

ifcardNumberRegex.IsMatch(str) then CardNumber str |> Ok

elsevalidati fieldName "Card number must be a 16 digits string"

接下来,咱们来界说范畴的中心:Card。咱们知道信用卡有一些永久的特点,比方卡号、到期时刻和信用卡上的名字,还有一些可变的信息,例如余额和每日限额,所以咱们将这些可变信息封装到其他类型中:

typeAccountInfo =

{ HolderId: UserId

Balance: Money

DailyLimit: DailyLimit }

typeCard =

{ CardNumber: CardNumber

Name: LetterString

HolderId: UserId

Expiration: (Month * Year)

AccountDetails: CardAccountInfo }

现在,还有几种类型咱们还没有声明:

1. Money

咱们能够运用decimal ,但decimal的描绘性不太好。此外,decimal 可用于表明金钱之外的其他东西,咱们不期望形成混杂。所以咱们运用自界说类型type [<Struct>] Money = Money of decimal。

2. DailyLimit

每日限额能够设置为特定数量,也或许底子不存在。假如存在,那有必要是正数。咱们没有运用decimal或Money,而是界说了这种类型:

[ <Struct>]

type DailyLimit =

private// private constructor so it can't be created directly outside of module

| Limit of Money

| Unlimited

with

staticmember ofDecimal dec =

ifdec > 0m then Money dec |> Limit

elseUnlimited

member this.ToDecimalOption =

match thiswith

| Unlimited -> None

| Limit limit -> Some limit.Value

这样做比仅仅用0M默许代表没有约束更具描绘性,由于0M也或许意味着你不能动这张卡上的钱。由于咱们躲藏了结构函数,因而 仅有的问题是咱们无法进行办法匹配。但不用忧虑,咱们能够运用Active Patterns:

let(|Limit|Unlimited|) limit=

match limitwith

| Limit dec -> Limit dec

| Unlimited -> Unlimited

现在咱们能够将DailyLimit作为惯例的DU在任何当地做匹配。

3. LetterString

这个很简略。咱们运用与韦德之道CardNumber相同的处理。但留意,LetterString不是信用卡固有的,它是另一个东西,咱们应该把它放入CommonTypes模块的Common项目中。因而咱们还需求将Validati移动到不同的当地。

4. UserId

咱们能够界说成:type UserId = System.Guid。这个类型仅仅为了供给描绘性。

5. Month和Year

这两个字段也需求放在Common中。Month是一个可辨认联合,还需求一个办法将它转化为unsigned int16,而Year与CardNumber相同,只不过需求把string换成uint16。

到这儿中止,范畴类型声明就完结了。下面,咱们需求为User供给一些用户和卡片信息,咱们需求在充值和付款的时分核算余额。

typeUserInfo =

{ Name: LetterString

Id: UserId

Address: Address }

typeUser =

{ UserInfo : UserInfo

Cards: Card list }

[<Struct>]

typeBalanceChange =

| Increase of increase: MoneyTransaction // another common typewith validation fo情侣网名,怎样战胜软件开发的杂乱性?-安博电竞 官网_安博电竞进口_安博电竞网页版rpositive amount

| Decrease of decrease: MoneyTransaction

with

member this.ToDecimal =

matchthis with

| Increase i -> i.Value

| Decrease d -> -d.Value

[<Struct>]

typeBalanceOperation =

{ CardNumber: CardNumber

Timestamp: DateTimeOffset

BalanceChange: BalanceChange

NewBalance: Money }

咱们规划类型的办法无法表明无效状况。现在每逢处理这些类型的实例时,咱们都能够确认其间的数据是有用的,咱们不用再次验证。下面咱们来看看事务逻辑。

事务逻辑

咱们有一个坚决不行违反的规矩:全部事务逻辑用纯函数编写。纯函数是满意以下规范的函数:

  • 函数仅有的功用便是核算输出值。底子没有副作用。

  • 针对同一输入,函数一向会发作相同的输出。

函数仅有的功用便是核算输出值。底子没有副作用。

针对同一输入,函数一向会发作相同的输出。

因而,纯函数不会抛出反常,不会发作随机值,也不会以任何办法与外部国际交互,不管是数据库仍是简略的DateTime.Now。当然,与不纯函数交互会主动导致调用函数不纯。那么咱们应该怎样完结呢?

咱们的需求如下:

  • 激活/停用卡

  • 处理付款

    付款条件:

    卡未过期

    卡处于激活状况

    有满意的钱付出

    今日的开销未超越每日限额

  • 充值余额

    咱们能够核算处于激活状况且未过期的信用卡余额。

  • 设定每日限额

    假如卡未过期且处于激活状况,则用户能够设置每日限额。

激活/停用卡

处理付款

付款条件:

卡未过期

卡处于激活状况

有满意的钱付出

今日的开销未超越每日限额

充值余额

咱们能够核算处于激活状况且未过期的信用卡余额。

设定每日限额

假如卡未过期且处于激活状况,则用户能够设置每日限额。

假如无法完结操作,则有必要回来过错,因而咱们需求界说OperationNotAllowedError:

typeOperationNotAllowedError =

{ Operation: string

Reason: string }

// and a helper functionto wrap it in`Error` whichis a casefor`Result< 'ok,'error> type

letoperationNotAllowed operation reason = { Operation = operation; Reason = reason } |> Error

在这个模块的事务逻辑中,上述是仅有需求回来的过错类型。在这儿我不做验证,不与数据库交互,仅仅履行操作,或回来OperationNotAllowedError。

点击这儿获取该模块完好的代码(https://github.com/atsapura/CardManagement/blob/master/CardManagement/CardActions.fs)。在这儿,咱们只评论最扎手的一段处理:processPayment。咱们有必要查看到期、活动/停用状况,今日花费的金额和当时余额。由于无法与外部国际互动,因而咱们有必要将全部必要的信息作为参数传递进去。在这种办法下,这个逻辑很简略测验,并且你也能够进行依据特点的测验。

let processPayment (currentDate: DateTimeOffset) (spentToday: Money) card (paymentAmount: Mo情侣网名,怎样战胜软件开发的杂乱性?-安博电竞 官网_安博电竞进口_安博电竞网页版neyTransaction) =

// first checkforexpiration

ifisCardExpired currentDate card then

cardExpiredMessage card.CardNumber |> processPaymentNotAllowed

else

// thenactive/deactivated

match柏林之声card.AccountDetails with

| Deactivated -> cardDeactivatedMessage card.CardNumber |> processPaymentNotAllowed

| Active accInfo ->

// ifactive thencheckbalance

ifpaymentAmount.Value > accInfo.Balance.Value then

sprintf "Insufficent funds on card %s"card.CardNumber.Value

|> processPaymentNotAllowed

else

// ifbalance isok checklimitandmoney spent today

matchaccInfo.DailyLimit with

| Limitlimitwhenlimit< spentToday + paymentAmount ->

sprintf "Daily limit is exceeded for card %s with daily limit %M. Today was spent %M"

card.CardNumber.Value limit.Value spentToday.Value

|> processPaymentNotAllowed

(*

We could usehere the ultimate wild card caselikethis:

| _ ->

but it 's dangerous because if a new case appears in `DailyLimit` type,

we won't geta compile errorhere, which would remind us toprocess this

newcaseinhere. So this isa safeway todothe same thing.

*)

| Limit_ | Unlimited->

let newBalance = accInfo.Balance - paymentAmount

let updatedCard = { card withAccountDetails = Active { accInfo withBalance = newBalance } }

// note that we have toreturnbalance operation, so it can be storedtoDB later.

let balanceOperation =

{ Timestamp= currentDate

CardNumber = card.CardNumber

NewBalance = newBalance

BalanceChange = Decrease paymentAmount }

Ok (updatedCard, balanceOperation)

关于这个spentToday,咱们有必要从数据库中保存的BalanceOperation调集进行核算。所以咱们为此树立一个模块,底子上只要1个公共函数:

letprivateisDecrease change =

match change with

| Increase _ -> false

| Decrease _ -> true

letspentAtDate ( date: DateTimeOffset) cardNumber operations =

letdate= date. Date

letoperationFilter { CardNumber = number; BalanceChange = change; Timestamp = timestamp } =

isDecrease change && number = cardNumber && timestamp. Date= date

letspendings = List. filteroperationFilter o我的美人大小姐perations

List.sumBy (fun s -> -s.BalanceChange.ToDecimal) spendings |> Money

好了,全部的事务逻辑完结都完结了。现在咱们来考虑映射。咱们的许多类型都运用了可辨认联合,咱们的某些类型没有公共结构函数,所以咱们不能将它们露出给外部国际。咱们需求处理(反)序列化。除此之外,现在咱们的运用程序中只要一个有界的上下文,可是在实践生活中你需求构建具有多个有界上下文的强壮体系,并且它们有必要经过公共契约彼此交互,这应该不难了解,包含其他编程言语。

咱们有必要做双向的映射:从公共模型到范畴,从范畴到公共模型。尽管从范畴到模型的映射很简略,但反向有点费事:模型或许包含无效数据,终究咱们运用的是能够序列化为json的一般类型。别忧虑,咱们有必要在这个映射中构建验证。事实上,咱们对或许无效的数据和数据运用不同的类型,这一向有用意味着编译器会提示咱们不要忘掉履行验证。

代码如下:

// You can usetypealiases toannotate your functions. This isjust an example, but sometimes it makes code more readable

typeValidateCreateCardCommand = CreateCardCommandModel -> ValidationResult<Card>

let validateCr武义天气预报eateCardCommand : ValidateCreateCardCommand =

fun cmd ->

// that 's a computation ex抖阴tvpression for `Result<>` type.

// Thanks to this we don't have tochose betweenshortcode andstrait forward one,

// likewe have todoinC #

result{

let! name= LetterString.create "name"cmd.Name

let! number= CardNumber.create "cardNumber"cmd.CardNumber

let! month= Month.create "expirationMonth"cmd.ExpirationMonth

let! year= Year.create "expirationYea易拉宝r"cmd.ExpirationYear

return

{ Card.CardNumber = number

Name= name

HolderId = cmd.UserId

Expiration = month, year

AccountDetails =

AccountInfo.Default cmd.UserId

|> Active }

}

到此中止,咱们现已完结了全部事务逻辑、映射、验证等等,而这些都脱离了实践国际:这些代码都是用纯函数编写的。现在你或许想知道,咱们终究应该怎样运用这些代码?由于咱们需求与外界互动。更重要的是,在履行作业流程期间,咱们有必要依据实在国际的交互成果做决议。所以现在的问题是咱们怎样拼装全部这些代码?在OOP中,咱们能够运用IoC容器来处理,但在这儿咱们不能这样做,由于咱们没有方针,咱们只要静态函数。

咱们将运用解说器办法(Interpreter pattern)!这有点扎手,首要是由于我不熟悉,但我会极力解说这种办法。首要,咱们来谈谈功用构成。例如,咱们有一个函数int -> string,这个函数需求int作为参数并回来字符串。现在假定咱们有另一个函数string->char。这时,咱们能够将这两个函数串到一同,也便是说先履行第一个函数,然后将输出传递给第二个函数,咱们能够运用运算符>>。实践的代码如下:

letintToString(i: int) = i.ToString

letfirstCharOrSpace(s: string) =

match s with

| ( null| "") -> ' '

| s -> s.[ 0]

letfirstDigitAsChar = intToString >> firstCharOrSpace

// A椰子鸡nd you can chain as many functions as you like

letalwaysTrue = intToString >> firstCharOrSpace >> Char.IsDigit

可是,在有些状况下咱们不能简略的将函数串到一同,例如激活卡,这触及一系列的动作:

  • 验证输入卡号。假如有用,则持续

  • 经过卡号获取卡。假如卡存在,则持续

  • 激活卡

  • 保存成果。假如保存成功,则持续

  • 映射到模型并回来。

验证输入卡号。假如有用,则持续

经过卡号获取卡。假如卡存在,则持续

激活卡

保存蔓蔓青萝成果。假如保存成功,则持续

映射到模型并回来。

上述前两个进程都有if句子,这便是为什么咱们无法直接串到一同的原因。

咱们能够简略地将这些函数作为参数注入,如下所示:

letactivateCard getCardAsync saveCardAsync cardNumber = ...

可是,这种办法存在一些问题。首要,依靠项会越来越多,函数签名会很丑陋;其次,这儿咱们需求完结特定的作用:咱们有必要挑选经过Task、Async或简略同步调用来完结;第三,假如你传递太多函数,就会引起紊乱,例如createUserAsync和replaceUserAsync具有相同的签名但作用不同,因而当你有必要传递数百次时,或许会由于十分古怪的现象而犯过错。由于这些原因,咱们挑选了解说器。

开端的主意是将组合代码分为两部分:履行树和该树的解说器。该树的每个节点都是咱们想要注入的函数的方位,例如getUserFromDatabase。这些节点的界说包含称号,例如getCard;输入参数类型,例如CardNumber;回来类型,例如Card option。这儿咱们指定Task或Async,由于它不是树的一部分,它是解说器的一部分。该树的每一条边都是一系列纯转化,比方验证或事务逻辑函数履行。边也有一些输入,例如卡号的原始字符串,然后还有验证,能够供给给咱们一个过错或有用的卡号。假如呈现过错,咱们将中止这个边;假如没有过错,咱们就会进入下一个节点:getCard。假如此节点回来Some card,那咱们就持续下一个边——即激活,依此类推。

关于activateCard、processPayment或topUp,咱们都要构建一个独自的树。在构建这些树时,有些节点是空白,它们没有真实的函数,它们仅仅为这些函数预留了方位。解说器的方针是填充这些节点,仅此而已。解说器知道咱们运用的作用,例如Task,它知道在给定节点中实践放入哪个函数。在拜访节点时,它会履行相应的实践功用,假如是Task或Async,情侣网名,怎样战胜软件开发的杂乱性?-安博电竞 官网_安博电竞进口_安博电竞网页版它就会等候,并将成果传递给下一个边。这个边或许会走向另一个节点,然后再次回到解说器,直到这个解说器抵达中止节点,递归的底部,然后咱们只需回来树的整个履行成果。

整个树用有差异的联合表明,某个节点的代码如下:

节点一向是一个元组,其间第一个元素是依靠项的输入,最终一个元素是一个函数,它接纳该依靠项的成果。你能够在元组元素之间的“空白”中放入依靠项,就像那些组合比方中你有函数'a -> 'b,'c -> 'd,而你需求在二者之间放入'b ->'c。

由于咱们处于有界的上下文中,因而咱们不该该有太多的依靠联系,这时咱们应该将上下文拆分为较小的上下文。

代码如下:

type Program< 'a> =

| GetCard of CardNumber * (Card option -> Program<'a>)

| GetCardWithAccountInfo of CardNumber * ((Card*AccountInfo) option -> Program< 'a>)

| CreateCard of (Card*AccountInfo) * (Result<unit, DataRelatedError> -> Program<'a>)

| ReplaceCard of Card * (Result<unit, DataRelatedError> -> Program< 'a>)

| GetUser of UserId * (User option -> Program<'a>)

| CreateUser of UserInfo * (Result<unit, DataRelatedError> -> Program< 'a>)

| GetBalanceOperations of (CardNumber * DateTimeOffset * DateTimeOffset) * (BalanceOperation list -> Program<'a>)

| SaveBalanceOperation of BalanceOperation * (Result<unit, DataRelatedError> -> Program< 'a>)

| Stop of 'a

// This bind functionallows you to pass a continuation forcurrent node of your expression tree

// the code is basically a boiler plate, as you can see.

let rec bind f instruction =

match instruction with

| GetCard (x, next) -> GetCard (x, ( next>> bind f))

| GetCardWithAccountInfo (x, next) -> GetCardWithAccountInfo (x, ( next>> bind f))

| CreateCard (x, next) -> CreateCard (x, ( next>> bind f))

| ReplaceCard (x, next) -> ReplaceCard (x, ( next>> bind f))

| GetUser (x, next) -> GetUser (x,( next>> bind f))

| CreateUser (x, next) -> CreateUser (x,( next>> bind f))

| GetBalanceOperations (x, next) -> GetBalanceOperations (x,( next>> bind f))

| SaveBalanceOperation (x, next) -> SaveBalanceOperation (x,( next>> bind f))

| Stop x -> f x

// this is a set of basic functions. Use them inyour expression tree builder to represent dependency call

let stopx = Stop x

let getCardByNumber number = GetCard (number, stop)

let getCardWithAccountInfo number = GetCardWithAccountInfo (number, stop)

let createNewCard (card, acc) = CreateCard ((card, acc), stop)

let replaceCard card = ReplaceCard (card, stop)

let getUserById id = GetUser (id, stop)

let createNewUser user = CreateUser (user, stop)

let getBalanceOperations (number, fromDate, toDate) = GetBalanceOperations ((number, fromDate, toDate), stop)

let saveBalanceOperation op = SaveBalanceOperation (op, stop)

咱们能够凭借核算表达式,十分轻松地构建作业流程,而无需关怀实践交互的完结。如下是CardWorkflow模块:

// `program` is the name of our computation expression.

// In every `let!` binding we unwrap the result of operation, which can be

// either `Program<'a>` or `Program<Result<'a, Error>>`. What we unwrap would be of type 'a.

// If, however, an operation returns `Error`, we stop the execution at this very step and return it.

// The only thing we have to take care of is making sure that type of error is the same in every operation we call

let processPayment (currentDate: DateTimeOffset, payment) =

program {

(* You can see these `expectValidati` and`expectDataRelatedErrors` functions here.

What they dois map different errors into `Error` type, since every execution branch

must returnthe same type, in this case`Result< 'a, Error>`.

They also help you quickly understand what's going on in every line of code:

validation, logic orcalling external storage. *)

let! cmd = validateProcessPaymentCommand payment |> expectValidati

let! card = tryGetCard cmd.CardNumber

let today = currentDate.Date |> DateTimeOffset

let tom祉痕orrow = currentDate.Date.AddDays 1.|> DateTimeOffset

let! operations = getBalanceOperations (cmd.CardNumber, today, tomorrow)

let spentToday = BalanceOperation.spentAtDate currentDate cmd.CardNumber operations

let! (card, op) =

CardActions.processPayment currentDate spentToday card cmd.情侣网名,怎样战胜软件开发的杂乱性?-安博电竞 官网_安博电竞进口_安博电竞网页版PaymentAmount

|> expectOperationNotAllowedError

do! saveBalanceOperation op |> expectDataRelatedErrorProgram

do! replaceCard card |> expectDataRelatedErrorProgram

returncard |> toCardInfoModel |> Ok

}

这个模块是咱们在事务层中的最终一个完结。此外,我还做了一些重构:我将过错和常见类型移动到Common项目。下面咱们来看看数据拜访层的完结。

数据拜访层

这一层实体的规划取决于与之交互的数据库或结构。因而,范畴层对这些实体一窍不通,这意味着咱们有必要在这儿处理与范畴模型之间的映射。这对咱们DAL API的顾客来说十分便利。在这个这个运用程序中,我挑选了MongoDB,不是由于MongoDB是这类使命的最佳挑选,而是由于现已有了许多运用SQL DB的比方,我期望写一些不相同的东西。我计划运用C#驱动程序。在大多数状况下,这些完结很简略,仅有扎手的是Card。当信用卡处于激活状况时,它内部有一个AccountInfo,而当非激活状况时则没有。因而,咱们有必要将其拆分为两个文档:CardEntity和CardAccountInfoEntity,意图是为了在停用卡时,不会删去有关余额和每日限额的信息。除此之外,咱们将运用原始类型以及类型自带的验证。

在运用C#库时,咱们需求留意几个问题:

  • 将null转化为Option<'a>

  • 捕获预期的反常,将它们转化为咱们的过错,并包装在Result<_,_>中

将null转化为Option<'a>

捕获预期的反常,将它们转化为咱们的过错,并包装在Result<_,_>中

咱们从界说了实体的CardDomainEntities模块开端:

咱们将在SRTP的协助下运用字段EntityId和IdComparer。咱们将界说一些函数,从恣意类型中获取这些字段,而不是强制每个实体完结接口:

关于null和Option,由于咱们运用了新建文件夹记载类型,因而F#编译器不答应运用null,既不能用于赋值也不能用于比较。可是,记载类型仅仅另一种CLR类型,所以严厉来讲,咱们能够并且也必定会取得一个null值,这要归功于C#和这个库的规划。处理这个问题的办法有两种:运用AllowNullLiteral特点,或运用Unchecked.defaultof<'a>。我挑选了第二种办法,由于这种null状况应该尽或许地本地化:

为了处理重复键的反常,咱们再次运用Active Patterns:

// First we define a function which checks, whether exception is about duplicate key

let privateisDuplicateKeyException (ex: Exception) =

ex :? MongoWriteException && (ex : ?>MongoWriteException).WriteError.Category = ServerErrorCategory.DuplicateKey

// Then we have to check wrapping exceptions for this

let rec private(|DuplicateKey|_|) (ex: Exception) =

match ex with

| :? MongoWriteException asex when isDuplicateKeyException ex ->

Some ex

| :? MongoBulkWriteException asbex when bex.InnerException |> isDuplicateKeyException ->

Some (bex.InnerException : ?>MongoWriteException)

| :? AggregateException asaex when aex.InnerException |> isDuplicateKeyException ->

Some (aex.InnerException : ?>MongoWriteException)

| _ -> None

// And here's the usage:

let inline privateexecuteInsertAsync (func: 'a -> Async<unit>) arg =

async {

try

do! func(arg)

return Ok

with

| DuplicateKey ex ->

return EntityAlreadyExists (arg.GetType.Name, (entityId arg)) |> Error

}

完结映射后,咱们就具有了为数据拜访层组成API所需的全部,如下所示:

最终,我想提一提在映射Entity -> Domain的时分,咱们有必要运用内置验证来实例化类型,因而或许存在验证过错。在这种状况下,咱们不会运用Result<_,_>,由于假如咱们的DB中存在无效数据,那么这便是一个bug,咱们可不想写bug。所以,咱们只抛反常。

组成、日志记载和其他功用

别忘了,咱们不会运用DI结构,咱们挑选了解说器办法。原因在于:

  • IoC容器是在运转时操作的,所以在运转程序之前无法知道是否全部依靠项都已满意。

  • DI很强壮,因而很简略被乱用:你能够经过它完结特点注入,完结懒散依靠,有时乃至一些事务逻辑也能够导致注册或处理依靠(我亲眼见过)。全部这些都会加大保护代码的难度。

IoC容器是在运转时操作的,所以在运转程序之前无法知道是否全部依靠项都已满意。

DI很强壮,因而很简略被乱用:你能够经过它完结特点注入,完结懒散依靠,有时乃至一些事务逻辑也能够导致注册或处理依靠(我亲眼见过)。全部这些都会加大保护代码的难度。

这意味着咱们需求一个当地来放置该功用。咱们能够将它放在Web API的最上层,但我以为这并不是最好的挑选:咱们现在只需处理一个有界上下文,但假如有许多有界上下文,那么将每个上下文的解说器都放在大局的方位上的做法会十分蠢笨。此外还有个单一呼应准则,Web API项目应该对Web做出呼应,对吧?所以咱们创立了CardManagement.Infrastructure项目。

在这个项目中,咱们需求处理:

  • 编撰咱们的功用

  • 运用装备

  • 日志

编撰咱们的功用

运用装备

日志

假如咱们有多于1个上下文,那么就应该将运用程序装备和日志装备移动到大局根底架构项目,并且此项意图仅有功用便是为咱们的有界上下文拼装API,但在咱们的这个比方中,这种别离尚不用要。

咱们来看看组合。咱们在范畴层中构建了履行树,现在咱们需求解说履行树。树中的每个节点表明某种依靠项调用,咱们的比便利是对数据库的调用。假如咱们需求与第三方API进行交互,那么这些交互也会呈现在这儿。因而,咱们的解说器有必要知道怎样处理该树中的每个节点,而这一步是在编译时进行验证的,这要归功于<TreatWarningsAsErrors>设置。代码如下:

请留意,这个解说器中运用了async。咱们能够运用Task编写解说器,也能够简略地编写同步的版别。现在你或许想知道怎样做单元测验,由于众所周知的mock库在这儿没有用武之地。其实也很简略:只需求再做一个解说器即可。代码如下:

咱们创立了TestInterpreterConfig,它保存了咱们想要注入的每个操作的所需成果。你能够很轻松地更改每个测验的装备,然后运转解说器即可。这个解说器是同步的,因而没必要牵扯Task或Async。

日志记载没有难度,运用这个模块(https://github.com/atsapura/CardManagement/blob/master/CardManagement.Infrastructure/Logging.fs)即可。办法是咱们将函数包装在日志记载中:咱们记载函数称号,参数和日志成果。假如成果没问题,那么输出info等级;假如犯错,就输出warning;假如是一个Bug,就输出error。

最终,咱们还需求树立一个外观,由于咱们不想露出原始的解说器调用。全体代码如下:

let createUser arg=

arg|> (CardWorkflow.createUser >> CardProgramInterpreter.interpret |> logifyResultAsync "CardApi.createUser")

let createCard arg=

arg|> (CardWorkflow.createCard >> CardProgramInterpreter.interpret |> logifyResultAsync "CardApi.createCard")

let activateCard arg=

arg|> (CardWorkflow.activateCard >> CardProgramInterpreter.interpret |> logifyResultAsync "CardApi.activateCard")

let deact怎样啪啪ivateCard arg=

arg|> (CardWorkflow.deactivateCard >> CardProgramInterpreter.interpret |> logifyResultAsync "CardApi.deactivateCard")

let processPayment arg=

arg|> (CardWorkflow.processPayment >> CardProgramInterpreter.interpret |> logifyResultAsync "CardApi.processPayment")

let topUp arg=

arg|> (CardWorkflow.topUp >> CardProgramInterpreter.interpret |> logifyResultAsync "CardApi.topUp")

let setDailyLimit arg=

arg情侣网名,怎样战胜软件开发的杂乱性?-安博电竞 官网_安博电竞进口_安博电竞网页版|> (CardWorkflow.setDailyLimit >> CardProgramInterpreter.interpret |> logifyResultAsync "CardApi.setDailyLimit")

let getCard arg=

arg|> (CardWorkflow.getCard >> CardProgramInterpreter.interpret |> logifyResultAsync "CardApi.getCard")

let g金丝熊etUser arg=

arg|> (CardWorkflow.getUser >> CardProgramInterpreter.interpretSimple |> logifyResultAsync "CardApi.getUser")

这儿注入了全部依靠项,还处理了日志记载,也不抛出任何反常,十分简略。关于web api,我运用了Giraffe结构。Web项意图代码在这儿(https://github.com/atsapura/CardManagement/tree/master/CardManagement.Api/CardManagement.Api)。

定论

咱们现已构建了一个带有验证、过错处理、日志记载和事务逻辑的运用程序,这些一般都是运用程序必不行少的功用。不同之处在于,本文中的代码更经用且易于重构。请留意,咱们没有运用反射或代码生成,没有反常,但咱们的代码仍然很简略,易于阅览,易于了解,且十分安稳。假如在模型中增加另一个字段,或许在咱们的某个联合类型中增加另一种状况,那么代码只要在更新全部调用之后才会经过编译。当然,这并不意味着彻底安全,或许底子不需求任何类型的测验,这只意味着你在开发新功用或进行重构时遇到的问题会更少。开发本钱能够下降,开发进程也很风趣,由于这个东西能够让你专心于范畴和事务使命,而不需求小心谨慎不要损坏其他功用。

别的,我并没有说OOP彻底没用,也没有说咱们不需求它。我说的是并非每一项使命都需求OOP来处理,并且咱们的很大一部分使命能够经过函数式编程更好地处理。事实上,咱们总是需求寻求平衡:咱们无法仅运用一种东西有用地处理全部问题,因而杰出的编程言语应该对函数式编程和OOP供给杰出的支撑。不幸的是,现在许多最盛行的言语仅支撑lambdas和函数式编程中的async。

原文:https://github.com/atsapura/CardManagement/blob/master/article/Fighting%20complexity%20in%20software%20development.md

作者:Anish Karandikar和Roman,Anish Karandikar是高档后端开发@KaterTech。

声明:本文为CSDN翻译,转载请注明来历出处。