最近我遇到了代码可读性的问题。
我有一个函数执行了一个操作,并返回了一个表示该操作ID的字符串以供将来参考(有点像Windows中的OpenFile返回句柄)。用户稍后将使用此ID来开始操作并监视其完成。出于互操作性的考虑,该ID必须为随机字符串。这创建了一个签名非常不清楚的方法,如下所示:



 public string CreateNewThing()
 


这使得返回类型的意图不清楚。我想将此字符串用另一种类型包装,这样可以使它的含义更清晰,如: >类型将仅包含字符串,并且在使用此字符串时都将使用。

显然,这种操作方式的优点是类型安全性更高,意图更清晰,但它还会带来更多代码和不是很习惯的代码。一方面,我喜欢增加的安全性,但同时也会造成很多混乱。

出于安全原因,您认为将简单类型包装在类中是一种好习惯吗?

评论

您可能对阅读Tiny Types有兴趣。我玩了一段时间,不推荐它,但这是一种有趣的思考方法。 darrenhobbs.com/2007/04/11/tiny-types

一旦使用了Haskell之类的东西,就会看到使用类型有多大帮助。类型有助于确保程序正确。

即使是像Erlang这样的动态语言,也可以从类型系统中受益,这就是为什么它具有Haskellers熟悉的类型规范系统和类型检查工具的原因,尽管它是动态语言。像C / C ++这样的语言,其中任何东西的真实类型都是我们同意编译器以某种方式对待的整数,或者Java,如果您决定假装类是类型,则几乎可以归类为类型,这会在语言中带来语义重载问题语言(这是“类”还是“类型”?)或类型不明确(这是“ URL”,“ String”还是“ UPC”?)。最后,使用任何可以消除歧义的工具。

我不确定C ++的想法从何而来。使用C ++ 11,没有一个编译器制造商看到为每个lambda赋予自己唯一类型的问题。但是TBH真的不能期望在评论“ C / C ++类型系统”时能找到很多见识,就像两者共享一个类型系统一样。

@JDB-不,不是。甚至都不相似。

#1 楼

stringint之类的基元在业务领域中没有任何意义。这样做的直接后果是,您可能会在期望产品ID时错误地使用URL,或者在期望价格时错误地使用数量。 :


规则3:包装所有原语和字符串

在Java语言中,int是原语,而不是实际对象,因此它遵循与以下规则不同的规则:对象。它与非面向对象的语法一起使用。更重要的是,一个int本身只是一个标量,因此没有任何意义。当方法将int作为参数时,方法名称需要完成所有表达意图的工作。如果相同的方法将“小时”作为参数,则更容易了解发生了什么。


同一文档说明了另一个好处: >
诸如Hour或Money之类的小对象也为我们提供了放置行为的明显场所,否则这些行为本来会在其他类的周围乱扔。通常很难跟踪与这些类型相关的代码的确切位置,通常会导致严重的代码重复。如果存在Price: Money类,很自然地可以在内部找到范围检查。相反,如果使用int(更糟糕的是double)存储产品价格,谁应该验证范围?产品?回扣?最后,文档中未提及的第三个好处是能够相对容易地更改基础类型。如果今天我的ProductId具有short作为其基础类型,后来我需要改用int,则更改的代码可能不会覆盖整个代码库。缺点-相同的论点适用于对象健美操锻炼的每条规则-如果很快变得不堪重负而无法为所有内容创建一个类。如果Product包含从ProductPrice继承的PositivePrice,而Price继而从Money继承,则这不是干净的体系结构,而是一个完全混乱的地方,为了查找单个对象,维护者应每次打开几十个文件。

要考虑的另一点是创建其他类的成本(就代码行而言)。如果包装器是不可变的(通常应该是不变的),则意味着,如果采用C#,则必须至少在包装器内:


属性获取器,
其后备字段,
将值分配给后备字段的构造函数,
自定义ToString()
XML文档注释(行很多),
一个Equals和一个GetHashCode会覆盖(也是很多LOC)。

