“避免溜溜球问题”是允许“原始痴迷”的正当理由吗?


42

根据什么时候原始的迷恋不是代码气味?,我应该创建一个ZipCode对象来代表一个邮政编码而不是一个String对象。

但是,根据我的经验,我更喜欢看到

public class Address{
    public String zipCode;
}

代替

public class Address{
    public ZipCode zipCode;
}

因为我认为后者需要我转到ZipCode类才能理解该程序。

而且我相信如果每个原始数据字段都被一个类替换,那么我需要在许多类之间移动以查看定义,这感觉就像是在遭受溜溜球问题(一种反模式)。

因此,我想将ZipCode方法移到新类中,例如:

旧:

public class ZipCode{
    public boolean validate(String zipCode){
    }
}

新:

public class ZipCodeHelper{
    public static boolean validate(String zipCode){
    }
}

因此只有需要验证邮政编码的人才能依赖ZipCodeHelper类。而且我发现保持原始痴迷的另一个“好处”是:它使类看起来像其序列化形式(如果有),例如:带有字符串列zipCode的地址表。

我的问题是,“避免溜溜球问题”(在类定义之间移动)是否是允许“原始痴迷”的有效理由?


9
@ jpmc26然后您会惊讶地看到我们的邮政编码对象有多复杂-并不是说它是对的,但是它确实存在
Jared Goguen

9
@ jpmc26,我看不到您是如何从“复杂”变为“设计不良”的。复杂代码通常是简单代码与现实世界(而不是我们希望存在的理想世界)的复杂性联系在一起的结果。 “返回到两页功能。是的,我知道,它只是显示窗口的简单功能,但是上面已经长了很多毛发和东西,没人知道为什么。好吧,我告诉你原因:那是bug修复。”
Kyralessa

18
@ jpmc26-包装像ZipCode这样的对象的要点是类型安全。邮政编码不是字符串,而是邮政编码。如果函数需要邮政编码,则您只能传递邮政编码,而不是字符串。
DavorŽdralo

4
这感觉是特定于语言的,不同的语言在这里做不同的事情。@DavorŽdralo在同一范围内,我们还应该添加很多数字类型。“仅正整数”,“仅偶数”也都可以是类型。
paul23

6
@ paul23是的,我们没有这些语言的主要原因是,许多语言不支持定义它们的优雅方法。如果仅将“年龄”定义为与“以摄氏度为单位的温度”不同的类型,这是完全合理的,前提是这样才能将“ userAge == currentTemperature”检测为废话。
IMSoP

Answers:


116

其假设是,你并不需要溜溜球的邮编类来了解Address类。如果ZipCode设计合理,那么只需阅读Address类,就可以清楚地知道它的作用。

程序不是端到端读取的-通常,程序太复杂而无法实现。您不能同时将所有代码保存在一个程序中。因此,我们使用抽象和封装将程序“分块”为有意义的单元,因此您可以查看程序的一部分(例如Address类),而不必阅读其依赖的所有代码。

例如,我确定您每次在代码中遇到String时,都不会溜溜地阅读String的源代码。

将类从ZipCode重命名为ZipCodeHelper意味着现在有两个独立的概念:邮政编码和邮政编码帮助器。所以复杂了两倍。现在,类型系统无法帮助您区分任意字符串和有效的邮政编码,因为它们具有相同的类型。这是适合“迷恋”的地方:您只是想避免在原语周围使用简单的包装类型,所以建议使用更复杂,更不安全的替代方法。

在没有验证或其他依赖于此特定类型的逻辑的情况下,恕我直言,使用原语是合理的。但是,一旦添加任何逻辑,将这种逻辑与类型封装在一起就容易得多。

至于序列化,我认为这听起来像是您所使用框架的限制。当然,您应该能够将ZipCode序列化为字符串或将其映射到数据库中的列。


2
我同意“有意义的单元”(main-)部分,但不太同意邮政编码和邮政编码验证是同一概念。ZipCodeHelper(我宁愿称之为ZipCodeValidator)可能很好地建立与Web服务的连接以完成其工作。这不属于“保留邮政编码数据” 这一单一责任的一部分。仍然可以通过使ZipCode构造函数与Java的package-private等效,并使用ZipCodeFactory始终调用验证器的a 来使类型系统禁止无效的邮政编码。
R. Schmitz

