使用getter/setter和/或属性是不是一个坏主意?

I am perplexed by comments under this answer: https://softwareengineering.stackexchange.com/a/358851/212639

用户在那里反对使用getter/setter和属性。他坚持认为,大多数时候使用它们都是糟糕设计的标志。他的评论正在获得提升。

嗯,我是一个新手程序员,但说实话,我发现属性的概念非常吸引人。为什么数组不能通过写入 sizelength 属性来调整大小?为什么我们不应该通过读取 size 属性来获取二叉树中的所有元素?为什么汽车模型不应将其最高速度作为财产?如果我们正在制作一个RPG,为什么一个怪物不应该让它的攻击暴露为吸气剂?为什么不将它当前的HP暴露在吸气剂中呢?或者,如果我们正在制作家居设计软件,为什么我们不应该通过写入 color setter来重新定义墙?

这听起来很自然,对我来说很明显,我永远无法得出结论,使用属性或getter/setter可能是一个警示标志。

使用属性和/或getter setter通常是一个坏主意,如果是这样,为什么?

11
@JacquesB我想我明白了你的意思。你的意思是说吸尘器和制定者真的坏。疑难杂症。现在要告诉我的三个朋友。 ;)
额外 作者 Justin Poliey,
关键字是 意外 。如果属性(在某个属性上)是可变的这一事实令某些可能使用该对象的代码感到惊讶,请考虑将该属性设置为只读。破坏代码的事情是正确性和安全性问题。另一方面,信息隐藏仅仅是卫生和可维护性问题。
额外 作者 Ryan Cox,
无论是100%信任还是不信任你的直觉。像气味检查一样使用它们。因为当人们告诉你“它必须永远如此”时,人们告诉你的99%是完整而彻底的废话。但是(真正的)封装是有价值的。
额外 作者 Erik Reppen,
与许多编程主题一样,可以有一定程度的狂热,或者认为所有软件领域都相同的近视。这是一个这样的话题。
额外 作者 whatsisname,
@JimmyJames:gnat的链接“Getter和Setters Justified什么时候存在”给我节省了很多麻烦。
额外 作者 whatsisname,
@immibis Thorbjorn在评论之前提到了Java;第二个原因是getter/setter在该语言中很常见,与你的评论相关:如果前一个字段需要获得一些逻辑,则兼容性。 Python可以把它变成一个属性,所有现有代码都可以使用更新版本,但Java(至少是旧版本;现在可能不同)不能,所以使用getter/setter来对抗这样的改变。
额外 作者 Izkata,
额外 作者 gnat,
不要冒汗 - 一些开发人员听说“吸气剂/孵化器并不总是塑造互动的最佳方式”,并认为这意味着“吸气剂/孵化器总是坏的”,然后这种误解就会蔓延开来。这一切都归结为背景。
额外 作者 JacquesB,
对于Java,getter和setter的全部原因是因为接口只处理方法而不是字段。
额外 作者 Thorbjørn Ravn Andersen,
@whatsisname我很想知道哪些问题域表明打破了封装。
额外 作者 JimmyJames,
如果你想要的东西行为像一个字段但没有实现为一个属性(如数组长度),那么属性肯定是合理的。但是很多时候属性用于表现像字段的事情, 实现为字段,在这种情况下,你不需要属性,只需使用字段!
额外 作者 immibis,

5 答案

简单地公开字段 - 无论是公共字段,属性还是访问方法 - 都可以作为对象建模不足的指标。结果是我们询问对象以获取各种数据并对该数据做出决策。所以这些决定将在对象之外做出。如果这种情况反复发生,也许该决定应该是对象的责任,并且应该由该对象的方法提供:我们应该告诉对象要做什么,并且它应该弄清楚如何这样做。

当然,这并不总是合适的。如果你过度使用它,那么你的对象最终会得到许多小的不相关方法的集合,这些方法只使用一次。也许,最好将您的行为与您的数据分开,并选择更加程序化的方法。这有时被称为“贫血域模型”。然后你的对象带有很少的行为,但通过某种机制暴露它们的状态。