并最终在相关时:覆盖==!=运算符,
最终,隐式转换运算符的重载可以无缝地与封装类型进行转换,
代码契约(包括不变式,这是一个相当长的方法,其三个属性),
在XML序列化,JSON序列化或将值存储到数据库或从数据库存储/加载值时将使用几个转换器。 e,这就是为什么您可以完全确定此类包装纸的长期盈利能力的原因。 Thomas Junk解释的范围概念在这里特别重要。编写一百个LOC代表整个代码库中使用的ProductId看起来非常有用。为一段代码编写这种大小的类,使一个方法中的三行代码更具可疑性。

结论:
当(1)帮助减少错误,(2)减少代码重复的风险或(3)帮助以后更改基础类型时,请将原语包装在应用程序的业务域中有意义的类中。 br />不要自动包装在代码中找到的每个原语:在许多情况下,使用stringint完全可以。

实际上,在public string CreateNewThing()中,返回的是ThingId类的实例string的对象可能会有所帮助,但您也可能:


返回Id<string>类的实例,该实例是通用类型的对象,指示基础类型是字符串。您将获得可读性的好处,而不必维护很多类型。

返回Thing类的实例。如果用户仅需要ID,则可以轻松实现:

var thing = this.CreateNewThing();
var id = thing.Id;




评论


我认为自由更改底层类型的能力在这里很重要。今天是字符串,但也许是GUID或明天很长。创建类型包装器会将信息隐藏起来,从而减少沿途的更改。 ++好答案。

–RubberDuck
2015年5月3日在18:28

对于货币,请确保货币要么包含在Money对象中,要么确保您的整个程序使用相同的货币(然后应将其记录在案)。

–PaŭloEbermann
2015年5月3日19:23

@Blrfl:尽管在某些情况下有必要进行深度继承,但我通常会在强迫性按本地支付的态度下,在过度设计的代码中看到这种继承,而忽略了可读性。因此,这是我回答这一部分的负面特征。

– Arseni Mourzenko
2015年5月3日在22:24

@ArTs:那是“让我们谴责这个工具,因为有人以错误的方式使用它”这样的说法。

– Blfl
2015年5月4日15:00

@MainMa有关含义可能避免代价高昂的错误的示例:en.wikipedia.org/wiki/Mars_Climate_Orbiter#Cause_of_failure

–cbojar
2015年5月4日15:12

#2 楼

我将使用范围作为经验法则:生成和使用此类values的范围越窄,则创建代表该值的对象的可能性就越小。

id = generateProcessId();
doFancyOtherstuff();
job.do(id);


然后范围非常有限,我认为将其定义为类型没有任何意义。将其传递给另一层(甚至另一个对象),然后为该类型创建类型将是非常有意义的。

评论


很好的一点。显式类型是传达意图的重要工具,跨对象和服务边界尤为重要。

– Avner Shahar-Kashtan
2015年5月4日4:57



尽管您尚未将doFancyOtherstuff()的所有代码重构为一个子例程,但+1可能会认为job.do(id)引用的本地性不足以保留为简单字符串。

–马克·赫德
15年5月6日在2:46

这是真的!在您拥有私有方法的情况下,您会知道外界永远不会使用该方法。之后,当该类受到保护时,以及当该类为公共但不属于公共api的部分时,您可以多想一点。范围范围范围和许多技巧可以使许多原始类型和许多自定义类型之间的界线处于良好位置。

–塞缪尔
15年5月6日在21:58

#3 楼

静态类型系统都是关于防止数据的不正确使用。请将两个字符串相乘。使用某人的名字作为URL。

我们可能会尝试使用double作为价格和长度,或者使用string作为名称和URL。但是这样做会破坏我们很棒的类型系统,并允许这些误用通过语言的静态检查。特别是字符串问题。它们经常成为“通用数据类型”。我们认为34.25是字符34.25。我们认为日期是字符05-03-2015。我们将UUID视为字符75e945ee-f1e9-11e4-b9b2-1697f925ec7b。
但是这种思维模式会损害API抽象。 ,似乎在我的思维机制中没有任何作用。
阿尔伯特·爱因斯坦

类似地,文本表示形式在设计类型和API中不应发挥任何作用。当心string! (以及其他过于通用的“原始”类型)

