it-swarm.cn

Getter和Setters何时合理

Getter和setter经常被批评为不合适的OO。另一方面,我见过的大多数OO代码都有大量的getter和setter方法。

何时使用吸气剂和塞特剂?您是否尝试避免使用它们?他们是否普遍滥用?

如果您最喜欢的语言具有属性(我的语言),那么此类问题也被视为此问题的获取者和准备者。从OO方法论的角度来看,它们是同一回事。他们只是语法更好。

Getter/Setter批评的来源(部分摘自评论以使他们更清晰可见):

简单地说一下批评:Getter和Setter允许您从对象外部操纵对象的内部状态。这违反了封装。只有对象本身应该关心其内部状态。

以及示例程序版本的代码。

struct Fridge
{
    int cheese;
}

void go_shopping(Fridge fridge)
{
     fridge.cheese += 5;
}

Mutator版本的代码:

class Fridge
{
     int cheese;

     void set_cheese(int _cheese) { cheese = _cheese; }
     int get_cheese() { return cheese; }
 }

void go_shopping(Fridge fridge)
{
     fridge.set_cheese(fridge.get_cheese() + 5);        
}

没有提供适当的封装,getter和setter会使代码变得更加复杂。由于其他对象可以访问内部状态,因此添加这些getter和setter不会带来很多好处。

该问题先前已在堆栈溢出中讨论过:

179
Winston Ewert

拥有getter和setter方法本身并不会破坏封装。破坏封装的方法是自动为每个数据成员(每个field,在Java lingo)中)添加一个getter和setter,而无需考虑任何事情。比让所有数据成员公开更好,这只是一小步之遥。

封装的重点不是您不应该从对象外部知道或更改对象的状态,而应该具有合理的policy来执行此操作。

  • 一些数据成员可能完全在对象内部,并且既不应具有getter也不应该具有setter。

  • 一些数据成员应该是只读的,因此它们可能需要getter但不需要setter。

  • 某些数据成员可能需要保持彼此一致。在这种情况下,您将不会为每个设置程序提供设置程序,而是同时设置它们的单一方法,以便您可以检查值的一致性。

  • 某些数据成员可能仅需要以某种方式进行更改,例如以固定量递增或递减。在这种情况下,您将提供increment()和/或decrement()方法,而不是setter。

  • 还有一些实际上可能需要读写,并且同时具有getter和setter。

考虑一个_class Person_的示例。假设一个人有一个名字,一个社会保险号和一个年龄。假设我们不允许人们更改其姓名或社会保险号。但是,该人的年龄每年应增加1。在这种情况下,您将提供一个构造函数,该构造函数会将名称和SSN初始化为给定值,并将年龄初始化为0。您还将提供方法incrementAge(),这会增加年龄by 1.您还将为这三个提供吸气剂。在这种情况下,不需要二传手。

在此设计中,允许从类外部检查对象的状态,并允许从类外部更改对象的状态。但是,您不允许随意更改状态。有一项策略有效地声明了名称和SSN完全不能更改,并且年龄可以一次增加1年。

现在,假设一个人也有薪水。人们可以随意更换工作,这意味着他们的工资也将改变。为了模拟这种情况,我们只能提供setSalary()方法!在这种情况下,允许随意更改工资是一个完全合理的政策。

顺便说一句,在您的示例中,我将为Fridge类提供putCheese()takeCheese()方法,而不是get_cheese()set_cheese()。这样您将仍然具有封装。


