我正在使用的代码库经常使用实例变量在各种简单方法之间共享数据。最初的开发者坚信这遵循了Bob / Robert Martin叔叔在Clean Code书中所述的最佳实践:“功能的第一个规则是它们应该很小。”和“函数的理想参数个数为
零(尼拉度)。(...)参数很难。它们具有很大的概念力。”

示例:

public class SomeBusinessProcess {
  @Inject private Router router;
  @Inject private ServiceClient serviceClient;
  @Inject private CryptoService cryptoService;

  private byte[] encodedData;
  private EncryptionInfo encryptionInfo;
  private EncryptedObject payloadOfResponse;
  private URI destinationURI;

  public EncryptedResponse process(EncryptedRequest encryptedRequest) {
    checkNotNull(encryptedRequest);

    getEncodedData(encryptedRequest);
    getEncryptionInfo();
    getDestinationURI();
    passRequestToServiceClient();

    return cryptoService.encryptResponse(payloadOfResponse);
  }

  private void getEncodedData(EncryptedRequest encryptedRequest) {
    encodedData = cryptoService.decryptRequest(encryptedRequest, byte[].class);
  }

  private void getEncryptionInfo() {
    encryptionInfo = cryptoService.getEncryptionInfoForDefaultClient();
  }

  private void getDestinationURI() {
    destinationURI = router.getDestination().getUri();
  }

  private void passRequestToServiceClient() {
    payloadOfResponse = serviceClient.handle(destinationURI, encodedData, encryptionInfo);
  }
}


我会使用局部变量将其重构为以下内容:

public class SomeBusinessProcess {
  @Inject private Router router;
  @Inject private ServiceClient serviceClient;
  @Inject private CryptoService cryptoService;

  public EncryptedResponse process(EncryptedRequest encryptedRequest) {
    checkNotNull(encryptedRequest);

    byte[] encodedData = cryptoService.decryptRequest(encryptedRequest, byte[].class);
    EncryptionInfo encryptionInfo = cryptoService.getEncryptionInfoForDefaultClient();
    URI destinationURI = router.getDestination().getUri();
    EncryptedObject payloadOfResponse = serviceClient.handle(destinationURI, encodedData,
      encryptionInfo);

    return cryptoService.encryptResponse(payloadOfResponse);
  }
}


这是更短的代码,它消除了各种琐碎方法之间的隐式数据耦合,将变量范围限制在所需的最小范围。尽管有这些好处,但我似乎仍然无法说服原始开发人员这种重构是必要的,因为它似乎与上述Bob叔叔的做法相矛盾。赞成局部变量胜于实例变量的客观科学依据?我似乎不太愿意把手指放在上面。我的直觉告诉我,隐藏的联轴器是不好的,狭窄的范围比广泛的范围要好。但是支持这一点的科学方法是什么?

相反,我可能忽略了这种重构的不利之处吗?

评论

评论不作进一步讨论;此对话已移至聊天。

#1 楼


偏爱局部变量而不是实例变量的客观科学依据是什么?

作用域不是二进制状态,而是梯度。您可以按从大到小的顺序对它们进行排序:据我所知,它们是同义词,但是我是C#开发人员,而不是Java开发人员。为了简洁起见,我将所有静态变量归入全局类别,因为静态变量不是问题的主题。
范围越小越好。这样做的理由是,变量应处于尽可能小的范围内。这样做有很多好处:

它使您可以考虑当前班级的责任并帮助您坚持SRP。
它使您不必避免全局命名冲突,例如如果两个或多个类具有Name属性,则不必强迫它们像FooNameBarName一样为它们加上前缀...这样就可以使变量名尽可能简洁明了。变量(例如,用于Intellisense)与上下文相关的变量。
它启用某种形式的访问控制,这样您的数据就不会被您不认识的某个参与者操纵(例如,同事开发的另一个类)。
使代码更易读,因为您确保这些变量的声明尽量保持与这些变量的实际用法尽可能接近。通常表示开发人员不太了解OOP或如何实现它。看到范围广泛的变量是一个危险信号,表明OOP方法可能存在问题(无论是一般开发人员还是特定代码库)。
(凯文评论)使用当地人会迫使您以正确的顺序做事。在原始的(类变量)代码中,您可能错误地将passRequestToServiceClient()移动到方法的顶部,并且仍然可以编译。对于本地人,只有传递了未初始化的变量,您才可能犯该错误,这很明显很明显,您实际上并没有这么做。要说服原始开发人员相信这种重构是有必要的,因为它似乎与上述Bob叔叔的做法相矛盾。
相反,我可能忽略了这种重构的不利之处吗?