16
@ R.Schmitz:从单一责任原则的意义上讲,“责任”不是什么意思。但是无论如何,只要封装了邮政编码及其验证,您当然应该使用所需数量的类。OP建议使用帮助程序,而不是封装邮政编码,这是个坏主意。
JacquesB

1
我要尊重地不同意。SRP表示一类应具有“一个且只有一个更改理由”(更改“邮政编码由什么组成”与“如何验证”)。清洁代码一书中进一步详细说明了这种特定情况:“ 对象将其数据隐藏在抽象后面,并公开对该数据进行操作的功能。数据结构公开其数据且没有任何有意义的功能。 ”- ZipCode将是“数据结构”并且ZipCodeHelper,“对象”在任何情况下,我想我们同意,我们不应该有Web连接传递给邮编构造。
R.施米茨

9
在没有验证或其他依赖于此特定类型的逻辑的情况下,恕我直言,使用原语是合理的。=>我不同意。即使所有值都是有效的,我仍然倾向于将语义传达给语言,而不是使用原语。如果可以在对当前语义使用无意义的原始类型上调用函数,则它不应是原始类型,而应是仅定义了明智函数的适当类型。(例如,使用intID可以将ID乘以ID ...)
Matthieu M.

@ R.Schmitz我认为邮政编码不是您所要区分的一个好例子。经常会发生变化的东西可能是分离类FooFooValidator类的候选者。我们可以使用一个ZipCode类来验证格式,并使用一个类ZipCodeValidator来击中某些Web服务以检查格式ZipCode是否正确。我们知道邮政编码会发生变化。但是实际上,我们将在ZipCode或某些本地数据库中封装一个有效的邮政编码列表。
不U

55

如果可以做:

new ZipCode("totally invalid zip code");

ZipCode的构造函数执行以下操作:

ZipCodeHelper.validate("totally invalid zip code");

然后,您破坏了封装,并向ZipCode类添加了一个非常愚蠢的依赖项。如果构造函数没有调用,ZipCodeHelper.validate(...)那么您将在自己的孤岛中隔离逻辑,而无需实际执行。您可以创建无效的邮政编码。

validate方法应该是ZipCode类上的静态方法。现在,将“有效”邮政编码的知识与ZipCode类捆绑在一起。鉴于您的代码示例看起来像Java,如果给出了错误的格式,则ZipCode的构造函数应引发异常:

public class ZipCode {
    private String zipCode;

    public ZipCode(string zipCode) {
        if (!validate(zipCode))
            throw new IllegalFormatException("Invalid zip code");

        this.zipCode = zipCode;
    }

    public static bool validate(String zipCode) {
        // logic to check format
    }

    @Override
    public String toString() {
        return zipCode;
    }
}

构造函数检查格式并引发异常,从而防止创建无效的邮政编码,并且静态validate方法可用于其他代码,因此将格式检查的逻辑封装在ZipCode类中。

ZipCode类的此变体中没有“溜溜球”。这就是所谓的适当的面向对象编程。


在这里,我们也将忽略国际化,这可能需要另一个名为ZipCodeFormat或PostalService的类(例如,PostalService.isValidPostalCode(...),PostalService.parsePostalCode(...)等)。


28
注意:@Greg Burkhardt的方法的主要优点是,如果有人给您一个ZipCode对象,您可以相信它包含有效字符串,而无需再次检查它,因为它的类型和成功构造的事实为您提供了那保证。如果改为传递字符串,您可能会觉得有必要在代码的各个位置“断言validate(zipCode)”,只是为了确保您具有有效的邮政编码,但是通过成功构建的ZipCode对象,您可以相信其内容有效,无需再次检查。
盖伊

3
@ R.Schmitz:该ZipCode.validate方法是可以在调用引发异常的构造函数之前执行的预检查。
格雷格·伯格哈特