如果您公开任何类型的属性,那么在命名和使用的技术方面都应该是一致的。如何做到这一点主要取决于语言。要么全部公开为公共字段,要么全部通过属性公开,或者全部通过访问方法公开。例如。不要将 rectangle.x 字段与 rectange.y()方法或 user.name()方法与< code> user.getEmail() getter。

属性的一个问题,特别是可写属性或设置器的一个问题是,您可能会削弱对象的封装。封装非常重要,因为您可以单独推断对象的正确性。这种模块化使您可以更轻松地理解复杂系统。但是,如果我们访问对象的字段而不是发出高级命令,我们就会越来越多地耦合到该类。如果可以从对象外部向字段写入任意值,则很难保持对象的一致状态。对象的责任是保持自身一致,这意味着验证传入的数据。例如。数组对象的长度不能设置为负值。这种验证对于公共领域是不可能的,并且容易忘记自动生成的setter。对于像 array.resize()这样的高级操作,这一点更为明显。

建议的搜索条件以获取更多信息:

20
额外
暴露价值的另一个问题是它使得改变实施变得困难。如果你有一个getter int millisecondsSince1970()并确定它需要是 longString ,你可能需要更改许多代码中其他地方的许多地方。或者,如果 String getName()变为 FancyNameObject getName(); 现在,通常没有一个好的解决方法,但如果没有人“知道”你使用的是一个int内部,没有人会在他们的代码中假设。
额外 作者 MrG,

它本身并不是一种糟糕的设计,但就像任何强大的功能一样,它可以被滥用。

属性点(隐式getter和setter方法)是它们提供了强大的语法抽象,允许使用代码将它们逻辑地视为字段,同时保留对定义它们的对象的控制。

虽然这通常被认为是封装,但它通常更多地与A控制状态有关,B与调用者的语法便利性有关。

为了得到一些上下文,我们首先要考虑编程语言本身。 虽然许多语言都有“属性”,但术语和功能都有很大差异。

C#编程语言在语义和语法层面都有一个非常复杂的属性概念。它允许getter或setter是可选的,并且它允许它们具有不同的可见性级别。如果您希望公开细粒度API,这会产生很大的不同。

考虑一个程序集,它定义了一组本质上是 cyclic 图形的数据结构。这是一个简单的例子:

public sealed class Document
{
    public Document(IEnumerable words)
    {
        this.words = words.ToList();
        foreach (var word in this.words)
        {
            word.Document = this;
        }
    }

    private readonly IReadOnlyList words;
}

public sealed class Word
{
    public Document Document
    {
        get => this.document;
        internal set
        {
            this.document = value;
        }
    }

    private Document document;
}

Ideally, Word.Document would not be mutable at all, but I cannot express that in the language. I could have created an IDictionary that mapped Words to Documents but ultimately there will be mutability somewhere

这里的目的是创建一个循环图,这样任何给定单词的函数都可以通过 Word查询 Word ,其中包含 Document 。文档属性(getter)。我们还希望在创建 Document 之后防止消费者发生变异。 setter的唯一目的是建立链接。它只能写一次。这很难做到,因为我们必须首先创建 Word 实例。通过使用C#将不同级别的可访问性归因于相同属性的getter和setter,我们能够封装 Word.Document <�的可变性包含程序集中的/ code>属性。

从另一个程序集中使用这些类的代码会将该属性视为只读(因为只有 get 而不是 set )。

这个例子让我知道我最喜欢的属性。他们只能有一个吸气剂。无论您只是想返回一个计算值,还是只想公开读取但不能写入的能力,对于实现和<�实现,属性的使用实际上是直观且简单的/ em>消费代码。

考虑一下这个简单的例子

class Rectangle
{
   public Rectangle(double width, double height) => (Width, Height) = (width, height);

   public double Area { get => Width * Height; }

   public double Width { get; private set; }

   public double Height { get; private set; }
}

现在,正如我之前所说,有些语言对属性的概念有不同的概念。 JavaScript就是这样一个例子。可能性很复杂。对象可以通过多种方式定义属性,并且它们是可以控制可见性的非常不同的方式。