_public class Fridge {
  private List objects;
  private Date warranty;

  /** How the warranty is stored internally is a detail. */
  public Fridge( Date warranty ) {
    // The Fridge can set its internal warranty, but it is not re-exposed.
    setWarranty( warranty );
  }

  /** Doesn't expose how the fridge knows it is empty. */
  public boolean isEmpty() {
    return getObjects().isEmpty();
  }

  /** When the fridge has no more room... */
  public boolean isFull() {
  }

  /** Answers whether the given object will fit. */
  public boolean canStore( Object o ) {
    boolean result = false;

    // Clients may not ask how much room remains in the fridge.
    if( o instanceof PhysicalObject ) {
      PhysicalObject po = (PhysicalObject)o;

      // How the fridge determines its remaining usable volume is a detail.
      // How a physical object determines whether it fits within a specified
      // volume is also a detail.
      result = po.isEnclosedBy( getUsableVolume() );
    }

     return result;
  }

  /** Doesn't expose how the fridge knows its warranty has expired. */
  public boolean isPastWarranty() {
    return getWarranty().before( new Date() );
  }

  /** Doesn't expose how objects are stored in the fridge. */
  public synchronized void store( Object o ) {
    validateExpiration( o );

    // Can the object fit?
    if( canStore( o ) ) {
      getObjects().add( o );
    }
    else {
      throw FridgeFullException( o );
    }
  }

  /** Doesn't expose how objects are removed from the fridge. */
  public synchronized void remove( Object o ) {
    if( !getObjects().contains( o ) ) {
      throw new ObjectNotFoundException( o );
    }

    getObjects().remove( o );

    validateExpiration( o );
  }

  /** Lazily initialized list, an implementation detail. */
  private synchronized List getObjects() {
    if( this.list == null ) { this.list = new List(); }
    return this.list;
  }

  /** How object expiration is determined is also a detail. */
  private void validateExpiration( Object o ) {
    // Objects can answer whether they have gone past a given
    // expiration date. How each object "knows" it has expired
    // is a detail. The Fridge might use a scanner and
    // items might have embedded RFID chips. It's a detail hidden
    // by proper encapsulation.
    if( o implements Expires && ((Expires)o).expiresBefore( today ) ) {
      throw new ExpiredObjectException( o );
    }
  }

  /** This creates a copy of the warranty for immutability purposes. */
  private void setWarranty( Date warranty ) {
    assert warranty != null;
    this.warranty = new Date( warranty.getTime() )
  }
}
_
163
Dima

在Java中,使用getter和setter的基本原因很简单:

  • 您只能在接口中指定methods,而不能指定字段。

因此,如果要允许字段通过接口传递,则将需要一个reader和writer方法。传统上将它们称为字段x的getX和setX。

42
user1249

来自 http://www.adam-bien.com/roller/abien/entry/encapsulation_violation_with_getters_and

JavaBean样式:

connection.setUser("dukie");
connection.setPwd("duke");
connection.initialize();

面向对象风格:

connection.connect("dukie","duke");

好吧,显然我更喜欢后一种方法。它不会泄漏实现细节,它更简单,更简洁,并且所有需要的信息都包含在方法调用中,因此更容易实现。我还更愿意在可能的情况下使用构造函数中的参数设置私有成员。

您的问题是,什么时候需要使用吸气剂/吹丝机?也许当需要更改模式时,或者您需要询问对象以获取某些信息时。

myObject.GetStatus();
myObject.SomeCapabilitySwitch = true;

考虑这一点时,当我第一次开始使用C#进行编码时,我以上述的Javabeans风格编写了许多代码。但是随着我在语言方面的经验的积累,我开始在构造函数中进行更多的成员设置,并使用看起来更像上面OO样式)的方法。

20
Robert Harvey

何时使用吸气剂和塞特剂?

当行为“ get”和“ set”实际上与模型中的行为匹配时,即实际上从不

其他所有用途只是作弊,因为业务域的行为尚不清楚。

编辑

这个答案可能是轻率的,所以让我扩展一下。上面的答案大部分是正确的,但专注于OO设计)的编程范例,以及一些错过大局的东西。以我的经验,这使人们认为,避免碰壁和塞特是某些OO编程语言(例如,人们认为用属性替换getter很重要)

实际上,这是设计过程的结果。您不必深入研究接口,封装和模式,而争论是否破坏了这些范式以及什么是好还是不好OO编程。唯一要说明的是,最终要解决的问题是,如果您的域中没有任何东西像这样通过将它们放进去而工作,那您就不会对域进行建模。

现实情况是,域空间中的任何系统极不可能有吸气剂和吸气剂。您不能走到负责工资单的男人或女人,而只说“将薪水设置为X”“给我薪水”。这种行为根本不存在

如果将其放入代码中,则不是在设计与域模型匹配的系统。是的,这破坏了接口和封装,但这不是重点。关键是要建模一些不存在的东西。

更重要的是,您可能错过了重要的步骤或过程,因为可能有一个原因,我不能仅仅走上工资单,并说将薪水设置为X。

