Java中哪种样式更好(实例变量与返回值)


32

当我需要在类中的某些方法中使用公共数据时,我常常会努力决定要使用这两种方法中的哪一种。有什么更好的选择?

在此选项中,我可以创建一个实例变量,以避免必须声明其他变量,并且避免定义方法参数,但是可能不清楚在哪里实例化/修改这些变量:

public class MyClass {
    private int var1;

    MyClass(){
        doSomething();
        doSomethingElse();
        doMoreStuff();
    }

    private void doSomething(){
        var1 = 2;
    }

    private void doSomethingElse(){
        int var2 = var1 + 1;
    }

    private void doMoreStuff(){
        int var3 = var1 - 1;
    }
}

还是只是实例化局部变量并将其作为参数传递?

public class MyClass {  
    MyClass(){
        int var1 = doSomething();
        doSomethingElse(var1);
        doMoreStuff(var1);
    }

    private int doSomething(){
        int var = 2;
        return var;
    }

    private void doSomethingElse(int var){
        int var2 = var + 1;
    }

    private void doMoreStuff(int var){
        int var3 = var - 1;
    }
}

如果答案是它们都是正确的,那么哪个更常被看到/使用?另外,如果您可以为每个选项提供其他优点/缺点,那将非常有价值。



1
我认为尚未有人指出,将中间结果放入实例变量会使并发更加困难,因为这些变量在线程之间可能会发生争用。
sdenham '16

Answers:


107

我很惊讶这还没有提到...

这取决于是否var1实际上是对象状态的一部分

您假设这两种方法都是正确的,并且只是样式问题。你错了。

这完全与如何正确建模有关。

类似地,private存在实例方法来改变对象的state。如果这不是您的方法所要执行的操作,那么应该这样做private static


7
@mucaho:transient与此无关,因为transient它与持久状态有关,就像在执行对象序列化之类的操作时保存的对象部分一样。例如,transient即使ArrayList的后备存储对ArrayList的状态至关重要,因为在序列化ArrayList时,您只想保存后备存储中保存实际ArrayList元素的部分,而不是保存可用空间最后保留用于进一步的元素添加。
user2357112支持Monica 2016年

4
添加答案-如果var1需要几种方法,但不是方法的一部分MyClass,可能是时候将var1这些方法放入另一个类中了,以供使用MyClass
迈克·帕特里奇

5
@CandiedOrange我们在这里谈论正确的建模。当我们说“对象状态的一部分”时,我们并不是在谈论代码中的文字部分。我们正在讨论一个概念上的状态的概念,什么应该是该对象的状态的概念模型正确。“有用的寿命”可能是最大的决定因素。
jpmc26 2016年

4
@CandiedOrange Next和Previous显然是公共方法。如果var1的值仅与一系列私有方法相关,则它显然不是对象持久状态的一部分,因此应作为参数传递。
Taemyr '16

2
@CandiedOrange经过两次评论,您确实阐明了您的意思。我是说您很容易就能在第一个答复中提出。而且,是的,我忘记了不仅仅是对象。我确实同意有时私有实例方法有目的。我只是想不起来。编辑:我只是意识到您实际上不是第一个回复我的人。我的错。我感到困惑的是,为什么一个答复是简短,单句,不答复,而下一个却能解释事情。
Fund Monica的诉讼

17

我不知道哪个更普遍,但我会一直这样做。它更清楚地传达了数据流和生存期,并且不会用仅在初始化期间具有相关生存期的字段来膨胀类的每个实例。我会说前者只是令人困惑,并且使代码审查变得更加困难,因为我不得不考虑任何方法都可能修改的可能性var1


7

您应该尽可能(且合理)减小变量的范围。不仅在方法上,而且在总体上。

对于您的问题,这意味着它取决于变量是否是对象状态的一部分。如果是,则可以在该范围(即整个对象)中使用它。在这种情况下,请选择第一个选项。如果否,请选择第二个选项,因为它降低了变量的可见性,从而降低了整体复杂性。


5

Java中哪种样式更好(实例变量与返回值)

还有另一种样式-使用上下文/状态。

public static class MyClass {
    // Hold my state from one call to the next.
    public static final class State {
        int var1;
    }

    MyClass() {
        State state = new State();
        doSomething(state);
        doSomethingElse(state);
        doMoreStuff(state);
    }

    private void doSomething(State state) {
        state.var1 = 2;
    }

    private void doSomethingElse(State state) {
        int var2 = state.var1 + 1;
    }

    private void doMoreStuff(State state) {
        int var3 = state.var1 - 1;
    }
}

这种方法有很多好处。状态对象可以独立于对象而变化,例如,为将来留出了很大的摆动空间。