这里的问题是您对局部变量的参数是有效的,但是您还进行了其他更改,这些更改不正确,并导致建议的修复程序无法通过气味测试。 ”的建议,这是有好处的,实际上您也删除了这些方法,这是完全不同的。这些方法应该保留下来,而应该更改它们以返回其值而不是将其存储在类变量中:
Global > Class > Local (method) > Local (code block, e.g. if, for, ...)

我确实同意您在process方法中所做的事情,但是您应该一直在调用私有子方法,而不是直接执行它们的主体。即使您当前不重用方法,也要在相关的地方创建子方法,即使只是为了提高代码的可读性,也是一个好习惯。
不管局部变量参数如何,我立即注意到您的建议该修复程序比原始修复程序的可读性差得多。我确实承认,肆意使用类变量也会降低代码的可读性,但是与将所有逻辑都堆叠在一个(现在很复杂)的方法中相比,乍一看并没有。

评论


评论不作进一步讨论;此对话已移至聊天。

– Maple_shaft♦
19年3月12日在17:13

#2 楼

原始代码使用诸如参数之类的成员变量。当他说要​​最小化参数的数量时,他真正的意思是要最小化方法运行所需的数据量。将数据放入成员变量并没有任何改善。

评论


完全同意!这些成员变量只是隐式函数参数。实际上,情况变得更糟,因为现在这些变量的值和函数用法之间没有明确的链接(来自外部POV)

–雷米
19 Mar 5 '19 at 20:08

我会说这不是本书的意思。有多少个功能需要零输入数据才能运行?我认为这是本书中的其中一部分。

– Qwertie
19 Mar 5 '19 at 22:46

@Qwertie如果您有一个对象,则它处理的数据可能会完全封装在其中。类似process.Start()的函数;或myString.ToLowerCase()似乎并不奇怪(并且确实是最容易理解的)。

–R。Schmitz
19年3月7日在10:54

两者都有一个论点:隐式this。甚至有人可能会争辩说这个论点是明确给出的-在点之前。

– BlackJack
19年3月7日在16:51

#3 楼

其他答案已经很好地解释了局部变量的好处,因此剩下的只是您的问题的一部分:

尽管有这些好处,我仍然似乎无法说服原始开发人员这种重构是有必要的,因为它似乎与上述鲍伯叔叔的作法相矛盾。

这应该很容易。只需将他指向Bob叔叔的Clean Code中的以下引用:

没有副作用
副作用是谎言。您的函数可以做一件事,但也可以做其他隐藏的事情。有时,它会对其类的变量进行意外更改。有时它将使它们成为传递给函数的参数或系统全局变量。无论哪种情况,它们都是曲折的和破坏性的误解,通常会导致奇怪的时间耦合和顺序依赖性。
(省略示例)
这种副作用会造成时间耦合。也就是说,只能在特定时间(换句话说,可以安全地初始化会话)调用checkPassword。如果调用顺序混乱,则会话数据可能会意外丢失。时间耦合令人困惑,尤其是当隐藏为副作用时。如果必须具有时间耦合,则应在函数名称中明确指出。在这种情况下,我们可能会重命名函数checkPasswordAndInitializeSession,尽管这肯定违反了“做一件事”。

,鲍勃叔叔不仅说一个函数应该带有很少的参数,他还说函数应尽可能避免与非本地状态进行交互。

评论


在“完美世界”中,这将是列出的第二个答案。同事倾听理性的理想情况的第一个答案-但是,如果同事是狂热者,那么这里的答案将处理情况,而不会产生太多混乱。

–R。Schmitz
19 Mar 7 '19 at 11:05

为了使这个想法更加务实,可以简单地推断出本地状态比实例或全局状态容易得多。定义明确且紧密包含的可变性和副作用很少导致问题。例如,许多排序函数都是通过副作用在原地运行的,但这很容易在本地范围内进行推理。

–贝斯特
19-3-7在20:08



啊,好老的“在矛盾的公理中,有可能证明任何事情”。由于没有硬道理和谎言是IRL,因此任何教条都必须包含陈述相反观点(即矛盾)的陈述。

