战略SCALA风格:最小电力原则(2014)

2021-06-15 04:37:27

Scala语言很大,很复杂,它提供了各种工具,开发人员可以使用各种方式来做同样的事情。鉴于每个问题的可能解决方案的范围,开发人员如何选择应该使用哪一个?这是一系列博客文章中的第一个旨在提供A&#34的风格指南;战略"等级。高于&#34的水平;我应该使用多少空格"或Camelcase VS PascalCase,它应该帮助开发人员与Scala语言一起使用可能的解决方案的自助式。

关于作者:Haoyi是一名软件工程师,以及许多开源Scala工具的作者,如氨铁型Repl和Mill构建工具。如果您在此博客上享受内容,您也可以享受豪尼'书实践Scala编程

这些指南基于我自己在Scala的开放和封闭源项目工作的经验。尽管如此,他们都遵循一套连贯的基本原则,希望能够提供正常的理由和普通的"我更喜欢你更喜欢"这些讨论的性质。

这些指南都认为您已经了解Scala的大部分'语言功能以及可以与它们进行的内容,并纯粹关注如何在选择解决方案时选择它们。它纯粹粘在"香草scala"及其标准图书馆功能/ API:您无法找到任何关于例如的内容。 Akka或Scalaz在这里。

毫无疑问,来自" Monadic"或"反应性"或"类型级"或" scala.js"营地(即基本上每个人)会不同意一些指导方针。尽管如此,希望过度所有的文件仍然广泛适用于分歧

您可以同意或不同意任何内容;让我知道在下面的评论中!

考虑到选择的解决方案,挑选最不能解决问题的强大解决方案

这并不明显。开发人员努力尝试创建强大,灵活的解决方案。然而,一个能够做任何事情的强大,灵活的解决方案是最难以分析的,而一个限制的解决方案,这是一些事情,实际上只能做几件事,对于某人来说是直接的,以便在以后检查,分析和操纵。

鉴于一种语言,选择能够解决问题的最少强大的语言

为什么这一原则适用于Scala语言的编程?你很容易想象应用的相反原理:

鉴于解决方案的选择,挑选最强大的解决方案,能够解决您的问题

例如,这将是例如元编程等解决方案将优先于其他更多"基本"解决方案。不是因为元编程是先进的,而是因为它通常可以在非常少量的代码中实现绝对任何东西的巨大灵活性。为什么这是一件坏事?

因此,您不需要在预期未来的工作中过度工程师;必要时只需重构

开发人员使用scala最常见的投诉是代码令人困惑和#34;难以阅读"和#34;复杂",编译器很慢。我不会谈论编译速度,因为它往往在你的控制之外,但赋予另一个投诉,制作代码"更容易阅读"和#34;较少复杂的"应该是一个以语言工作的开发人员的优先事项。

每种语言都不是这种情况!例如,在Python或Ruby中,人们经常叫它"易于阅读"或#34;可执行伪码"指代码如何看起来就像你会想象从白板上的粗略。首席投诉中心周围和重新吸引/可维护性和运行时间性能。在Python或Ruby中编程经验丰富的程序员,可以提高代码的重构,例如,写入单位测试的负载以捕获类型错误,远远超过您以静态类型的语言,如Java或Scala。那个'没有"更好"或"更糟糕的"而不是在Java或Scala中写下更少的单位测试,"不同"为了适应不同的约束和问题,语言向您提供。

回到Scala,开发人员应该额外付出代码和#34;更容易阅读"和"更复杂"这是在每种语言中的情况,但这是你应该做的事情,以便减轻Scala编程语言的这种弱点。幸运的是,Scala提供了有关其他帮助的其他工具。

在像Python和Ruby这样的动态类型语言中,甚至是Java或C等其他略微较弱的静态语言,琐碎的重构通常非常困难或可怕。例如,在大的Python CodeBase中重命名字段或方法很难,因为您无法向自己保证您正确更新所有手叫。即使在Java中,广泛使用铸造和反射意味着您可以轻松地添加/删除/修改类,成功编译一切,并在运行时弹出ClassCastException或InvocationTargetException。要打击这一点,您通常会略微先发制地编程:即使在一个小的codebase中,您常常将参数传递给您'尚未使用的函数,或通过"超过您的需求"例如通过整个对象而不是必需的单个方法/回调。这通常是微妙和潜意识的,但目标通常是为了稍后尝试避免重构的需要:如果您需要做更多的事情,可以为现有代码进行最小的更改。