当人们使用吸气剂和吸气剂时,他们倾向于将这一过程的规则推到错误的地方。这正在进一步远离您的域。以现实世界为例,这就像薪水,假设走进的随机人有权获得这些值,否则他不会要求它们。这不仅不是域的方式,实际上还在于域的方式。

14
Cormac Mulhall

通常,吸气剂和吸气剂是个坏主意。如果字段在逻辑上不是接口的一部分,并且您将其设为私有,那很好。如果从逻辑上讲它是接口的一部分,并且您将其公开,那就很好。但是,如果您将其设为私有,然后通过提供一个getter和setter将其转回并再次有效地公开,则您将回到开始的地方,只是您的代码现在变得更加冗长和混乱。

显然,也有例外。在Java中,您可能需要使用接口。标准库Java以后用即时计算替换存储的字段而不会破坏接口,但这是例外,getter和setter是需要特殊说明的反模式。

11
rwallace

是直接访问字段还是通过方法访问字段并不重要。

类不变式(有用的)很重要。为了保护它们,我们有时有时无法从外部进行更改。例如。如果我们将Square的宽度和高度分开,则更改其中之一会使它变成不是Square的东西。因此,我们需要方法changeSide如果它是Rectangle,则可以有setter/public字段。但是setter比会测试其大于零是否更好。

在具体的语言(例如Java)中,这就是我们需要这些方法(接口)的原因。方法的另一个原因是兼容性(源和二进制)。因此,添加它们然后考虑公共领域是否足够容易。

顺便说一句我喜欢对公共final字段使用简单的不可变值持有类。

3
user470365

您可能希望将内部结构更改为任何内容,同时保持接口相同。如果您的界面不变,则您编写的代码不会中断。您仍然可以根据需要更改内部结构。

2
George Silva

考虑一个封装宽度和高度的Size类。我可以通过使用构造函数来消除设置器,但是如何使用Size绘制矩形?宽度和高度不是该类的内部数据;它们是必须供Size使用者使用的共享数据。

对象由行为和状态-或属性组成。如果没有暴露状态,那么只有行为是公开的。

没有状态,您将如何排序对象的集合?您将如何搜索对象的特定实例?如果仅使用构造函数,那么当对象的属性列表很长时会发生什么?

未经验证,不得使用任何方法参数。因此,编写以下内容是懒惰的:

setMyField(int myField){
    this.myField = myField;
}

如果确实以这种方式编写,那么您至少已经准备好使用setter进行验证;它比公共领域要好-但仅此而已。但是至少您有一个一致的公共接口,您可以返回该接口并放入验证规则,而不会破坏客户的代码。

Getter,setter,property,mutator称呼您想要的东西,但它们是必需的。

0
Dale

我的方法是这样的-

当我期望以后再使用数据时,使用getter/setter是合理的。另外,如果发生更改,我经常将数据推送到getter/setter中。

如果是POD结构,则可以保留插槽。

从更抽象的角度讲,问题是“谁来管理数据”,这取决于项目。

0
Paul Nathan

如果getter和setter违反了封装和真正的OO,那么我就很麻烦了。

我总是觉得一个对象代表着您需要它做的一切。

我刚写完一个用Java生成迷宫的程序,并且有一个表示“迷宫方块”的类。我在此类中的数据表示坐标,墙和布尔值等。

我必须有某种方式来更改/操作/访问该数据!没有getter和setter的情况下我该怎么办? Java不使用属性,并且将我在该类本地的所有数据都设置为public。这绝对违反了封装和OO。

0
Bryan Harrington

如果使用getter和setter感到很复杂,那么问题可能出在语言上,而不是概念本身。

这是用Ruby编写的second示例中的代码:

class Fridge
  attr_accessor :cheese
end

def go_shopping fridge
  fridge.cheese += 5
end

注意,它看起来很像Java中的first示例吗?当将吸气剂和装塞器当作头等舱公民时,他们并不是一件繁琐的事,而增加的灵活性有时可以算是真正的好处-例如,我们可以决定在新冰箱上返回奶酪的默认值:

class Fridge
  attr_accessor :cheese

  def cheese
    @cheese || 0
  end
end

当然,会有很多不应该公开暴露的变量。不必要地公开内部变量会使您的代码更糟,但是您几乎不能将其归咎于getter和setter。

0
Tobias Cohen