– ivan_pozdeev
19 Mar 8 '19在15:44

#4 楼

“这与某人叔叔的想法相矛盾”从来都不是一个好论点。决不。不要从叔叔那里汲取智慧,请自己考虑。

也就是说,实例变量应该用于存储实际上需要永久或半永久存储的信息。这里的信息不是。没有实例变量,这很简单,因此可以使用它们。

测试:为每个实例变量编写文档注释。您能写出并非完全没有意义的东西吗?并向四个访问者写文档注释。他们同样毫无意义。

最糟糕的是,假定使用解密更改的方法,因为您使用了不同的cryptoService。不必更改四行代码,而必须用不同的变量替换四个实例变量,用不同的变量替换四个getter,并更改四行代码。

但是,如果您按代码行付费,则当然最好使用第一个版本。 31行而不是11行。如果要调试某些东西,则需要写三行,并且可以永久维护,以便在调试某些内容时进行读取,可以在需要更改时进行修改,如果您支持第二个cryptoService,则可以复制三行。

(缺少要点,即使用局部变量会强制您以正确的顺序进行调用)。

评论


为自己思考当然是件好事。但是,您的开篇段落实际上包括拒绝教师或前辈投入的学习者或下级;太过分了。

–更
19 Mar 5 '19 at 7:29

@Flater在考虑了老师或前辈的意见后,发现他们错了,唯一的选择就是拒绝他们的意见。最后,它不是关于解雇,而是关于质疑它,只有在确实证明是错误的时候才解雇它。

– glglgl
19 Mar 5 '19 at 8:56

@ChristianHackl:我全力以赴,不会盲目遵循传统主义,但我也不会盲目地拒绝它。答案似乎表明,为了自己的观点而避开已有的知识,这不是健康的做法。盲目听从别人的意见,不。当您不同意时质疑,显然是。完全驳回它,因为您不同意,不。当我读到它时,答案的第一段似乎至少暗示了后者。这很大程度上取决于对“为自己思考”的含义,这需要详细说明。

–更
19 Mar 5 '19在10:27



在共享智慧平台上,这似乎有点不合时宜...

–drjpizzle
19 Mar 5 '19在16:24

从来都不是一个好论点……我完全同意规则存在指导而不是规定。但是,关于规则的规则只是规则的子类,因此您要说自己的结论...

–cmaster-恢复莫妮卡
19 Mar 6 '19 at 22:36

#5 楼


与实例变量相比,偏爱局部变量的客观科学依据是什么?我似乎不太愿意把手指放在上面。我的直觉告诉我,隐藏的耦合不好,而狭窄的范围要比广泛的范围好。但是支持这种科学的科学方法是什么?比对象本身。似乎尚未涵盖的做出这种区分的一些原因围绕并发性和可重入性。如果方法通过设置实例变量的值来交换数据,则两个并发线程可以轻易地破坏彼此的实例变量值,从而产生间歇性的,难以发现的错误。遇到这些问题,因为依赖实例变量的数据交换模式很可能使方法不可重入。类似地,如果使用相同的变量在不同的方法对之间传递数据,则存在即使执行非递归方法调用链的单个线程也将陷入围绕所涉及实例变量的意外修改而引起的错误的风险。

为了在这种情况下可靠地获得正确的结果,您需要使用单独的变量在每对方法之间进行通信,其中每一个方法调用另一个方法,或者要考虑到每个方法的实现它直接或间接调用的所有其他方法的所有实现细节。这很脆,并且缩放性很差。

评论


到目前为止,这是唯一提及线程安全性和并发性的答案。考虑到问题中的特定代码示例,这真是太神奇了:SomeBusinessProcess的实例无法一次安全地处理多个加密请求。方法public EncryptedResponse进程(EncryptedRequest encryptionRequest)不同步,并发调用很可能破坏实例变量的值。这是提出的好点。

–约书亚·泰勒(Joshua Taylor)
19 Mar 5 '19在21:00

#6 楼

就业务逻辑而言,仅讨论process(...),您的同事的例子就清晰多了。相反,您的反例不仅仅需要粗略的浏览即可提取任何含义。

话虽如此,干净的代码既清晰又可读,而且质量很高-将局部状态推向更全球化的空间只是高层组装,因此质量为零。是的,编译器会生成它们,但重要的部分是它可以控制它们,从而使代码高效。同时也相对清晰易懂。