但是,在微不足道的情况下,就像在C#中一样,JavaScript允许定义属性,使它们只暴露一个getter。这对于控制突变非常有用。

考虑:

function createRectangle(width, height) {
   return {
     get width() {
       return width;
     },
     get height() {
       return height;
     }
   };
}

那么什么时候应该避免属性?通常,避免使用具有逻辑的 setter 。即使是简单的验证通常也可以在其

回到C#,编写诸如此类的代码通常很诱人

public sealed class Person
{
    public Address Address
    {
        get => this.address;
        set
        {
            if (value is null)
            {
                throw new ArgumentException($"{nameof(Address)} must have a value");
            }
            this.address = value;
        }
    }

    private Address address;
}

public sealed class Address { }

抛出的异常可能让消费者感到意外。毕竟, Person.Address 被声明为可变, null 是类型为 Address 的值的完全有效值。更糟糕的是,正在引发 ArgumentException 。消费者可能没有意识到他们正在调用一个函数,因此异常的类型会更加令人惊讶。可以说,一个不同的异常类型,例如 InvalidOperationException 会更合适。然而,这突显了制定者可能变得丑陋的地方。消费者正在以不同的方式思考问题。有时候这种设计很有用。

但是,最好使 Address 成为必需的构造函数参数,或者如果我们必须公开写访问权限并创建一个只有getter的属性,则创建一个专用的方法来设置它。

我将在这个答案中添加更多内容。

3
额外

反复地添加getter和setter(也就是说,没有想到,不要因为你链接的问题反复地做到这一点而混淆)是一个问题的迹象。首先,如果你有字段的getsthrough getter和setter,为什么不公开字段呢?例如:

class Foo
{
    private int _i;
    public int getData() { return _i; }
    public void setData(int i) { _i = i; };
}

您上面所做的只是使数据难以使用,并且使用代码难以阅读。你只是强迫用户做 foo.setData(foo.getData()+ 1),而不仅仅是 foo.i + = 1 。为了什么好处?据说你正在为封装和/或数据控制做getter和setter,但是如果你有一个直通的公共getter和setter你无论如何也不需要其中任何一个。有些人认为您可能希望稍后添加数据控件,但实际执行的频率是多少?如果你有公共的getter和setter,你已经在抽象层次上工作,那么你根本就没有这样做。如果你有机会,你可以在那时解决它。

公共吸气者和制定者是货物崇拜节目的标志,你只是遵循死记硬背规则,因为你相信他们使你的代码更好,但没有实际考虑他们是否这样做。如果您的代码库中充满了getter和setter,请考虑为什么您只是不使用POD,或许这只是处理域的数据的更好方法?

对原始问题的批评并不是说吸气剂或制定者是一个坏主意,而是只是为所有领域添加公共吸气剂和制定者作为一个原则问题实际上并没有做任何好事。