10
@ R.Schmitz:如果您担心令人烦恼的异常,另一种构造方法是将ZipCode构造函数设为私有,并提供一个公共的静态工厂函数(Zipcode.create?),该函数执行传入参数的验证,如果失败,则返回null,否则构造一个ZipCode对象并返回它。当然,调用者将始终必须检查是否有空返回值。另一方面,例如,如果您习惯于在构造ZipCode之前始终进行验证(regex?validate?等),那么实际上该异常可能不会那么令人烦恼。
盖伊

11
返回Optional <ZipCode>的工厂函数也是可能的。然后,调用者别无选择,只能明确处理工厂功能的可能故障。无论如何,无论哪种情况,该错误都会在远离原始错误的地方被客户端代码发现,而不是在更晚的地方被发现。
盖伊

6
您不能独立地验证ZipCode,所以不要。您确实需要Country对象来查找ZipCode / PostCode验证规则。
约书亚

11

如果您在这个问题上花了很多力气,也许您使用的语言不是适合该工作的工具?这种“域类型基元”很容易用例如F#表示。

例如,您可以在其中编写:

type ZipCode = ZipCode of string
type Town = Town of string

type Adress = {
  zipCode: ZipCode
  town: Town
  //etc
}

let adress1 = {
  zipCode = ZipCode "90210"
  town = Town "Beverly Hills"
}

let faultyAdress = {
  zipCode = "12345"  // <-Compiler error
  town = adress1.zipCode // <- Compiler error
}

这对于避免常见错误(例如比较不同实体的ID)非常有用。而且由于这些类型化的原语比C#或Java类轻得多,因此最终您将真正使用它们。


有趣的是,如果要强制执行验证,会是什么样ZipCode
绿巨人

4
@Hulk您可以用F#编写OO样式,并将类型变成类。但是,我更喜欢函数样式,声明类型为ZipCode =字符串的private ZipCode类型,并添加带有create函数的ZipCode模块。这里有一些例子:gist.github.com/swlaschin/54cfff886669ccab895a
古兰经

@ Bent-Tranberg感谢您的编辑。没错,简单类型的缩写不会赋予编译时类型安全性。
古兰特

如果您收到的是我的第一条评论,那么我将其删除了,原因是我首先误解了您的来源。我没有足够仔细地阅读它。当我尝试编译它时,我终于意识到您实际上是在试图证明这一点,因此我决定进行编辑以使其正确。
Bent Tranberg

是的 我的原始消息来源是有效的,但不幸的是,其中包括被认为无效的示例。h!如果甲肝只是挂Wlaschin而不是键入自己的代码:) fsharpforfunandprofit.com/posts/...
Guran

6

答案完全取决于您实际上要对邮政编码执行的操作。这有两种极端的可能性:

(1)保证所有地址都在一个国家中。完全没有例外。(例如,没有外国客户,或者没有为外国客户工作时其私人地址在国外的雇员。)该国家/地区具有邮政编码,可以期望他们永远不会出现严重问题(即,他们不需要格式自由的输入例如“当前为D4B 6N2,但每2周更改一次”)。邮政编码不仅用于寻址,还用于验证付款信息或类似目的。-在这种情况下,邮政编码类非常有意义。

(2)地址几乎可以在每个国家/地区使用,因此数十个或数百个具有或不具有邮政编码(以及成千上万的怪异异常和特殊情况)的寻址方案都是相关的。实际上,仅要求使用“邮政编码”来提醒那些使用邮政编码的国家/地区的人们不要忘记提供他们的密码。仅使用地址,以便如果某人失去了对其帐户的访问权限,并且可以证明其姓名和地址,则将恢复访问权限。-在这种情况下,所有相关国家/地区的邮政编码类别将是一项巨大的努力。幸运的是根本不需要它们。


3

其他答案涉及OO域建模以及使用更丰富的类型来表示您的价值。

我没有不同意,尤其是考虑到您发布的示例代码。

但是我也想知道这是否真的回答了您的问题的标题。

考虑以下情形(从我正在从事的实际项目中提取):

您在与中央服务器对话的现场设备上有一个远程应用程序。设备条目的数据库字段之一是该现场设备所在地址的邮政编码。您不必担心邮政编码(或与此有关的地址的其余部分)。所有关心它的人都在HTTP边界的另一端:您恰好是数据真相的唯一来源。它在您的域建模中没有位置。您只需记录,验证,存储它,然后根据要求将其在JSON Blob中拖移到其他位置。

