如果一个类属性创建并返回一个新的类实例,它是一种反模式吗?


24

我有一个叫做Heading一些事情的类,但它也应该能够返回当前标题值的反面,最后必须通过创建Heading类本身的新实例来使用它。

我可以使用一个简单的属性reciprocal来返回当前值的相反标题,然后手动创建Heading类的新实例,或者我可以创建一种方法createReciprocalHeading()来自动创建Heading类的新实例并将其返回给用户。

但是,我的一位同事建议我只创建一个名为的类属性reciprocal,该属性通过其getter方法返回该类本身的新实例。

我的问题是:类属性的行为不是一种反模式吗?

我特别觉得这不太直观,因为:

  1. 在我看来,类的属性不应返回类的新实例,并且
  2. 如果reciprocal没有从IDE获得帮助或检查getter签名,该属性的名称为,将无法帮助开发人员完全理解其行为。

我是否对类属性应该做的事情过于严格?还是真正的顾虑?我一直试图通过类的字段和属性来管理类的状态,并通过其方法来管理类的行为,而我却看不出它如何适合于类属性的定义。


6
正如@Ewan在回答的最后一段中所说,与其说是一种反模式,Heading不如说是一成不变的类型并reciprocal返回一个新值,这Heading是“成功的秘诀”。(需要注意的两个调用reciprocal应该返回“同一件事”,即它们应该通过相等性测试。)
David Arno

20
“我的课不是一成不变的”。那里是您真正的反模式。;)
David Arno

3
@DavidArno好吧,有时候使某些东西不可变会带来巨大的性能损失,尤其是如果该语言不支持它的本机,但是我同意在很多情况下不可变是一种好的做法。
53777A

2
@dcorking该类在C#和TypeScript中都实现了,但是我宁愿保持这种语言无关性。
53777A

2
@Kaiserludi听起来像是一个答案(一个不错的答案)-注释要求澄清
dcorking

Answers:


22

拥有诸如Copy()或Clone()之类的东西并不陌生,但是是的,我认为您担心这一点是正确的。

例如:

h2 = h1.reciprocal
h2.Name = "hello world"
h1.reciprocal.Name = ?

每次都警告该属性是一个新对象会很好。

您可能还假设:

h2.reciprocal == h1

然而。如果标题类是不可变的值类型,那么您将能够实现这些关系,并且倒数可能是该操作的好名字


1
只需注意Copy()Clone()是方法,而不是属性。我相信OP指的是返回新实例的属性获取器。
林恩

6
我知道。但是属性也是(某种)c#中的方法
Ewan

4
在这种情况下,C#有更多提供:String.Substring()Array.Reverse()Int32.GetHashCode(),等
林恩

If your heading class was an immutable value type-我认为您应该添加不可变对象的属性设置器应始终返回新实例(或者至少在新值与旧值不同的情况下),因为这是不可变对象的定义。:)
伊万·科尔米切克

17

类的接口使类的用户对它的工作方式进行假设。

如果这些假设中的许多是正确的,而很少是错误的,则界面是好的。

如果这些假设中有许多是错误的,很少是正确的,则说明界面是垃圾。

关于属性的一个常见假设是,调用get函数很便宜。关于属性的另一个常见假设是,连续两次调用get函数将返回相同的结果。


您可以通过使用一致性来解决此问题,以便更改期望值。例如,对于需要VectorRay Matrix等等的小型3D库,您可以使其像get Vector.normal和as这样的获取Matrix.inverse器几乎总是很昂贵。不幸的是,即使你的接口统一使用昂贵的房产,该接口将充其量只能作为有良知的作为一个使用Vector.create_normal()Matrix.create_inverse()-但我知道可以做的是使用属性创建一个更直观的界面,即使是不断变化的期望后,没有有力的论据的。


10

属性应该很快返回,通过重复调用返回相同的值,并且获取它们的值应该没有副作用。您在这里介绍我应该不会被实现为一个属性。

您的直觉大体上是正确的。重复调用时,工厂方法不会返回相同的值。每次返回一个新实例。这几乎使它失去了作为财产的资格。如果以后的开发增加了实例创建的权重,例如网络依赖关系,那也不是一件容易的事。

属性通常应该是获取/设置类当前状态一部分的非常简单的操作。类似工厂的操作不符合此条件。


1
DateTime.Now属性是常用的,它指向一个新对象。我认为属性的名称可以帮助消除关于每次返回相同对象的不确定性。一切都在名称中以及如何使用。
Greg Burghardt

3
DateTime.Now被视为错误。参见stackoverflow.com/questions/5437972/…–
布拉德·托马斯

@GregBurghardt新对象的副作用本身可能不是问题,因为它DateTime是一种值类型,并且没有对象标识的概念。如果我理解正确,那么问题在于它是不确定性的,每次都会返回一个新值。
Zev Spitz