这种模式在分布式/服务器系统中也很好用,在该系统中,必须在调用之间保留一些详细信息。您可以在state对象中存储用户详细信息,数据库连接等。


3

这是关于副作用。

询问国家是否var1属于这一问题的重点。确定是否var1必须坚持下去,它必须是一个实例。无论是否需要持久性,都可以使用两种方法来工作。

副作用方法

一些实例变量仅用于在调用之间进行私有方法之间的通信。可以将这种实例变量重构为不存在,但不一定必须如此。有时候,事情变得更加清晰。但这并非没有风险。

您正在让变量超出其范围,因为它在两个不同的私有范围中使用。不是因为您要放置它的范围中需要它。这可能会造成混淆。“全球人民是邪恶的!” 混乱的程度。这可以工作,但伸缩性不好。它仅适用于小型企业。没有大物件。没有长的继承链。不要造成溜溜球的效果。

功能方法

现在,即使var1必须坚持,也没有什么可以说的,您必须使用每个瞬态值在达到两次公共调用之间要保留的状态之前就需要使用的值。这意味着您仍然可以var1使用更多的功能方法来设置实例。

因此,无论是否处于状态的一部分,您仍然可以使用这两种方法。

在这些示例中,'var1'被如此封装,除了您的调试器知道其存在之外。我猜您是故意这样做的,因为您不想偏向我们。幸运的是我不在乎。

副作用的风险

也就是说,我知道您的问题来自哪里。我下惨的工作 “荷兰国际集团继承问题发生变异,在多种方法多层次的实例变量匆匆疯狂的试图跟随它。这就是风险。

这是使我转向功能更强的方法的痛苦。方法可以记录其依赖性并在其签名中输出。这是一种强大而清晰的方法。它还允许您更改传递私有方法的内容,从而使其在类中更可重用。

副作用的上升

这也是限制。纯函数没有副作用。那可能是一件好事,但它不是面向对象的。面向对象的很大一部分是能够引用方法外部的上下文。做到这一点而又不会泄漏全局变量,这是OOP的优势。我获得了全局变量的灵活性,但是它很好地包含在课程中。如果愿意,我可以调用一个方法并立即更改每个实例变量。如果这样做,我必须至少给该方法指定一个名称,该名称应清楚说明它的作用,以便人们在发生这种情况时不会感到惊讶。评论也可以提供帮助。有时,这些评论被形式化为“职位条件”。

功能性私有方法的缺点

功能方法明确了一些依赖关系。除非您使用纯功能语言,否则不能排除隐藏的依赖项。您不知道,仅查看方法签名,就不会在其余代码中对您隐藏副作用。你只是没有。

附加条件

如果您和团队中的其他每个人都在注释中可靠地记录了副作用(前/后条件),那么从功能方法中获得的收益就少得多。是的,我知道,梦见。

结论

就我个人而言,如果可以的话,我倾向于两种情况下的函数私有方法,但是老实说,这主要是因为这些前/后条件副作用注释在过时或方法被无序调用时不会导致编译器错误。除非我真的需要副作用的灵活性,否则我只想知道事情是可行的。


1
“某些实例变量仅用于在调用之间进行私有方法之间的通信。” 那是代码的味道。当以这种方式使用实例变量时,表明该类太大。将该变量和方法提取到新类中。
凯文·克莱恩

2
这真的没有任何意义。尽管OP 可以在功能范式中编写代码(这通常是一件好事),但这显然不是问题所在。试图告诉OP他们可以避免通过更改范例来避免存储对象的状态并没有真正的意义……
jpmc26 '16

@kevincline OP的示例只有一个实例变量和3个方法。从本质上讲,它已经被提取到一个新类中。并不是说该示例没有做任何有用的事情。类的大小与您的私有帮助器方法如何相互通信无关。
MagicWindow '16

@ jpmc26 OP已经表明它们可以避免var1通过更改范例将其存储为状态变量。类作用域不仅仅是状态存储的地方。它也是一个封闭范围。这意味着将变量放在类级别有两种可能的动机。您可以声称只在封闭范围内这样做,而不是说国家是邪恶的,但我说这是与善良的权衡,这也是邪恶的。我知道这一点是因为我必须维护执行此操作的代码。有些做得很好。有些是一场噩梦。两者之间的界线不是状态。它的可读性。
MagicWindow '16

状态规则增强了可读性:1)避免使操作的临时结果看起来像状态2)避免隐藏方法的依赖关系。如果方法的实用性很高,那么它要么无法还原且真实地代表该方法的复杂性,要​​么当前设计具有不必要的复杂性。
sdenham '16

1

