📜 ⬆️ ⬇️

Interfaces as abstract data types in Go

Not so long ago, a colleague retweeted an excellent post How to Use Go Interfaces . It discusses some of the errors when using interfaces in Go, and also provides some recommendations on how to use them anyway.

In the article mentioned above, the author lists the interface from the standard library's sort package as an example of an abstract data type. However, it seems to me that such an example does not reveal the idea very well when it comes to real-world applications. Especially about applications that implement the logic of some business area or solve real-world problems.

Also, when using interfaces in Go, over-engineering disputes often arise. And it also happens that, after reading such recommendations, people not only stop abusing interfaces, they are trying to almost completely abandon them, thereby depriving themselves of using one of the strongest programming concepts in principle (and one of Go’s particular). On the subject of typical errors in Go, by the way, there is a good report from Stive Francia from Docker. There, in particular, interfaces are mentioned several times.
')
In general, I agree with the author of the article. However, it seemed to me that the topic of using interfaces as abstract data types in it is quite superficially disclosed, so I would like to develop it a bit and think about this topic with you.

Turn to the original


At the beginning of the article, the author cites a small example of code with which he points out errors when using interfaces that developers often make. Here is the code.

package animal type Animal interface { Speaks() string } // implementation of Animal type Dog struct{} func (a Dog) Speaks() string { return "woof" } 

 package circus import "animal" func Perform(a animal.Animal) string { return a.Speaks() } 

The author calls this approach “Java-style interface usage” . When we declare an interface, then we will implement a single type and methods that will satisfy this interface. I agree with the author, the approach is so-so. A more idiomatic code in the original article is as follows:

 package animal // implementation of Animal type Dog struct{} func (a Dog) Speaks() string { return "woof" } 

 package circus type Speaker interface { Speaks() string } func Perform(a Speaker) string { return a.Speaks() } 

Here, in general, everything is clear and understandable. The basic idea: “First declare types, and only then declare interfaces at the point of use” . It is right. But now let's develop a little idea in terms of how interfaces can be used as abstract data types. The author to the word points out that in such a situation there is nothing wrong with declaring the interface “in advance” . We will work with the same code.

Let's play with abstractions


So, we have a circus and there are animals. Inside the circus there is a fairly abstract method `Perform` (perform an action), which accepts the interface` Speaker` and forces the pet to make sounds. For example, the dog from the example above, he will force the bark. Create a pet tamer. Since he is not dumb here, we, in general, can also force him to make sounds. The interface is rather abstract. :)

 package circus type Tamer struct{} func (t *Tamer) Speaks() string { return "WAT?" } 