1
@GregBurghardt DateTime.New是静态的,因此不能期望它表示对象的状态。
Tsahi Asher

5

我认为对此没有语言不可知的答案,因为构成“财产”的是一个特定语言的问题,而“财产”的调用者所期望的也是一个特定于语言的问题。我确实认为思考这一问题的最富有成果的方法是从呼叫者的角度考虑它的外观。

在C#中,属性的区别在于(通常)使用大写字母(如方法),但没有括号(如公共实例变量)。如果您看到以下代码,没有文档,您期望得到什么?

var reciprocalHeading = myHeading.Reciprocal;

作为C#的相对新手,但我读过Microsoft的《财产使用指南》,因此,我希望Reciprocal能:

  1. 成为Heading该类的逻辑数据成员
  2. 通话费用低廉,因此我无需缓存价值
  3. 缺乏明显的副作用
  4. 如果连续调用两次,将产生相同的结果
  5. (也许)提供一个ReciprocalChanged活动

在这些假设中,(3)和(4)可能是正确的(假设Heading是一个不变的值类型,如Ewan的回答所示),(1)可争论,(2)未知但也可争论,(5)具有语义上的意义(尽管具有标题的任何事物都应该具有HeadingChanged事件)。这向我表明,在C#API中,“获取或计算倒数”不应被实现为属性,但是特别是如果计算便宜且不Heading可变,这是一个临界情况。

(但是请注意,所有这些问题都与调用属性是否创建新实例无关,甚至与(2)也没有关系。在CLR中创建对象本身并不昂贵。)

在Java中,属性是方法命名约定。如果我看到

Heading reciprocalHeading = myHeading.getReciprocal();

我的期望与上面的期望相似(如果未明确列出):我希望此调用便宜,等幂且没有副作用。但是,在JavaBeans框架之外,“属性”的概念在Java中并不是那么有意义,尤其是当考虑不带对应属性的不可变属性时setReciprocal()getXXX()约定现在有点过时了。从有效Java的第二版(至今已有八年历史了):

返回boolean调用对象的非功能或属性的方法通常以名词,名词短语或以动词get… 开头的动词短语来命名。有一个声音特遣队声称只有第三种形式(以开头get)是可以接受的,但是这种主张没有任何依据。前两种形式通常会导致代码更具可读性……(第239页)

那么,我希望在一个当代的,更流畅的API中

Heading reciprocalHeading = myHeading.reciprocal();

-这再次表明该调用便宜,等幂且没有副作用,但是对于执行新的计算还是创建新的对象一无所知。这可以; 在一个好的API中,我不在乎。

在Ruby中,没有属性。有“属性”,但如果我看到

reciprocalHeading = my_heading.reciprocal

我无法立即知道我是@reciprocal通过attr_reader还是简单的访问器方法来访问实例变量,或者是否正在调用执行昂贵计算的方法。方法名称是一个简单名词的事实calcReciprocal再次表明,该调用至少便宜,并且可能没有副作用,这一事实而不是说出。

在Scala中,命名约定是具有副作用的方法带有括号,而没有副作用的方法则不带括号,但是

val reciprocal = heading.reciprocal

可以是以下任何一种:

// immutable public value initialized at creation time
val reciprocal: Heading = … 

// immutable public value initialized on first use
lazy val reciprocal: Heading = … 

// public method, probably recalculating on each invocation
def reciprocal: Heading = …

// as above, with parentheses that, by convention, the caller
// should only omit if they know the method has no side effects
def reciprocal(): Heading = …

(请注意,Scala 允许样式指南不鼓励的各种操作。这是我对Scala的主要厌烦之一。)

没有括号告诉我这个电话没有副作用。该名称再次表明该呼叫应该相对便宜。除此之外,我不在乎它如何为我带来价值。

简而言之:了解您使用的语言,并了解其他程序员会对您的API带来什么期望。其他所有内容都是实现细节。


1
+1了我最喜欢的答案之一,感谢您抽出宝贵的时间编写本文。
53777A

2

正如其他人所说,返回相同类的实例是一种相当普遍的模式。

命名应与语言的命名约定紧密结合。

例如,在Java中,我可能希望它被称为 getReciprocal();

话虽这么说,我会考虑可变对象与不可变对象。

使用不可变的对象,事情非常容易,并且无论是否返回同一对象都不会受到伤害。

如果是可变的,这会变得非常可怕。

b = a.reciprocal
a += 1
b = what?

现在b指的是什么?原始值的倒数a?还是改变后的对立?这可能只是一个示例,但您已经明白了。

在那些情况下,寻找一个更好的命名来传达发生的事情,例如createReciprocal()可能是一个更好的选择。

但这实际上也取决于上下文。


你的意思是可变的吗?
卡斯顿S

@CarstenS是的,当然,谢谢。不知道我的头在哪里。:)
Eiko