在Scala编程语言中,您不应该担心重构。您有编译器来指导您,从普通的更改,如将管道额外的参数等额外的参数进入函数,更涉及的代码库的重组。仍然存在危险的危险.ToString或==,但它们'重新收到他们成为A"有点恼火滋扰"而不是"展示停止障碍"它们以动态语言呈现给许多重构。

如果您的函数只需要来自对象的单个方法,并且又在' t确定它是否需要稍后需要其他件事,以该方法而不是整个对象,所以您可以' t使用其他东西。

如果您'重新确定是否需要在将来重新使用一次使用辅助方法,将其嵌套在使用它的方法内,所以它可以立即重新使用它。

这似乎是反直观的,但它为目的是:通过执行所有这些目前的无需事物可以' t发生,你限制了可以使用代码完成的东西。通过这样做,我们可以尽可能强大地缩小代码库的不同部分之间的界面。这提供了好处:

您在界面的两侧都能自由地独立发展!如果将单个方法传递给函数,而不是整个复杂对象,则会稍后交换函数变得微不足道。

您可以更好地理由对您的代码的位与其他位交互:如果将单个方法传递成函数而不是整个对象,您现在可以立即看到该功能仅使用该呼叫,而以前则可以使用该功能需要挖掘来源来查看它的使用位置。

实际上,由于不是先发制人的过度开发,我们正在易于制作易于阅读和理解。但是,如果我们相信较早的点,即Scala' S弱点是了解复杂代码的困难,它' S的力量是易于做重构,这是一个合理的贸易:我们减轻了问题通过倾向于它且善于善于(重构),Scala是不好的(复杂性)。这是出于以下指南的原则。

不可变的东西不要改变,并且在他们不应该时改变的事情和事物.t是一个常见的错误来源。如果您'重新确定是否需要更改某些东西,请将其作为val或collection.seq留下,并使跳转到var或可变。在必要时稍后会降低。

一般来说,如果您实际上建模了随时间变化的东西,则使用类似可变的vars或类似物的集合。很好。例如在视频游戏中,您可能有:

类项目{...}类播放器(var health:int = 100,val项目:mutable.buffer [项目] = mutable.buffer.empty)val Player =新手()

我们实际上建模了健康和物品随时间变化的东西。这可以。在理论上,您可以使用像事件采购或CQR等幻想技术来模拟此"不可变形"在实践中,使用可变状态建模可变的东西是正常的。

类项目{...}类播放器(var health:int = 100,var项目:fulable.buffer [项目] = null)val player = new player()player.items = mutable.buffer.preasty [项目]

在这里,我们正在将项目变量初始化为null,然后再次初始化它"正确"稍后有一个空名单。这是Java和其他语言中的一种非常常见的模式,并且不正常:如果您忘记在使用之前忘记了Player.Items,或者更多 - 可能会忘记您使用的某些方法是使用Player.Item在它设置之前使用。它现在会爆炸或(更差)稍后用NullPointerException。

这是一个真实的例子,从Scala并行集合库中取出,违反了这个原则:

scala>导入scala.collection.parallel._import scala.collection.Parallel._Scala> val pc = fuerable.pararray(1,2,3)pc:scala.collection.parelial.mutable.pararray [int] =帕拉雷(1,2,3)scala> pc.tasksupport = new forkjointasksupport(new scala.concurrent.forkjoin.forkjoinpool(2))scala> PC映射{_ + 1} Res0:Scala.Collection.Parallel.Mutable.Pararray [Int] = Pararray(2,3,4)