在这种情况下,除了使用SQL regex约束(或其ORM等效项)验证插入之外,做很多事情都可能对YAGNI品种造成过大杀伤力。


6
您可以将SQL regex约束视为合格类型-在数据库中,邮政编码不存储为“ VarChar”,而是“受此规则约束的VarChar”。在某些DBMS中,您可以轻松地给该类型+约束一个名称作为可重用的“域类型”,并且我们回到了为数据提供有意义的类型的建议位置。我原则上同意您的回答,但认为示例不匹配;一个更好的例子是您的数据是“原始传感器数据”,而最有意义的类型是“字节数组”,因为您不知道数据的含义。
IMSoP

@IMSoP有趣的一点。但是不确定我是否同意:您可以使用正则表达式验证Java(或任何其他语言)的字符串邮政编码,但仍将其作为字符串而不是更丰富的类型来处理。根据域逻辑,可能需要进行进一步的操作(例如,确保邮政编码与状态匹配,使用正则表达式很难/不可能进行验证)。
贾里德·史密斯

可以,但是一旦这样做,便将其归因于特定于域的行为,而这正是引用的文章所说的应该导致自定义类型的创建。问题不在于您是否可以以一种或另一种方式来实现,而是您是否应该假设编程语言为您提供了选择。
IMSoP

您可以将事物建模为RegexValidatedString,其中包含字符串本身和用于验证字符串的正则表达式。但是除非每个实例都有一个唯一的正则表达式(这是可能的,但不太可能),否则这似乎有点愚蠢且浪费内存(可能还有正则表达式的编译时间)。因此,您可以将正则表达式放入单独的表中,并在每个实例中留下一个查找键来查找它(由于间接性,这可能会更糟),或者您可以为每种共享该规则的常见类型的值找到某种存储方式- -例如 如IMSoP所说,域类型或等效方法上的静态字段。
Miral

1

ZipCode仅当您的Address类也没有TownName属性时,抽象才有意义。否则,您将获得一半的抽象:邮政编码指定城镇,但是这两个相关的信息位在不同的类别中。这没有任何意义。

但是,即使那样,它仍然不是原始痴迷的正确应用(或者是解决方案)。据我了解,这主要集中在两件事上:

  1. 使用基元作为方法的输入(甚至输出)值,尤其是在需要基元集合时。
  2. 这些类会随着时间的推移而增加额外的属性,而无需重新考虑其中的某些属性是否应分组为自己的子类。

你的情况都不是。地址是一个定义明确的概念,具有明显必要的属性(街道,数字,邮政编码,城镇,州,国家/地区...)。还有一点要没有理由打破这种数据,因为它有一个单一的责任:指定地球上的位置。地址需要所有这些字段才能有意义。半个地址毫无意义。

这就是您知道不需要进一步细分的原因:进一步细分会降低Address该类的功能意图。同样,您不需要NamePerson该类中使用子类,除非Name(在没有人陪伴的情况下)在您的域中是有意义的概念。(通常)不是。名称是用来识别人的,它们通常没有价值。


1
@RikD:从答案中可以看出:除非类中没有Name有意义的人)是有意义的概念否则不需要在Name类中使用子类。” Person对名称进行自定义验证后,名称便成为您域中有意义的概念;我明确提到这是使用子类型的有效用例。其次,对于邮政编码验证,您要引入其他假设,例如需要遵循给定国家/地区格式的邮政编码。您提出的主题比OP的问题的意图要广泛得多。
平坦的

5
地址是一个定义明确的概念,具有明显必要的属性(街道,数字,邮编,镇,州,国家/地区)。 ”- 嗯,这完全是错误的。为了解决这个问题,请查看亚马逊的地址表。
R. Schmitz

4
@Flater好吧,我不会怪您没有阅读虚假的完整列表,因为它很长,但是字面上包含“地址将有一条街道”,“一个地址需要一个城市和一个国家”,“一个地址将具有邮政编码”等,这与引用的句子所说的相反。
R. Schmitz