在上存在一些异议getReciprocal(),因为这种“声音”就像“正常”的JavaBeans风格的吸气剂。更喜欢“ createXXX()”或“ calculateXXX()”,它可以向其他程序员表明或暗示发生了一些不同的事情
user949300 2016年

@ user949300我在某种程度上同意您的担忧-并进一步提到了create ...名称。不过也有缺点。create ...暗示可能并非总是如此的新实例(对于某些特殊情况,可能想到单个专用对象),计算...某种泄漏的实现细节-这可能会导致对价格标签的错误假设。
Eiko

1

为了单一负责和清晰起见,我将有一个ReverseHeadingFactory来接收对象并返回其反向对象。这将使返回的对象更加清楚,这意味着将产生相反结果的代码与其他代码进行了封装。


1
+1表示清楚,但其他解决方案中的SRP有什么问题?
53777A

3
为什么在工厂方法上推荐工厂类?(我认为,对象的有用职责通常是发出与自身类似的对象,例如iterator.next。这种对SRP的轻度违反会导致其他软件工程问题吗?)
dcorking

2
@dcorking工厂类与方法(在我看来)通常与“对象是可比较的,还是我应该做一个比较器?”相同类型的问题。做一个比较器总是可以的,但是如果比较它们是一种相当普遍的方式,那么就可以使它们具有可比性。(例如,较大的数字大于较小的数字,这对于可比性是有益的,但是您可以说所有偶数都大于所有几率,我可以将其作为比较器)–因此,我认为只有一个反向标题的方法,因此我倾向于制作一种避免多余的类的方法。
曼队长

0

这取决于。我通常希望属性返回实例的一部分,因此返回同一类的不同实例会有些不同。

但是例如,字符串类可以具有属性“ firstWord”,“ lastWord”,或者如果它处理Unicode“ firstLetter”,“ lastLetter”,则它们将是完整的字符串对象-通常只是较小的对象。


0

由于其他答案涵盖了“属性”部分,因此我将仅谈谈您的#2 reciprocal

请勿使用reciprocal。这是您所描述的唯一正确术语(并且是正式术语)。不要编写错误的软件来使开发人员免于学习他们正在使用的领域的知识。在导航中,术语具有非常具体的含义,并使用看起来像是无害的东西,reverse或者opposite在某些情况下可能导致混淆。


0

从逻辑上讲,不,它不是标题的属性。如果是这样,您也可以说数字的负数是该数字的财产,并且坦率地说,这会使“财产”失去所有含义,几乎所有东西都是财产。

倒数是标题的纯函数,与数字的负数相同是该数字的纯函数。

根据经验,如果设置没有意义,则可能不应该将其作为属性。当然,仍然不允许设置它,但是在理论上添加setter应该是可能的并且有意义。

现在,在某些语言中,如果由于某种语言的机制而带来一些优势,那么无论如何还是将其作为在该语言的上下文中指定的属性可能是有意义的。例如,如果您要将其存储到数据库中,并且允许通过ORM框架自动处理,则将其设置为属性可能是明智的。也许这是一种在属性和无参数成员函数之间没有区别的语言(我不知道这种语言,但是我敢肯定有一些语言)。然后由API设计人员和文档编制人员来区分功能和属性。


编辑:具体来说,对于C#,我将研究现有的类,尤其是那些标准库。

  • BigIntegerComplex结构。但是它们是结构,并且仅具有静态函数,并且所有属性都是不同的类型。因此,对于您的Heading课程,没有太多的设计帮助。
  • Math.NET Numerics具有Vectorclass该类具有很少的属性,并且似乎非常接近您的Heading。如果您这样做,那么您将拥有倒数功能,而不是财产。

您可能需要查看代码实际使用的库或现有的类似类。如果一种方法不是很正确,另一种是错误的,请尝试保持一致,这通常是最好的。


-1

确实,这是一个非常有趣的问题。在您提出的建议中,我没有发现SOLID + DRY + KISS违规行为,但仍然闻起来很糟糕。

返回类实例的方法称为构造函数,对吗?因此您要使用非构造方法创建新实例。这不是世界末日,但作为客户,我不希望那样做。这通常是在特定情况下完成的:工厂或有时是同一类的静态方法(对单例和非面向对象的程序员有用吗?)

加:如果您getReciprocal()对返回的对象进行调用会发生什么?您获得了该类的另一个实例,该实例很可能是第一个实例的精确副本!如果您调用getReciprocal()那个?对象蔓延到任何人?

还是那句话:如果在调用需要的倒数(标量,我的意思)?getReciprocal()->getValue()?我们违反了得墨meter耳定律,却无济于事


我很乐意接受下降投票者的反对意见,谢谢!
Gianluigi Zane Zanettini博士

1
我没有拒绝投票,但我怀疑原因可能是它并未真正为其余答案增加太多,但我可能是错的。
53777A

2
SOLID和KISS通常彼此相反。
whatsisname
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.