关于命名的一点。您需要有意义的最短名称,并在现有信息上进行扩展。即。 destinationURI,类型签名已经知道“ URI”。

评论


消除所有变量并不一定会使代码更易于阅读。

–法老王
19 Mar 6 '19 at 10:09

使用无点样式完全消除所有变量en.wikipedia.org/wiki/Tacit_programming

–马辛
19年3月6日在21:16

@Pharap是的,缺少变量不能确保可读性。在某些情况下,它甚至使调试更加困难。关键是精心挑选的名称(明确表达的用法)可以非常清晰地传达想法,同时仍然有效。

– Kain0_0
19年3月6日在23:05



#7 楼

我将完全删除这些变量和私有方法。这是我的重构:

public class SomeBusinessProcess {
  @Inject private Router router;
  @Inject private ServiceClient serviceClient;
  @Inject private CryptoService cryptoService;

  public EncryptedResponse process(EncryptedRequest encryptedRequest) {
    return cryptoService.encryptResponse(
        serviceClient.handle(router.getDestination().getUri(),
        cryptoService.decryptRequest(encryptedRequest, byte[].class),
        cryptoService.getEncryptionInfoForDefaultClient()));
  }
}


对于私有方法,例如router.getDestination().getUri()getDestinationURI()更清晰易读。如果在同一课程中两次使用同一行,我什至会重复一遍。换句话说,如果需要getDestinationURI(),则它可能属于其他类,而不属于SomeBusinessProcess类。

对于变量和属性,通常需要持有稍后将使用的值。如果该类没有用于属性的公共接口,则它们可能不应该是属性。最糟糕的类属性使用可能是通过副作用在私有方法之间传递值。

无论如何,该类只需要执行process(),然后该对象将被丢弃,不需要将任何状态保留在内存中。进一步的重构潜力是将CryptoService从该类中删除。

基于评论,我想添加此答案是基于现实世界的实践。确实,在代码审查中,我首先要挑选的是重构类并移出加密/解密工作。一旦完成,然后我会问是否需要方法和变量,它们是否正确命名等等。最终的代码可能更接近于此:

public class SomeBusinessProcess {
  @Inject private Router router;
  @Inject private ServiceClient serviceClient;

  public Response process(Request request) {
    return serviceClient.handle(router.getDestination().getUri());
  }
}


使用上面的代码,我认为它不需要进一步的重构。与规则一样,我认为需要经验来知道何时以及何时不应用它们。规则并不是在所有情况下都适用的理论。另一方面,代码审查对一段代码可以通过多长时间具有真正的影响。我的诀窍是减少代码并使其易于理解。变量名可以作为讨论的重点,如果我可以删除它,那么审阅者甚至都不需要考虑它。

评论


我的支持,尽管许多人会在这里犹豫。当然,有些抽象是有意义的。 (顺便说一句,叫做“过程”的方法是可怕的。)但是这里的逻辑是最小的。但是,OP的问题在于整个代码风格,而且情况可能更复杂。

–乔普·艾根(Joop Eggen)
19 Mar 5 '19 at 10:20

将其全部链接到一个方法调用中的一个明显问题是纯粹的可读性。如果您需要对一个给定的对象执行多个操作,那么它也不起作用。同样,这几乎是不可能调试的,因为您无法单步执行操作并检查对象。尽管这在技术层面上可行,但我不主张这样做,因为它极大地忽略了软件开发的非运行时方面。

–更
19 Mar 5 '19在10:30



@Flater我同意您的意见,我们不想将其应用于所有地方。我修改了答案,以阐明自己的实际立场。我想说明的是,实际上,我们仅在适当时才应用规则。在这种情况下,链式方法调用很好,如果需要调试,可以调用链式方法的测试。

– imel96
19 Mar 6 '19 at 2:43

@JoopEggen是的,抽象很有意义。在该示例中,私有方法始终不提供任何抽象,该类的用户甚至都不了解它们

– imel96
19 Mar 6 '19 at 2:47

@ imel96有趣的是,您可能是少数几个注意到ServiceClient和CryptoService之间的耦合的人之一,因此有必要将精力集中在将CS注入SC而不是SBP中,从而在更高的体系结构级别解决潜在的问题...这就是IMVHO这个故事的重点;在关注细节时跟踪大图太容易了。