8
@GregBurghardt“邮政编码假定使用美国邮政服务,您可以从邮政编码中得出城镇名称。城市可以有多个邮政编码,但是每个邮政编码仅与一个城市绑定。” 通常这是不正确的。我的邮政编码主要用于附近的城市,但我的住所不在那里。邮政编码并不总是与政府界限保持一致。例如,42223包含来自TN和KY的县
JimmyJames

2
在奥地利,有一个只能从德国进入的山谷(en.wikipedia.org/wiki/Kleinwalsertal)。该地区有一项特殊条约,除其他外,还包括该地区的地址同时具有奥地利和德国的邮政编码。因此,通常来说,您甚至不能假设一个地址只有一个有效的邮政编码;)
绿巨人

1

从文章:

更一般而言,溜溜球问题也可以指任何人为了理解一个概念而必须在不同信息源之间不断翻转的任何情况。

读取源代码的频率远远超过写入源代码的频率。因此,必须在许多文件之间切换的溜溜球问题是一个问题。

但是,,在处理高度相互依赖的模块或类(彼此之间来回调用)时,溜溜球问题显得更为重要。这些是一种特殊的噩梦,很可能是溜溜球问题的创造者想到的。

但是- 是的,避免过多的抽象层很重要!

在某种程度上,所有非平凡的抽象都是泄漏的。-泄漏抽象定律。

例如,我不同意mmmaaa的回答中的假设,即“您不需要溜溜[(访问)] ZipCode类就可以了解Address类”。我的经验是,您至少在阅读代码的前几次就这样做了。然而,正如其他人所指出的,时候一ZipCode类是合适的。

YAGNI(雅是不会需要它)是一个更好的模式来遵循,以避免烤宽面条代码(代码太多层) -抽象,比如类型和类是有帮助的程序员,并且不应该被使用,除非它们一种帮助。

我个人的目标是“保存代码行”(当然还有相关的“保存文件/模块/类”等)。我相信有些人会对我适用“原始痴迷”的表述-我发现拥有易于推理的代码比担心标签,样式和反样式更为重要。何时创建函数,模块/文件/类或将函数放置在公共位置的正确选择非常有根据。我的目标大致是3-100行函数,80-500行文件,以及“ 1、2,n”用于可重用的库代码(SLOC-不包括注释或样板;我通常希望强制性每行至少至少增加1个SLOC样板)。

大多数积极的模式来自于开发人员在需要它们时正是这样做的。学习如何编写可读代码比尝试应用没有解决相同问题的模式更为重要。任何优秀的开发人员都可以在很少见的情况下实现工厂模式,而在这种情况下最适合他们的问题的情况很少见。我已经使用了工厂模式,观察者模式,甚至还有数百个模式,却不知道它们的名称(即,是否有“可变分配模式”?)。为了进行有趣的实验-看看JS 语言中内置了多少个GoF模式-我在2009年大约12-15以后就停止了计数。例如,Factory模式非常简单,例如从JS构造函数返回一个对象-无需一个WidgetFactory。

所以- 是的有时候 ZipCode是一门好课。但是,,溜溜球问题并不严格相关。


0

溜溜球问题仅在您必须来回翻转时才有意义。这是由一两件事(有时两者)引起的:

  1. 命名错误。ZipCode看起来不错,ZoneImprovementPlanCode将要求大多数人(以及少数不会被打动的人)外观。
  2. 耦合不当。假设您的ZipCode类具有区号查询。您可能会认为这很有意义,因为它很方便,但是它与ZipCode并没有真正的联系,将它嵌入其中意味着现在人们不知道该去哪里找东西了。

如果您可以查看名称并对其名称有一个合理的了解,并且方法和属性可以做一些明显的事情,则无需查看代码,只需使用它即可。首先,这就是类的全部要点-它们是可以单独使用和开发的模块化代码段。如果您必须查看该类的API以外的其他内容以查看其功能,那么这充其量是部分失败的。


-1

记住,没有银弹。如果您要编写一个非常简单的应用程序,需要快速进行爬网,那么一个简单的字符串就可以完成任务。但是,在98%的时间内,Eric Evans在DDD中描述的值对象将是完美的选择。通过阅读,您可以轻松地看到Value对象提供的所有好处。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.