2
额外
@Alexander属性帮助,但他们也混淆了。如果我想让图书馆做某事我宁愿称之为直接函数。
额外 作者 Your Common Sense,
@Alexander我的观点是,在未来的实践中,属性根本不会改变,我认为设计最终未来不太可能出现的情况并不有用。然后,当它实际上确实出现变化时,对于大多数情况来说并不是什么大问题,我们的接口很少是面向公众的。不要为了它而应用设计,最好先看看手头的实际情况。
额外 作者 Your Common Sense,
@Dunk POD是直截了当的,没有任何惊喜。但我确实同意,如果您假设OOP,那么使用POD的理由很少,除非您使用POD在私有实现中使用。我认为你假设很多。
额外 作者 Your Common Sense,
但是你在这里假设第一次吸气剂和孵化器是正确的。在实践中,你真的永远不需要改变。我想不出一个我必须要做的例子。但老实说,图书馆不应该公开这些类型。与使用OOP和getter/setter相比,还有许多其他处理数据封装的方法。
额外 作者 Your Common Sense,
几乎所有这些答案都是针对Java的。其他语言支持属性直接和一个没有荒谬:foo.setData(foo.getData()+ 1)
额外 作者 miguel.de.icaza,
我可以想到使用属性而不是POD的六个原因,但除了一些神秘的效率需求之外,我们无法想到为什么使用POD会是更好的选择。也许这是一种语言的东西。
额外 作者 Switch,
我原则上同意:盲目的cargoculting并不是一个好的设计。但是如果代码是库的一部分,我不能再回去修改它。我需要在第一时间做到正确或进行向后不兼容的更改。但这与我确实拥有该选项的应用程序或内部库完全无关,即大多数代码。只读访问器方法也是仅提供数据的某些视图的好机会,例如,只暴露一个接口而不是一个具体的类,只暴露一个不可变的视图等。但这些只是访问者的指示,而不是一直使用它们的理由。
额外 作者 amon,
@SebastianZander这正是属性的重点。他们让你不必担心,让你有能力在将来改变它。赢/赢
额外 作者 Alexander,
@SebastianZander“为什么不公开字段”将您的公共接口与私有实现隔离开来。在具有属性的合理语言(ivars +自动生成的getter/setter)中,您可以像ivars一样轻松地创建属性。如果属性需要在将来更改,您可以提供该属性的新实现来替换原始的“传递”行为,从而破坏您的公共接口
额外 作者 Alexander,
@SebastianZander然后你就一直在写堕落的getter/setter。
额外 作者 Alexander,

或者,如果我们正在制作家居设计软件,为什么我们不应该通过写入其颜色设定器来重新填充墙?

所以假设我们有一个程序可以让你看到各种油漆和装饰组合的样子。因此,您创建一个墙类并为颜色创建一个setter。然后,当用户想要设置墙壁的颜色时,您必须遍历所有墙壁并在每个墙壁上设置颜色。

或者,您可以让墙壁从共同的源头拉出颜色。然后你更新那个属性,当需要重新绘制墙时,它们都会从该属性中拉出来。

差异似乎很小,但循环是嘈杂的样板。另外,为什么要保留一个值的X副本?这种方法不能扩展。这些都很简单,但是这些问题使得很难使用更先进的设计技术。

为什么汽车模型不应将其最高速度作为财产?

这可能很有用,但让我们举一个更有趣的例子:轮胎的摩擦系数。一个简单的模型在属性中具有该值。但这实际上取决于表面。假设我们需要模拟当汽车碰到一个大水坑时会发生什么,但只能在汽车的一侧。我们可以反复检查车轮的位置以及何时遇到不同的表面,更新它的摩擦系数。但我们也需要考虑正常的力量。所以我们可以在那个考虑到它的循环中添加一些代码。还有空气速度。和暂停的状态。

或者,你可以让车轮根据它能够从相关来源检索的信息来计算它的当前摩擦系数。逻辑是相同的,但它现在与车轮对齐。然后,我们可以轻松添加不同版本的车轮,以便考虑不同的材料。如果将所有逻辑保留在某个外部循环中,则需要一堆case或if语句来处理所有各种场景。

1
额外
可能我会尝试使用 Color 类,并且可能使 Pattern 成为 Color 的派生类 - 或者更可能的是,我会使纯色成为 Pattern 的特殊情况,只有一种颜色。无论如何,AFAIK,这是如何做到的:3D模型具有可以设置的纹理,但它们不会自己绘制,绘图是引擎的工作。
额外 作者 baron,
我的观点是,如果每个墙可以是不同的颜色,那么每个墙应该能够存储其当前的颜色。如果墙的 color 字段设置为null,则是,墙从公共源中拉出颜色。或者,如果用户已写入 color setter,则 color 字段不再为null,并且此新值将覆盖从公共源中提取的任何内容。该逻辑在 color getter中实现。至少,这是我如何实现这一点。也许这是解决这个问题的次优方法。
额外 作者 baron,
用户可能希望每个墙具有不同的颜色。
额外 作者 baron,
想知道一个用户在同一个问题上发布两个答案有什么意义,之前的问题是否有问题
额外 作者 gnat,
@gaazkam你可以随时移动球门柱。我认为很明显可以调整模型以满足要求。
额外 作者 JimmyJames,
@gnat因为它们是不同的接近角度。另一个已经太长了。如果这是一个问题,为什么系统支持此功能?
额外 作者 JimmyJames,
@gaazkam所以大概,你有一些例行程序可以查看每个墙,并根据它的颜色属性绘制它。如果您被要求使其支持模式,可以选择模式中的每种颜色,请考虑如何修改它。
额外 作者 JimmyJames,
@gaazkam我只做了一些3D绘图,但是如果你尝试做一些重要的事情,你只有具有属性的对象和一个绘制它们的大循环,那么在它变得难以理解之前你将不会做太多的事情。
额外 作者 JimmyJames,