–user88637
19 Mar 8 '19 at 0:15

#8 楼

Flater的答案涵盖了范围界定的问题,但我认为这里还有另一个问题。
前者执行实际的业务逻辑,而后者通过添加更简单和更可重用的接口来节省类型并可能增加安全性。

在这种情况下,数据访问功能似乎并没有保存输入内容,并且不会在任何地方重复使用(否则删除它们还会有其他问题)。因此,这些功能根本不应该存在。

通过仅在命名函数中保留业务逻辑,我们可以兼得两者(在Flater的答案和imel96的答案之间的某个地方):

public EncryptedResponse process(EncryptedRequest encryptedRequest) {

    byte[] requestData = decryptRequest(encryptedRequest);
    EncryptedObject responseData = handleRequest(router.getDestination().getUri(), requestData, cryptoService.getEncryptionInfoForDefaultClient());
    EncryptedResponse response = encryptResponse(responseData);

    return response;
}

// define: decryptRequest(), handleRequest(), encryptResponse()


#9 楼

首先也是最重要的一点:鲍勃叔叔有时似乎像一位传教士,但指出他的规则有例外。有几个相互冲突的规则。我个人认为4也可以。

使用实例变量时,它们应该构成一个连贯的类。这意味着,即使不是所有非静态方法,也应在许多方法中使用变量。 >我既不认为原始版本也不认为重构版本是最佳版本,@ Flater已经很好地说明了使用返回值可以做什么。它提高了可读性并减少了使用返回值的错误。

#10 楼

局部变量缩小了范围,因此限制了变量的使用方式,从而有助于防止某些类型的错误,并提高了可读性。

实例变量减少了函数的调用方式,帮助减少某些类型的错误,并提高可读性。 />,
TL; DR:我认为您闻到过多热情的原因是,热情过高。

#11 楼

尽管以get ...开头的方法不应返回void,但在第一个解决方案中给出了方法内抽象级别的分隔。尽管第二个解决方案的范围更广,但仍然很难对方法中发生的事情进行推理。这里不需要分配局部变量。我将保留方法名称,并将代码重构为类似以下内容:

public class SomeBusinessProcess {
  @Inject private Router router;
  @Inject private ServiceClient serviceClient;
  @Inject private CryptoService cryptoService;

  public EncryptedResponse process(EncryptedRequest encryptedRequest) {
    checkNotNull(encryptedRequest);

    return getEncryptedResponse(
            passRequestToServiceClient(getDestinationURI(), getEncodedData(encryptedRequest) getEncryptionInfo())
        );
  }

  private EncryptedResponse getEncryptedResponse(EncryptedObject encryptedObject) {
    return cryptoService.encryptResponse(encryptedObject);
  }

  private byte[] getEncodedData(EncryptedRequest encryptedRequest) {
    return cryptoService.decryptRequest(encryptedRequest, byte[].class);
  }

  private EncryptionInfo getEncryptionInfo() {
    return cryptoService.getEncryptionInfoForDefaultClient();
  }

  private URI getDestinationURI() {
    return router.getDestination().getUri();
  }

  private EncryptedObject passRequestToServiceClient(URI destinationURI, byte[] encodedData, EncryptionInfo encryptionInfo) {
    return serviceClient.handle(destinationURI, encodedData, encryptionInfo);
  }
}


#12 楼

两者都一样,并且性能差异不明显,因此我认为没有科学依据。然后归结为主观偏好。

我也倾向于比您的同事更喜欢您的方式。为什么?因为尽管有些书作者说了我的看法,但我认为它更易于阅读和理解。要阅读该代码,您需要在几个函数和成员变量之间来回切换。并非所有事物都凝聚在一个地方,您需要记住所有这些内容才能理解它。相比之下,这是一个更大的认知负担。

,您的方法将所有内容打包得更加密集,但并没有使它难以理解。您只需要逐行阅读它,就不需要记住那么多东西就可以理解它。可能正好相反。

评论


事实证明,这确实是我理解流程的主要障碍。这个示例很小,但是另一个示例使用35(!)实例变量作为中间结果,而不计算包含依赖项的十几个实例变量。很难遵循,因为您必须跟踪早先已设置的内容。有些甚至在以后被重用,这使它变得更加困难。幸运的是,这里提出的论点终于说服了我的同事同意重构。

–亚历山大
19 Mar 14 '15:52