英文:
Does embedding in golang violate law of demeter?
问题
这是Effective GO关于在golang中的嵌入的说法:
当我们嵌入一个类型时,该类型的方法成为外部类型的方法,但当调用这些方法时,方法的接收者是内部类型,而不是外部类型。
我有一个代码片段,其中Struct User
定义如下:
type User struct {
Name string
Password string
*sql.Tx
}
然后我调用u.Query("some query here")
等等。我之前这样做是为了避免像u.Transaction.Query("query")
这样的调用,这显然违反了Demeter法则。现在在阅读文档和effective go之后,我对第一种方法的优点表示怀疑。
我是否违反了Demeter法则?如果是的话,我该如何避免它?
英文:
This is what the Effective GO had to say about Embedding in golang
> When we embed a type, the methods of that type become methods of the outer type, but when they are invoked the receiver of the method is the inner type, not the outer one
I had a code snippet where I had Struct User
defined as follows
type User struct {
Name string
Password string
*sql.Tx
}
And then I call u.Query("some query here")
etc. I had done this specifically so that I could avoid calls like u.Transaction.Query("query")
, which clearly violates Law of Demeter. Now after reading the docs and effective go, I am doubtful about merits of the first approach as well.
Am I violating Law of Demeter? If yes the how can I avoid it ?
答案1
得分: 6
嵌入概念在某种程度上违反了Demeter法则,因为它“不隐藏”类型被嵌入的事实,如果该类型本身是“导出的”。请注意,嵌入未导出的类型不会违反LoD(您无法引用未导出的字段和方法)。
但是,这并不强制您以违反LoD的方式引用提升的字段或方法。嵌入本身只是一种技术,让您可以将常见的共享代码“外包”给其他类型;或者从另一个角度来看,当创建新类型时利用其他类型。您引用嵌入类型的提升字段或方法的方式可能会违反该法则。
正如您所说,如果将其称为u.Tx.Query()
,那就明显违反了Demeter法则:您正在使用User
嵌入*sql.Tx
的实现细节。
但是,如果像这样调用它:u.Query()
,那就没问题。这种形式不会暴露或利用*sql.Tx
被嵌入的事实。如果实现发生更改,并且*sql.Tx
不再被嵌入(例如,将其更改为“常规”字段或完全删除,并添加了一个User.Query()
方法),这种形式仍将继续工作。
如果不希望允许访问导出的嵌入类型的字段值,请将其设置为未导出的常规字段,并添加一个“显式”的User.Query()
方法,该方法可以委托给该字段,例如:
type User struct {
Name string
Password string
tx *sql.Tx // 常规字段,非嵌入;且未导出
}
func (u *User) Query(query string, args ...interface{}) (*sql.Rows, error) {
return u.tx.Query(query, args...)
}
进一步说明:
在示例中,如果使用u.Query()
,使用此方法的客户端不会受到User
内部更改的影响(无论u.Query()
表示提升的方法还是User
的方法,即User.Query()
)。
如果sql.Tx
发生更改,是的,u.Query()
可能不再有效。但是,不太可能发生不兼容的sql.Tx
更改。如果您是更改包的开发人员,并进行不兼容的更改,则您有责任更改依赖于您不兼容更改的其他代码。通过这样做(正确更新u.Query()
),调用u.Query()
的客户端不会受到影响,客户端仍然不需要知道底层发生了什么变化。
这正是LoD所保证的:如果您使用u.Query()
而不是u.Tx.Query()
,如果User
在内部发生更改,调用u.Query()
的客户端不需要知道或担心。LoD并不是一件坏事。您不应该放弃它。您可以选择遵循哪些原则,但您也应该思考,并不总是以任何代价始终遵循所选择原则的一切。
还有一件事需要澄清:LoD不涉及API不兼容的更改。它提供的是,如果遵循LoD,实体的内部更改不会对使用实体的“公共”界面的其他实体产生影响。如果sql.Tx
以一种激进的方式发生更改,Tx.Query()
将不再可用,这不是由LoD“覆盖”的。
英文:
The embedding concept somewhat violates Law of Demeter as it doesn't hide the fact that a type was embedded if the type itself is exported. Note that embedding an unexported type does not violate LoD (you can't refer to unexported fields and methods).
But this doesn't force you to refer to promoted fields or methods in a way that also violates LoD. Embedding itself is just a technique so that you can "outsource" common, shared code to other types; or from another point of view to make use of other types when creating new ones. The way you refer to the promoted fields or methods of the embedded types is what may violate the law.
As you said, if you call it as u.Tx.Query()
, that is a clear violation of Law of Demeter: you are using the implementation detail that User
embeds *sql.Tx
.
But if you call it like this: u.Query()
that is ok. This form does not expose or take advantage of the fact that *sql.Tx
is embedded. This form will continue to work if implementation changes and *sql.Tx
will not be embedded anymore (e.g. it is changed to be a "regular" field or removed completely, and a User.Query()
method is added).
If you don't want to allow access to the field value of the exported embedded type, make it an unexported regular field and add an "explicit" User.Query()
method which may delegate to the field, e.g.:
type User struct {
Name string
Password string
tx *sql.Tx // regular field, not embedded; and not exported
}
func (u *User) Query(query string, args ...interface{}) (*sql.Rows, error) {
return u.tx.Query(query, args...)
}
Further notes:
In the example, if u.Query()
is used, the clients using this are not affected if internals of User
are changed (it doesn't matter if u.Query()
denotes a promoted method or it denotes a method of User
, that is: User.Query()
).
If sql.Tx
changes, yes, u.Query()
might not be valid anymore. But an incompatible sql.Tx
is unlikely to happen. And if you're the developer of the changed package, and making incompatible changes, it is your responsibility to change other code that depends on your incompatible change. Doing so (properly updating u.Query()
) the client who calls u.Query()
will not be affected, the client still doesn't need to know something changed under the hood.
This is exactly what LoD guarantees: if you use u.Query()
instead of u.Tx.Query()
, if the User
changes internally, the client calling u.Query()
does not need to know or worry about that. LoD is not a bad thing. You should not drop it. You may choose what principles you follow, but you should also think and not follow everything a chosen principle dictates all the time at any costs.
One more thing to clear: LoD does not involve API incompatible changes. What it offers is if followed, internal changes of an entity will not have effect on other entities using the "public" face of the entity. If sql.Tx
is changed in a drastic way that Tx.Query()
will not be available anymore, that is not "covered" by LoD.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论