要了解getter和setter的问题,我认为重新回过头来讨论抽象和接口的概念。我不是指编程关键字 interface ,例如Java,我的意思是更一般意义上的接口。例如,如果你有一个微波炉,它有一些允许你控制它的用户界面。界面设计是系统设计最基本的方面之一。它直接关系到您的系统将如何发展。

让我们从一个反面的例子开始吧。假设我想存储有关库存的信息。所以我创建了一个类: Inventory 。首先,我的要求说我需要知道库存中有多少不同的物品或某种类型,因此这些信息可以用在程序的其他部分。好吧,让我们的代码:

public class Inventory {
    public final String sku;
    public int numberOfItems;
}

太好了,现在我可以创建可以在程序中共享的Inventory对象。但很快我遇到了一个问题。当物品被出售或添加到库存时,我需要更新这些对象。可能会有很多不同的人同时购买或出售这些东西。当程序的两个部分尝试同时更新数字时,我们开始出错。例如,假设我们有50个物品和100个物品被带入仓库,但同时我们卖了25个。所以两个部分读取了值然后修改它并将其写回。如果销售代码设置了最后的值,我们现在在对象中有25个项目,而实际上我们有125个。

所以我们添加一个包装器并同步:

public class Inventory {
    public final String sku;
    private int numberOfItems;

    public int getNumberOfItems() {
        synchronized (this) {
            return numberOfItems;
        }
    }

    synchronized public void setNumberOfItems(int number) {
        synchronized (this) {
            this.numberOfItems = number;
        }
    }
}

大。现在两个线程不能同时读写。但我们还没有真正解决问题。这两个部分仍然可以从getter中读取(一次一个)然后写回错误的新值(一次一个)。我们还没有真正得到任何结果。

这是一个更好的方法:

public class Inventory {
    private final String sku;
    private int numberOfItems;

    public int getSku() {
      return sku;
    }

    public int getNumberOfItems() {
        synchronized (this) {
            return numberOfItems;
        }
    }

    public void addItems(int number) {
        synchronized (this) {
            this.numberOfItems += number;
        }
    }
}

现在我们有一些事实上解决了这个问题。它确保一次一个地进行库存变更。

这是一个非常简单的例子,并不是为了展示如何在2017年编写好的代码。这里的要点是,为了拥有一个健壮的设计,你需要关注你的对象/类如何将控件定义到其他部分申请。内部状态的定义与此无关。在上面的例子中,假设公司在增长,现在已经分发。我们的库存将存储在某个远程服务器上。甚至没有一个领域。为了保持getter/setter接口,您需要创建其他帮助程序类来管理bean并来回传输数据。这是程序性(例如COBOL)程序的工作方式。数据被移动并且存在各种操作该数据的程序。这些类型的设计非常脆弱且修改成本很高,因为您有许多不同的代码需要一致修改。每个部分都可以做破坏系统其他部分的事情。

请注意,在改进的设计中仍然存在吸气剂。吸气剂肯定比安装者少。虽然我个人不喜欢事物上的 get 前缀,但有时候最好符合期望。但是,你仍然可以遇到吸气剂问题。如果您使用像数据包这样的对象来传递到具有逻辑的代码部分,那么它基本上是同一个问题。

我希望有所帮助。欢迎评论。

0
额外