-
Notifications
You must be signed in to change notification settings - Fork 0
/
usefulinterfaces.slide
323 lines (167 loc) · 10.2 KB
/
usefulinterfaces.slide
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
Creating Useful Interfaces
Brian Ketelsen
@bketelsen
* Creating Useful Interfaces
In this section we’ll look at Go’s interfaces and learn how to use them for maximum flexibility and code reuse. We’ll single out several interfaces from the standard library and explore what makes them so powerful. You’ll learn how to make your abstractions count with small and concise interfaces.
- What Makes a Good Interface
- Standard Library Interface Examples
- Interface Size versus Interface Utility
* What Makes a Good Interface
- Interfaces should represent behaviors
- Interfaces should wrap the behavior of a dependency
- Good interfaces represent the smallest behavior possible
* Philosophy: Go and Interfaces
Proverb:
The bigger the interface, the weaker the abstraction
.caption _Rob_Pike_ at [[https://www.youtube.com/watch?v=PAAkCSZUG1c][Gopherfest SV, 2015]]
* What Makes a Good Interface
To define your interfaces, start by defining the behaviors of your types.
.code usefulinterfaces/demos/inventory/product.go /START OMIT/,/END OMIT/
* What Makes a Good Interface
An interface should represent one type of behavior. In the previous example it was _Storage_.
An interface should not mix different types behaviors. Don't have a ProductInterface that mixes _Storage_ with _Display_ for example.
* Practical Interfaces
We're going to use an Inventory application as an example that models something that you might write for your business.
*Business*Domain*Information*
- Products -- things we sell
- Suppliers -- vendors that provide Products
- Orders -- requests from us to our supplier to get more Products
- We store our data in *postgres*
- We have one Supplier *Acme*, with an HTTP API for placing orders
* Practical Interfaces - Dependencies
Our fictional inventory application has two data sources, a PostgreSQL database and a vendor API.
Suppliers, Products, and Orders are stored in PostgreSQL, but the interactions with the supplier are done through the supplier's HTTP API.
That means we have two dependencies which should be in their own packages:
- postgres
- acme
* Practical Interfaces - Dependencies
The *postgres* package will have implementations of the domain interfaces for each of the domain types that are stored in the PostgreSQL database.
The *acme* package will have an implementation of the domain interface for interacting with the supplier.
* Practical Interfaces - Transport Mechanisms
For compatibility with other systems, the inventory application will offer consumers of the data two choices in network transport:
- HTTP/REST API
- net/rpc calls
Therefore we'll have a package *transport* containing an *rpc* package and an *http* package.
* Package Organization - Interface
PRO TIP:
- Use https://github.com/josharian/impl to generate your implementation stubs
go get github.com/josharian/impl
USAGE:
impl 'r *Receiver' gopath/to/pkg.Interface
Example:
impl 's *Source' golang.org/x/oauth2.TokenSource > mocks/token.go
To make it easy to isolate and test each individual package you should create mock implementations of each Interface you define in the domain package. Keep all your mock implementations in a *mocks* package in the root of your application directory.
*impl* requires that the package with the interfaces can compile. Make sure you have a compiling package before using it.
* Practical Interfaces - Exercise 1
In the *$GOPATH/src/github.com/gophertrain/material/usefulinterfaces/exercises* directory there's a folder called *impl*
cd impl
cat interface.go
Now install the *impl* tool:
go get github.com/josharian/impl
Try it out:
$ impl 'c *CustomerService' github.com/gophertrain/material/usefulinterfaces/exercises/impl.CustomerGetter
You should see output like this:
.code usefulinterfaces/includes/impl/implout.txt
* Practical Interfaces - Exercise 1
That's very useful for both implementing the actual service and implementing the mocks. But it outputs to *stdout*. Let's make a postgres implementation of the CustomerGetter service:
impl 'c *CustomerService' {full gopath}/impl.CustomerGetter > postgres/customer.go
The postgres folder already exists, if it didn't you'd have to create it first.
Now make an implementation for a mock of the CustomerGetter:
mkdir mocks
impl 'c *CustomerService' {full gopath}impl.CustomerGetter > mocks/customer.go
*impl* generates the methods for a type that doesn't exist. To finish the implementation, you need to add a type called *CustomerService*.
type CustomerService struct{}
Now your code should compile.
* Practical Interfaces - Example Application
$ cd $GOPATH/src/github.com/gophertrain/material/usefulinterfaces/demos/inventory
In the root package *inventory* there are three domain objects:
- *Order* in order.go
- *Product* in product.go
- *Supplier* in supplier.go
* Practical Interfaces
The *inventory* package also defines five interfaces:
- *OrderStorage* in order.go
- *ProductStorage* in product.go
- *SupplierStorage* in supplier.go
- *SupplierService* in supplier.go
- *InventoryService* in inventory.go
* Practical Interfaces
Our application has *storage* interfaces, so there are (incomplete) implementations of those *storage* interfaces in the *postgres* package.
.code usefulinterfaces/demos/inventory/postgres/orders.go /START OMIT/,/END OMIT/
* Practical Interfaces
Similarly there's a *SupplierService* that represents the interactions over an external API to our supplier "ACME Widgets". The implementation of that service is in the *acme* package.
.code usefulinterfaces/demos/inventory/acme/client.go /START OMIT/,/END OMIT/
* Practical Interfaces
The *Service* interface defines the external API of our application to its consumers.
.code usefulinterfaces/demos/inventory/inventory.go /START OMIT/,/END OMIT/
* Practical Interfaces
We have customers who demand a REST style interface over HTTP, and we also have internal users in our own company who can use something faster like Go's *net/rpc*
It makes sense to create a new interface to represent the delivery of our service over any type of connection. In the *transport* package, there's an interface just for this:
.code usefulinterfaces/demos/inventory/transport/transporter.go
* Practical Interfaces
Now we can have transport specific implementations in packages under *transport*:
- *http*
- *rpc*
Let's browse through those two implementations quickly.
Of note, they both implement the *transport.InventoryTransporter* interface, which *embeds* the *inventory.Service*.
Now our *http* and *rest* implementations of the *transport.InventoryTransporter* interface are required to also implement the *inventory.Service* interface.
* Practical Interfaces
PROTIP:
Prove that your type implements an interface at compile time with this declaration:
`src/github.com/gophertrain/material/usefulinterfaces/demos/inventory/transport/http/rest.go`
.code usefulinterfaces/demos/inventory/transport/http/rest.go /START OMIT/,/END OMIT/
This variable declaration assigns a concrete type *RESTService* to the interface it should implement, then discards the result.
By doing this, your code won't compile if the *RESTService* doesn't implement the *transport.InventoryTransporter* interface.
- Type Safety -- even when using Interfaces for abstraction
* Practical Interfaces
Now with interfaces all defined and (mostly) implemented, we can tie it all together in *main.go*
`src/github.com/gophertrain/material/usefulinterfaces/demos/inventory/cmd/main.go`
Because the network services take interfaces, we can easily swap out storage drivers when changing databases, or more commonly, for testing.
* Practical Interfaces
Every interface in this application represents a behavior that is tied to a dependency.
- Storage: postgres
- Transport: http & rpc
- SupplierService: vendor API
With clearly defined interfaces, we can create new implementations of behaviors as business requirements change.
- Migrate to mysql? - implement the Storage interfaces
- New supplier? Implement the SupplierService interface
- New service delivery method? Implement the Transport interface
* Practical Interfaces
Your domain types (Order, Supplier, Product, etc) don't know or *care* how they're stored or delivered.
- No circular dependencies
- Easy testing with mocks that implement the interfaces
* Standard Library Interface Examples
*encoding/TextMarshaler*
.link https://sourcegraph.com/github.com/golang/go/-/info/GoPackage/encoding/-/TextMarshaler Implementations
* Standard Library Interface Examples
*crypto/Signer*
.link https://sourcegraph.com/github.com/golang/go/-/info/GoPackage/crypto/-/Signer Implementations
* Standard Library Interface Examples
- database/sql may be the best example of many interfaces used to represent a single aggregate dependency
*database/sql/Driver*
.link https://godoc.org/database/sql/driver Interfaces
* Standard Library Interface Examples
The *REAL* power of these interfaces is that many of them are so inter-related.
*sql/driver.Rows* is an iterator over a query's results
*rows.Next(dest*[]Value)* populates a slice of *sql/driver.Value*
*sql/driver.Value* represents base types that any *sql/driver* must be able to retrieve and store.
Convert to and from a *sql/driver.Value* by implementing the *sql/driver.Valuer* and *sql/driver.ValueConverter* interfaces.
* Interface Size vs Interface Utility
Rob Pike's quote at the beginning of the module stated clearly that the larger the interface, the weaker the abstraction.
Interface Guidelines:
- Start big
Abstract out your dependencies first
- Use interfaces for ONE and only ONE behavior
- Don't get sucked into Interface hell!
Create interfaces only where multiple types can implement the same behavior
*AND*
It is necessary to use that behavior in multiple places without coupling the
types that implement the behavior.
* Exercise
*src/github.com/gophertrain/material/usefulinterfaces/exercises/inventory* contains a *mocks* folder.
Create a file in the mocks directory that implements the OrderStorage interface.
- OrderStorage = mocks/orders.go
Hint: use a type that has a map[int]*DomainModel (e.g. map[int]*inventory.Order)
Bonus: Implement Supplier and Product
Double Bonus Round: implement a mock for SupplierService in mocks/acme.go