第一个变体对我来说似乎不是直觉的,并且有潜在的危险(可以想象由于某种原因,有人将您的私有方法公开)。

我宁愿在类构造时实例化您的变量,也可以将它们作为参数传递。后者使您可以选择使用功能性惯用语,而不依赖于包含对象的状态。


0

已经有关于对象状态以及何时使用第二种方法的答案。我只想为第一种模式添加一个常见的用例。

当您的类所做的全部工作是封装一个算法时,第一种模式是完全有效。一个用例是,如果您将算法编写为单个方法,那将太大。因此,您将其分解为较小的方法,使其成为一个类,并将子方法设为私有。

现在,通过参数传递算法的所有状态可能会变得很乏味,因此您可以使用私有字段。它也与第一段中的规则一致,因为它基本上是实例的状态。如果您为此使用私有字段,则只需记住并正确记录该算法不会重入。在大多数情况下,这不应该成为问题,但可能会咬你。


0

让我们尝试一个执行某些操作的示例。原谅我,因为这不是Java语言,而是JavaScript。重点应该是相同的。

访问https://blockly-games.appspot.com/pond-duck?lang=zh-CN,点击javascript标签,然后粘贴以下内容:

hunt = lock(-90,1), 
heading = -135;

while(true) {
  hunt()  
  heading += 2
  swim(heading,30)
}

function lock(direction, width) {
  var dir = direction
  var wid = width
  var dis = 10000

  //hunt
  return function() {
    //randomize() //Calling this here makes the state of dir meaningless
    scanLock()
    adjustWid()
    if (isSpotted()) {
      if (inRange()) {
        if (wid <= 4) {
          cannon(dir, dis)
        }
      }
    } else {
      if (!left()) {
        right()
      }
    }
  }

  function scanLock() {
    dis = scan(dir, wid);
  }

  function adjustWid() {
    if (inRange()) {
      if (wid > 1)
        wid /= 2;
    } else {
      if (wid < 16) {
        wid *= 2; 
      }
    }
  }

  function isSpotted() {
    return dis < 1000;
  }

  function left() {
    dir += wid;
    scanLock();
    return isSpotted();
  }

  function right() {
    dir -= wid*2;
    scanLock();
    return isSpotted()
  }

  function inRange() {
    return dis < 70;
  }

  function randomize() {
    dir = Math.random() * 360
  }
}

你应该注意到dirwiddis没有得到周围很多过去了。您还应该注意,lock()返回函数中的代码与伪代码非常相似。好吧,这是实际的代码。但是,它很容易阅读。我们可以添加传递和分配,但这会增加您在伪代码中看不到的混乱。

如果您希望因为这三个变量是持久状态而认为不进行赋值和传递是可以的,那么请考虑重新设计,dir该算法在每个循环中分配一个随机值。现在不是持久的,是吗?

当然,现在我们可以dir缩小范围,但不能不通过传递和设置而使我们的伪类代码混乱。

所以不,状态不是为什么您决定使用或不使用副作用而不是传递和返回。副作用本身也不意味着您的代码不可读。您无法获得纯功能代码好处。但是做得好,有了好名字,它们实际上可以很好地阅读。

但这并不是说他们不能变成像噩梦般的意大利面。但是,那又不能呢?

快乐的鸭子狩猎。


1
内部/外部功能是不同于OP描述的范式。-我同意外部函数中传递给内部函数的参数的局部变量之间的权衡类似于类传递给私有函数的参数的类中的私有变量。但是,如果进行此比较,则对象的生存期将对应于函数的一次运行,因此外部函数中的变量是持久状态的一部分。
Taemyr '16

@Taemyr OP描述了在公共构造函数中调用的私有方法,对我而言,这很像inner的内部。如果您在每次输入功能时都踩到状态,则状态并不是真正的“持久”状态。有时状态被用作共享位置,方法可以读写。并不意味着它需要持久化。无论如何,持久化这一事实并不是要依靠的任何东西。
candied_orange

1
即使您每次处置该对象时都踩到它,它也是持久的。
Taemyr '16

1
好的要点,但是用JavaScript表达它们确实使讨论变得模糊。另外,如果剥离JS语法,最终将传递上下文对象(外部函数的关闭)
fdreger

1
@sdenham我在争辩说,类的属性和操作的瞬时中间结果之间存在差异。它们不相等。但是一个可以存储在另一个中。当然不是必须的。问题是是否应该这样做。是的,存在语义和生命周期问题。还有arity问题。您认为它们不够重要。没关系。但是要说,将类级别用于对象状态以外的任何东西都是不正确的建模,这是坚持一种建模方法。我维护着专业的代码,而这样做是相反的。
candied_orange
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.