如您所见,它使用可变的.tasksupport属性来配置并行映射操作的运行方式。这是糟糕的:它很容易被传递作为映射的参数,无论是明确还是隐含。由于TaskSupport不会模拟任何实际变形的值,使用可变的var只是初始化它绝对是糟糕的风格,谁曾写过它应该感觉不好。

一般情况下,如果您正在建模的内容随时间改变,则可以使用可变性。如果您正在建模的东西没有,并且您只是使用可变性作为一些初始化过程的一部分,您应该重新考虑。

通常,可变代码最终比不可变版版本更快。实现算法时,这更明显,因为CLR等书籍中最常见的快速算法以可变的方式完成。如果具有少量的可变性可以提高您的性能十 - 或百倍,这可以让您通过使用并行性,缓存,批处理以及各种各样的基因来简化您的其余代码。不要害怕做出这个权衡。

def getfibs(n:int):seq [int] = {val fibs = fumable.buffer(1,1)(fibs.length< n){fibs.append(fibs(fibs.length-1)+ fibs( fibs.length-2))} fibs}

def getfibs(n:int,fib:mutable.buffer [int]):Unit = {fibs.clear()fibs.append(1)fibs.append(1)(fibs.length< n){fibs.append (FIBS(FIBS.Length-1)+ FIB(FIBS.LENGTH-2))}}}}}}

即使你和#39; ve决定你和#39;重新在代码的某些部分中介绍可变性,不要让它在任何地方泄漏!理想情况下,它' s封装在一个函数中,在这种情况下,它看起来与使用不可变内部实现的相同功能相同。

请注意,有时您确实需要可变性以跨函数,类或模块边界泄漏。例如,如果您需要性能,上面的第二个例子比第一示例更快,并减少分配,从而减少垃圾收集压力。但是,默认到上面的第一个示例,除非您100%确定您需要perf。

但是,您几乎不需要容器可以是可变的,以及持有容器的变量是可变的!更好的Java代码是

是否希望它是一个可变的var,持有一个不可变的收集或持有可变集合的不可变的val是值得简言的,但你基本上从不希望它是双重可变的

经常,你'请听到人们谈论制作变形状态改变的代码和#34;纯"或者"不变的",而是通过储存大部分不变的,仅追加事件的日志。这有许多不变性的好处,因为即使是新事件改变了东西,旧事件仍在那里,你可以查询"州"通过重新播放到该点的任何时间点的系统。这是您在使用类似可变的vars或类似物中的集合时无法做的事情。缓冲器,并且该技术在许多地方施用于很大的效果。

视频游戏,存储输入日志允许您重新播放会话期间发生的所有内容。未来观看

这些技术都可以在内存中使用,或者持久地持续到磁盘,甚至可以使用仅包含仅附加日志的完整数据库/数据存储。完整解释这些技术如何工作超出本文档的范围。

一般来说,如果您希望这些技术带来的好处:重新播放,隔离,流复制,则所有方法都使用这些技术。但是,通常,对于大多数用例来说,它可能是过度杀戮,除非您知道您想要这些福利,否则不应默认使用。

我使用术语"已发布的接口"与A&#34不同;普通" Java接口或Scala特质。已发布的接口是在程序的中等大小的程序之间的较大界面,包括多个类,对象:由整个包或.jar呈现给开发人员的接口。