So far, so good. We go further. Let's teach our tamer to give commands to pets? For now, we will have one voice command. :)

 package circus const ( ActVoice = iota ) func (t *Tamer) Command(action int, a Speaker) string { switch action { case ActVoice: return a.Speaks() } return "" } 

 package main import ( "animal" "circus" ) func main() { d := &animal.Dog{} t := &circus.Tamer{} t2 := &circus.Tamer{} t.Command(circus.ActVoice, d) // woof t.Command(circus.ActVoice, t2) // WAT? } 

Mmmm, I wonder is not it? It seems our colleague is not thrilled that he became a pet in this context? : D What to do? Looks like Speaker is not a very appropriate abstraction here. We will create a more suitable one (or rather, we will in some way return the first version from the “wrong example” ), after which we will change the method notation.

 package circus type Animal interface { Speaker } func (t *Tamer) Command(action int, a Animal) string { /* ... */ } 

It does not change anything, you say, the code will still be executed, since Both interfaces implement one method, and you’ll be right in general.

However, this example allows you to catch an important idea. When we talk about abstract data types, context is crucial. The introduction of the new interface, at least, made the code an order of magnitude more obvious and readable.

By the way, one of the ways to force the handler not to execute the voice command is simply to add a method that he should not have. Let's add this method, it will give information about whether the pet is trainable.

 package circus type Animal interface { Speaker IsTrained() bool } 

Now the tamer can not slip into the pet.

Expand behavior


Let's make our pets, for a change, execute other commands, besides, let's add a cat.

 package animal type Dog struct{} func (d Dog) IsTrained() bool { return true } func (d Dog) Speaks() string { return "woof" } func (d Dog) Jump() string { return "jumps" } func (d Dog) Sit() string { return "sit" } type Cat struct{} func (c Cat) IsTrained() bool { return false } func (c Cat) Speaks() string { return "meow!" } func (c Cat) Jump() string { return "meow!!" } func (c Cat) Sit() string { return "meow!!!" } 

 package circus const ( ActVoice = iota ActSit ActJump ) type Animal interface { Speaker IsTrained() bool Jump() string Sit() string } func (t *Tamer) Command(action int, a Animal) string { switch action { case ActVoice: return a.Speaks() case ActSit: return a.Sit() case ActJump: return a.Jump() } return "" } 

Great, now we can give different commands to our animals, and they will carry them out. To one degree or another ...: D

 package main import ( "animal" "circus" ) func main() { t := &circus.Tamer{} d := &animal.Dog{} t.Command(circus.ActVoice, d) // "woof" t.Command(circus.ActJump, d) // "jumps" t.Command(circus.ActSit, d) // "sit" t2 := &circus.Tamer{} c := &animal.Cat{} t2.Command(circus.ActVoice, c) // "meow" t2.Command(circus.ActJump, c) // "meow!!" t2.Command(circus.ActSit, c) // "meow!!!" } 

Domestic cats, we are not particularly amenable to training. Therefore, we will help the tamer and make sure that he does not suffer with them.

 package circus func (t *Tamer) Command(action int, a Animal) string { if !a.IsTrained() { panic("Sorry but this animal doesn't understand your commands") } // ... } 

That's better. Unlike the initial Animal interface, which duplicates Speaker , we now have an `Animal` interface (which is essentially an abstract data type) that implements quite meaningful behavior.

Let's discuss the sizes of interfaces


Now let's think about the problem of how to use broad interfaces.

This is a situation where we use interfaces with a large number of methods. In this case, the recommendation goes something like this: “Functions should accept interfaces that contain the methods they need” .

In general, I agree that the interfaces should be small, but in this case, the context again matters. Let's return to our code and teach our tamer to “praise” your pet.

In response to praise, the pet will give a vote.

 package circus func (t *Tamer) Praise(a Speaker) string { return a.Speaks() } 

It would seem that everything is fine, we use the minimum necessary interface. There is nothing superfluous. But here's the problem again. Damn it, now we can “praise” another coach and he will give a voice . : D Do you catch it? .. Context always matters a lot.

 package main import ( "animal" "circus" ) func main() { t := &circus.Tamer{} t2 := &circus.Tamer{} d := &animal.Dog{} c := &animal.Cat{} t.Praise(d) // woof t.Praise(c) // meow! t.Praise(t2) // WAT? } 

Why am I doing this? In this case, the best solution would still be to use a wider interface (representing the abstract data type “pet” ). Since we want to learn how to praise a pet, and not any creature that can make sounds.

 package circus // Now we are using Animal interface here. func (t *Tamer) Praise(a Animal) string { return a.Speaks() } 

So much better. We can praise the pet, but we can not praise the tamer. The code again became simpler and more obvious.

Now a little about the Law of Bed


The last point I would like to touch is the recommendation that we should take an abstract type and return a specific structure. In the original article, this reference is given in the section describing the so-called Postel's Law .

The author himself cites the law :.
“Be accept with you accept”

And interprets it in relation to the Go language
“Go”: “Accept interfaces, return structs”
func funcName(a INTERFACETYPE) CONCRETETYPE

You know, in general, I agree, this is a good practice. However, I once again want to emphasize. Do not take it literally. The devil is in the details. As always, context is important.
Not always the function should return a specific type. Those. if you need an abstract type, return it. No need to try to rewrite code avoiding abstraction.

Here is a small example. An elephant appeared in the neighboring “African” circus, and you asked the owners of the circus to lend the elephant to a new show. For you in this case it is important, only that the elephant is able to carry out all the same commands as other pets. The size of the elephant or the presence of the trunk in this context does not matter.

 package african import "circus" type Elephant struct{} func (e Elephant) Speaks() string { return "pawoo!" } func (e Elephant) Jump() string { return "o_O" } func (e Elephant) Sit() string { return "sit" } func (e Elephant) IsTrained() bool { return true } func GetElephant() circus.Animal { return &Elephant{} } 

 package main import ( "african" "circus" ) func main() { t := &circus.Tamer{} e := african.GetElephant() t.Command(circus.ActVoice, e) // "pawoo!" t.Command(circus.ActJump, e) // "o_O" t.Command(circus.ActSit, e) // "sit" } 

As you can see, since we do not care about the specific parameters of the elephant that distinguish it from other pets, we can easily use abstraction, and returning the interface in this case would be quite appropriate.

Summarize


Context is extremely important when it comes to abstractions. Do not neglect abstractions and are afraid of them, exactly the same way as they should not be abused. Do not just take the recommendations as the rules. There are approaches tried by time, there are approaches that have yet to be tested. I hope I managed to expand a little deeper into the topic of using interfaces as abstract data types, and get away from the usual examples from the standard library.

Of course, for some people this post may seem too obvious, and examples sucked from the finger. For others, my thoughts may be controversial, and the arguments are inconclusive. Nevertheless, someone may be inspired and start thinking a little deeper not only about the code, but also about the essence of things, as well as abstractions in general.

The main thing, friends, is that you continuously develop and receive true pleasure from work. All good!

Ps. Code samples and the final version can be found on GitHub .

Source: https://habr.com/ru/post/450386/


All Articles