<p>A colleague and I have recently been refactoring a bunch of the internal libraries that are used across our Go-based backend services. As part of this refactoring I've been trying to move towards a more idiomatic use of interfaces across the board. Historically, we've defined interfaces right next to their concrete counterparts, but this of course does come with some drawbacks, not limited to making it harder to name either the interface or concretion specifically. In some cases we're also trying to move away from using interfaces full-stop, because we've found some of them to be unnecessary (e.g. if it's an API client, we can just use a the httptest server instead and swap the base URL for the "API").</p>
<p>Now we're aiming to move the definition of interfaces to where these concretions will actually be used as dependencies we have the joy of naming these new types, but that's where we're struggling a little. We both come from a similar background of a mix of PHP and Java, with little bits of other things thrown in, so that probably makes it clear how we ended up in the initial situation, and may also shed some light on why we're struggling a little now to break old habits.</p>
<p>We both know that we want to name these interfaces something that is actually useful, but quite generic. The problem is when you have types that do several things; for example, some type that performs crud operations with a database, so you have methods like <code>FindXyz</code>, <code>PersistAbc</code>, <code>DeleteMno</code>, so on.</p>
<p>In those cases, would it be idiomatic to go with interface names like (let's pretend we're talking about users, just to make it a little more realistic, because it's easy to see how you can have multiple similar but slightly different methods in that case):</p>
<pre><code>type UserFinder interface {
FindAll() ([]User, error)
FindByID(id int) (User, error)
FindByUsername(username string) (User, error)
}
type UserPersister interface {
Persist(user User) error
}
type UserDeleter interface {
Delete(user User) error
DeleteByID(id int) error
DeleteByUsername(username string) error
}
</code></pre>
<p>Now, given what I've said above, firstly, that shouldn't ever all be defined like that... should it, unless we plan on actually providing multiple implementations of these interfaces out of the box?</p>
<p>Would an appropriate compound interface just be a combination of those things?</p>
<pre><code>type UserFindPersistDeleter interface {
UserFinder
UserPersister
UserDeleter
}
</code></pre>
<p>... or any combination of the above in a similar fashion?</p>
<p>All of this would then match a concrete type like that defines all of those methods, that maybe actually interacts with a database. In that case, it doesn't really tell you much about what the concretion is doing, which is good in my opinion (and my colleagues), but then you do end up with a concretion that is named something very different, e.g. <code>type UserRepository struct</code> or <code>type DBUserGateway struct</code>, or even <code>type MySQLUserGateway struct</code>, so on.</p>
<p>I've found it a little difficult to find some solid, concrete examples of HTTP based services, written in Go that use technologies like MySQL, Redis, and various APIs via API clients. If anyone has any that they could point me at I'd really appreciate it. For now I've been looking around Moby, Etcd, Upspin, and Kubernetes for inspiration for a few things.</p>
<hr/>**评论:**<br/><br/>jerf: <pre><p>I'm interpolating a bit here, but you make an offhand comment about how your interfaces are defined next to the types that makes me wonder if you've considered whether or not one of your issues is that you're trying to define interfaces in modules that don't actually <em>care</em> about the interfaces. It is possible the thing you are struggling with is that you are getting no guidance from the problem as a result, and are thus having a hard time figuring out what to do with the interfaces.</p>
<p>For something like a "user" object, I wouldn't be surprised to encounter a module that defines a User that has no interfaces at all. The module itself probably doesn't need any interfaces, because it has the concrete User object, and what else would it need? The test code might use interfaces, or the User module might have private interfaces for its own internal use, but I wouldn't necessarily expect there to be any public ones.</p>
<p>Instead, if there is a function in some other module (perhaps a separate module for the HTTP API) that is going to create a user, I would expect <em>that</em> module to define <code>type UserDeleter interface { ... }</code>, and then it will be obvious what to put in the interface, which is exactly the methods it needs. The utility of the interface is right in the module where it matters, and that module can define a trivial "UserDeleter" implementer to, say, test whether or not the deletion business logic works as expected.</p>
<p>I've even got a couple of places where two modules define the exact same interface, but they don't share it because either A: I don't want those modules to be dependent on each other or B: I <em>can't</em> have them dependent due to it causing a circular import loop. There's not much wrong with that; the compiler will pick up any errors you may make. That said, if numerous models end up defining the same interface, by all means pull it up to the core User module. Now instead of trying to guess, the rest of the code told you what it needs, which is way easier.</p>
<p>This is one of those things that is hard to describe in generic <a href="/r/programmer" rel="nofollow">/r/programmer</a> or Hacker News debates about Go that makes it not just a language capable of doing work, but occasionally, a language that is more fun than many other languages. Structural satisfaction of interfaces, which is the computer science way of saying that interfaces can be implicitly satisfied rather than having to have classes explicitly declare what interfaces satisfy them, makes it so that it is much less catastrophic to have a User module that just defines a user somehow. That module is not responsible for also guessing every other way in which the things using that module may want to slice and dice the services that a User object presents, and it is very freeing, while still being statically typed. </p>
<p>(Theoretically you can do this in the dynamic scripting languages too, but since you have no help from the compiler to be <em>sure</em> that an incoming object implements all the target methods, it is observably the case that in practice people are unable to confidently program this way, and I can speak from personal experience that in practice this breaks down relatively quickly because the code diverges too quickly and dangerously and eventually all of the things trying to be an "interface consumer" discover that they simply can't count on the "interface" over code development time.)</p></pre>
这是一个分享于 的资源,其中的信息可能已经有所发展或是发生改变。
入群交流(和以上内容无关):加入Go大咖交流群,或添加微信:liuxiaoyan-s 备注:入群;或加QQ群:692541889
0 回复
- 请尽量让自己的回复能够对别人有帮助
- 支持 Markdown 格式, **粗体**、~~删除线~~、
`单行代码`
- 支持 @ 本站用户;支持表情(输入 : 提示),见 Emoji cheat sheet
- 图片支持拖拽、截图粘贴等方式上传