类型传达“什么操作有意义”。
例如,我曾经在HTTP REST API的客户端上工作。正确完成的REST使用超媒体实体,该实体具有指向相关实体的超链接。在此客户端中,不仅键入了实体(例如,用户,帐户,订阅),还键入了到这些实体的链接(UserLink,AccountLink,SubscriptionLink)。这些链接只不过是Uri周围的包装器,但单独的类型使得无法尝试使用AccountLink来获取用户。如果一切正常,或者甚至更糟糕的是Uri,这些错误只能在运行时发现。
同样,在您的情况下,您拥有的数据仅用于一个目的:识别string。不应将其用于其他任何用途,我们也不应尝试使用构成的随机字符串来识别Operation。创建一个单独的类可以提高代码的可读性和安全性。

当然,所有好的东西都可以过量使用。请考虑


它为您的代码增加了多少清晰度


它多久使用一次


如果出于某种不同的目的以及在代码接口之间频繁使用某种“类型”的数据(抽象意义上),那么它很适合作为单独的类,而以冗长为代价。

评论


“字符串通常成为'通用'数据类型”-这就是嘲讽“字符串型语言”的绰号。

–MSalters
2015年5月4日在8:52

@MSalters,哈哈,非常好。

– Paul Draper
15年5月4日在16:29



当然,2015年5月3日解释为日期是什么意思?

–用户
2015年5月6日14:02

#4 楼


出于安全原因,您认为将简单类型包装在类中是一种好习惯吗?


有时。需要权衡使用string而不是更具体的OperationIdentifier可能引起的问题的情况。他们的严重程度是多少?他们的可能性如何?

那么您需要考虑使用其他类型的成本。使用有多痛苦?要做多少工作?

在某些情况下,您可以通过选择一种好的具体类型来节省时间和精力。在其他情况下,这样做是不值得的。

总的来说,我认为应该比今天做更多的事情。如果您的实体在您的域中具有某种意义,那么最好将其作为自己的类型,因为该实体更可能随业务而变化/成长。

#5 楼

我通常都同意,很多时候您应该为基元和字符串创建类型,但是由于上述答案建议在大多数情况下创建类型,因此我将列出一些为什么/何时不这样做的原因:


性能。我必须在这里参考实际的语言。在C#中,如果客户机ID较短,并且将其包装在一个类中,则会造成大量开销:内存,因为在64位系统上它现在为8字节,并且自从现在分配到堆上以来,速度一直很高。
对类型进行假设时。如果客户端ID较短,并且您有一些以某种方式打包它的逻辑-通常您已经对类型进行了假设。现在,在所有这些地方,您都必须破坏抽象。如果只是一个地方,那没什么大不了的;如果到处都是,那么您可能会发现使用原始语言的时间减少了一半。
因为并非每种语言都有typedef。对于不使用的语言和已经编写的代码,进行这样的更改可能是一项艰巨的任务,甚至可能会引入错误(但每个人都拥有覆盖面广的测试套件,对吗?)。
在某些情况下,降低可读性。我该如何打印?我该如何验证?我应该检查null吗?是否所有问题都需要您深入研究类型的定义。


评论


当然,如果包装器类型是结构而不是类,则short(例如)具有相同的包装或展开成本。 (?)

–数学兰花
2015年5月5日13:58

封装自己的验证不是一个好的设计吗?还是创建一个辅助类型进行验证?

–尼克·乌德尔(Nick Udell)
15年5月6日在9:01

不必要的打包或乱涂是一种反模式。信息应透明地在应用程序代码中流动,除非明确和确实需要进行转换。类型是好的,因为类型通常会将少量好的代码(位于单个位置)替换为遍布整个系统的大量不良实现。

–托马斯W
15年5月7日在1:25

#6 楼

不,您不应该为“所有内容”定义类型(类)。但是,正如其他答案所述,这样做通常很有用。在编写,测试和维护代码时,由于缺少合适的类型或类,您应该有意识地(如果可能)发展一种过于摩擦的感觉。对我来说,太多摩擦的发生是当我想将多个基本值合并为一个值时,或者当我需要验证值时(即确定基本类型的所有可能值中的哪个对应于有效值) “隐式类型”。)