对象本码{/ ** *需要一组`t`s和一个函数,它定义了从每个`t` * *的外向边缘可以到达的其他`t` * *返回一组强连接的组件(每个组件*是一个设置的[t])* * o(n + e)在节点n和边缘的总数e * / def强的components [t](节点:设置[t],边缘:t = > set [t]):set [set [t]] = {... 500行疯狂代码...}} object mycode {导入他们的审核码{forplylyconnected components(...,...)}

它封装了一种非琐碎的算​​法,它在内部使用大量变形状态,而且您可能无法赶上自己。

也许它' s使用tarjan' s算法?也许是使用双堆栈算法的'对于这个界面的消费者,你不要关心:你知道你可以在一个设置[t]和t =>设置出传出边缘,它将吐出所有强连接组件的SET [SET [T]]。你看到它有500行的疯狂算法代码,但你不需要关心任何一个。 2线签名和5行Doc-Chought,就​​像在您自己的代码中使用这一切,您需要知道。

当然,你可以' t始终向您的代码呈现一个超简单的单静态函数只有录用的界面:您的代码可能只是做多件事,可能需要以多种方式配置,而不是填充到单个函数' s的争论。这种死亡简单"界面只有静态函数处理已知类型"是渴望的东西。

对象对象{特征碎片{def rador:string} // html构造函数def div(儿童:frag *):frag def p(儿童:frag *):frag def h1(儿童:frag *):frag ...隐性def StringFrag(s:String):frag}对象mycode {导入他们的码._ val frag = div(h1(" hello world"),p("我是段落"))frag .Render //< div>< h1>你好世界和gt;< p>我是一个段落< / p>< / p>< / p>

在这里,他们的代码界面开始让开发人员的需求:您可以' t只需打电话"一个功能"并获取您想要的内容,现在您需要了解一个碎片是什么,它们的刻度曝光的静态方法中的哪一个返回碎片,以及如何通过隐式转换将自己的东西转换为碎片。最后,您也必须知道您可以使用碎片处理方法:在这种情况下,您可以调用渲染将其转换为字符串。只有这样,开发人员可以与图书馆做有用的东西!毕竟,开发人员试图使用您的图书馆ISN' t思考

当然,开发人员必须学习如何在他们可以做他们想要的事情之前完成所有这个碎片的东西,但他们需要更好地学习更好。

此示例接口ISN' t非常复杂,但它肯定比以前的强大竞争组成者更复杂!这也是NOSN' t全部或全部:您可以为您的用户介绍更多或更少的自定义类型和构造函数,并且相应地对局外人讨论了如何使用您的代码。

同样,并不总是可以使您的界面更简单,并且我发布了许多使用这种界面样式的代码。尽管如此,它比需要从课程继承以获得工作......

强制使用API​​的用户继承自上课或特征应该是最后的手段。并不是说围绕继承设计的API是不可用的:它们'在Java World中使用了年龄。尽管如此,如果您在曝光几个静态函数之间进行了选择,则开发开发人员将必须使用的某些类/类型,并强制开发人员从类/特征继承,因此继承应在选项列表中持续用作最后的手段。

这适用该值是否是方法参数,类参数,字段或方法返回类型。一般来说,使用"最简单的"每个用例的事情意味着稍后观看代码的人都可以对该价值进行更强烈的假设。

如果我看到一个原始或内置的集合,我完全知道它包含。如果我看到一个不透明的函数,我知道可以完成的唯一事情是称之为。如果我看到一个简单的案例类,我就可以相对自信,' s一个笨蛋。如果它' s密封的特征,它可能是多种愚蠢的结构之一。如果它'是一种定制的手工轧制类或特质,所有的赌注都熄灭:它可能是什么!

通过从最简单的类型开始并仅在升高的列表时才能在必要时越来越多地,您尊重最小电量和向API用户发送信号的原理,了解您的值是如何使用的。

在可能的情况下,您应该始终使用内置的基元和集合。虽然它们都以各种方式缺陷,但它们是众所周知的,而且"无聊"以及有人看如何使用界面知道它们' re刚刚通过已知的数据类型进行交互。应使用标准int,strings,seqs和选项和组合而不是您自己的自定义版本的相同概念。

如果您发现自己将对象传递给仅在该对象上访问单个字段的方法,请考虑直接传递该字段。例如这:

foo foo(val x:int,Val S:String,Val D:Double){...更多的东西...} Def句柄(foo:foo)= {... foo.x ... //只有一个foo} val foo = new foo(123," hellol" 1.23)处理(foo)

类foo(val x:int,Val S:String,Val D:双)Def句柄(x:int)= {... x ... //只有一个使用x} val foo = new foo(123, " hellol",1.23)句柄(foo.x)

这使得句柄实际上需要整个foo,它包含x:int和s:string和可能的其他东西。 它只需要单个整数x。 此外,如果我们想在我们的代码库中的其他部分中重新使用手柄或在我们的单位测试中锻炼,我们赢得了' t需要走 ......