我发现了一些问题,例如您在问题中提出的内容,可能导致我的代码中的过多设计。我养成了一种习惯,刻意避免编写多余的代码。希望您正在为代码编写良好的(自动化的)测试-如果您愿意,则可以轻松地重构代码并添加类型或类,如果这样做可以为正在进行的代码开发和维护带来净收益。
Telastyn的答案和Thomas Junk的答案都很好地说明了相关代码的上下文和用法。如果您在单个代码块中使用一个值(例如method,loop,using块),那么只使用基本类型就可以了。如果您在其他许多地方重复使用一组值,则使用原始类型甚至更好。但是,使用一组值的频率越高,范围越广,并且该组值与原始类型表示的值的对应关系越不紧密,则应考虑将这些值封装在一个类或类型中的可能性就越大。 >

#7 楼

从表面上看,您需要做的就是识别一个操作。

另外,您还说了一个操作应该执行的操作:操作[和]监视其完成情况


您说的方式好像是“只是如何使用标识”,但是我想说这些是描述操作的属性。
对我来说,这听起来像是类型定义,甚至有一个非常适合的模式,称为“命令模式”。 >命令模式用于封装稍后执行动作或触发事件所需的所有信息。你想做你的操作。 (比较我在两个引号中都加粗的短语)而不是返回字符串,而是以抽象的意义返回Operation的标识符,例如,指向oop中该类对象的指针。注释


将此类称为命令,然后在其中没有逻辑将是wtf-y。没错
请记住,模式是非常抽象的,实际上如此抽象,以至于它们有点元。也就是说,他们经常抽象编程本身,而不是某些现实世界的概念。命令模式是函数调用的抽象。 (或方法)就好象有人在传递参数值后立即在执行前按下了暂停按钮,然后又在执行前按了暂停,稍后再恢复。适用于任何范例。我想给出为什么将逻辑放入命令中被认为是一件坏事的原因。您
会想:“哦,所有这些功能都应该放在单独的类中。”您可以从
命令中重构逻辑。
如果您在命令中具有所有逻辑,那么很难进行测试
并妨碍进行测试。 “天哪,为什么我必须使用
这个命令废话?我只想测试此函数是否在
上吐出
1,我不想以后再调用它,我想测试现在就可以了!”
如果您在命令中拥有所有逻辑,那么在完成报告后就不必花费太多时间了。如果您考虑默认情况下
要同步执行的函数,则在完成执行时通知
是没有意义的。也许您正在启动另一个
线程,因为您的操作实际上是将头像呈现为电影格式
(可能需要在树莓派上添加其他线程),也许您已经
从服务器中获取了一些数据, ...无论是什么原因,如果在完成报告时都有报告的原因,那可能是因为发生了一些异步操作(这是一个单词吗?)。我认为运行
线程或联系服务器是您的
命令中不应包含的逻辑。这在某种程度上是一个元论证,也许是有争议的。如果您认为这没有任何意义,请在评论中告诉我。

回顾一下:命令模式允许您将功能
包装到一个对象中,以便稍后执行。为了模块化(功能存在,无论是否通过命令执行都存在功能),可测试性(功能应该可以在没有命令的情况下进行测试)以及所有其他本质上表达编写好的代码需求的流行语,您不会放置实际逻辑进入命令。


由于模式是抽象的,因此很难提出良好的现实隐喻。这是一个尝试:

“嘿,奶奶,您能在12点按一下电视上的录制按钮,这样我就不会错过第1频道上的辛普森一家吗?”

我的祖母不知道按下记录按钮会发生什么技术上的变化。逻辑在其他地方(在电视中)。那是一件好事。该功能已封装并且命令中隐藏了信息,它是API的用户,不一定是逻辑的一部分...哦,天哪,我再次不休,我现在最好完成此编辑。

评论


你是对的,我还没有那样想过。但是命令模式并不是100%合适的,因为操作的逻辑封装在另一个类中,并且我不能将其放在Command的对象中。将此类称为命令,然后在其内部没有逻辑将是很可能的。

– Ziv
2015年5月5日在9:03

这很有道理。考虑返回一个操作,而不是一个OperationIdentifier。这类似于您返回Task或Task 的C#TPL。 @Ziv:您说“操作的逻辑封装在另一个类中”。但这真的意味着您也不能从这里提供它吗?

– Moby磁盘
2015年5月5日13:44

@Ziv,我希望与众不同。看看我添加到答案中的原因,看看它们对您是否有意义。 =)

–空
2015年5月5日14:28

#8 楼

包装原始类型背后的想法,


建立特定于域的语言
通过传递不正确的值来限制用户所犯的错误

显然,这很困难并不适合在任何地方使用,但是将类型包装在需要的位置很重要,例如,如果您有Order类, >
 Order
{
   long OrderId { get; set; }
   string InvoiceNumber { get; set; }
   string Description { get; set; }
   double Amount { get; set; }
   string CurrencyCode { get; set; }
}
 


查找订单的重要属性主要是OrderId和InvoiceNumber。而且Amount和CurrencyCode是密切相关的,如果有人更改了CurrencyCode而未更改Amount,则该订单不再被视为有效。在这种情况下很有意义,并且可能进行包装说明没有任何意义。因此首选结果可能看起来像是

     Order
    {
       OrderIdType OrderId { get; set; }
       InvoiceNumberType InvoiceNumber { get; set; }
       string Description { get; set; }
       Currency Amount { get; set; }
    }
 


所以没有理由包装所有东西,但实际上很重要。

#9 楼

不受欢迎的观点:

通常不应该定义一个新类型! IBM在这里将其描述为一种不好的做法(在这种情况下,它们专门针对滥用泛型)。与所有数字原语。但是,如果您定义一个新的类Percentage(包装一个可以在0〜1范围内的double值),那么所有这些函数都将无用,并且需要被那些需要了解Percentage类的内部表示形式的类包装(甚至更糟) 。

伪类型会传播病毒

创建多个库时,您经常会发现这些伪类型会传播病毒。如果您在一个库中使用上述Percentage类,则可能需要在库边界处将其转换(失去创建该类型所必须的所有安全性/含义/逻辑/其他原因),或者必须创建这些类其他库也可以访问。用您的类型感染新的库,其中可能只需要简单的double就可以了。建议不要将其包装在伪类中。仅在存在严重业务限制的情况下才应包装类。在其他情况下,正确命名变量应该在传达含义方面大有帮助。

示例:

uint可以完美地表示UserId,我们可以继续将Java的内置运算符用于uint(例如==),并且不需要业务保护用户ID的“内部状态”的逻辑。

评论


同意。所有编程原理都很好,但是在实践中,您必须使其同样工作

–伊万
15年5月6日在9:09

如果您看到有任何向英国最贫困人口贷款的广告,您会发现将百分比范围从0..1换成两倍的百分比无效。

– gnasher729
2015年5月6日14:53

在C / C ++ / Objective C中,您可以使用typedef,它为您提供了现有原始类型的新名称。另一方面,无论底层类型是什么,您的代码都应该工作,例如,如果将OrderId从uint32_t更改为uint64_t(因为我需要处理40亿以上的订单)。

– gnasher729
2015年5月6日14:56

我不会为您的英勇而投票,但是伪类型和最高答案中定义的类型是不同的。这些是特定于业务的类型。伪类型是仍然通用的类型。

–0fnt
2015年6月6日在2:22

@ gnasher729:C ++ 11还具有用户定义的文字,这些文字使用户的包装过程非常贴心:距离d = 36.0_mi + 42.0_km; 。 msdn.microsoft.com/zh-CN/library/dn919277.aspx

–damix911
16年8月12日在11:41



#10 楼

与所有技巧一样,知道何时应用规则是技巧。如果您使用类型驱动的语言创建自己的类型,则会进行类型检查。因此,通常,这将是一个好的计划。

NamingConvention是可读性的关键。两者结合可以清楚地传达意图。
但是///仍然有用。

所以,是的,当它们的生命周期超出类边界时,我会说创建许多自己的类型。还请考虑同时使用StructClass,而不是始终使用Class。

评论


这是什么意思?

–加布
15年5月5日在20:01

///是C#中的文档注释,它允许intellisense在调用方法点显示签名的文档。考虑记录代码的最佳方法。可以使用工具进行养殖,并提供触及智能的行为。 msdn.microsoft.com/zh-CN/library/tkxs89c5%28v=vs.71%29.aspx

– phil soady
15年5月6日在12:38