10 ms·
Great article that corroborates what I've also personally seen in many Go applications. IMHO, if you're coding Java style interfaces up-front ("Repository", "S
by tepidandroid 9y ago
Great article that corroborates what I've also personally seen in many Go applications.
IMHO, if you're coding Java style interfaces up-front ("Repository", "Service", etc) you're already doing it wrong. It's likely that you're either over-engineering your project or prematurely optimizing for flexibility you will not need. You'll likely feel as though you're fighting the language to build these abstractions and layered architectures.
Go is all about minimalism. As the article says, you should be writing the concrete implementations first, then defining interfaces in the client code. The calling code should pick and choose the behaviour it needs from the implementation. As hard as it is for most seasoned Java developers, you have to silence that anxious voice inside that keeps asking "what if I need to swap this out for something else?". Go is about getting things done, not beautiful abstractions.
- dionian 9y agoAs someone who works in a lot of Java codebases, I would say that good Java code is more similar to how you describe Go and less similar to the bad, over-abstracted Java code.
- seanmcdirmid 9y agoSometimes you don’t know what the implementation will be yet, and writing a bunch of interfaces out can help you discover what it should be, then you start working it out with interfaces gradually giving way to implementation. Interfaces are rarely rarely about interoperability even in Java. Most interfaces have one implementation. Instead, they are about abstraction, design, and so on. This article assumes that the end product is what was intended all along.
- xienze 9y ago> Most interfaces have one implementation. Close, with the exception of mocking interfaces for unit testing, which is very common in Java.
- stubish 9y agoThis. So many packages are a pain to test, because only a concrete implementation is provided. If an interface was used, consumers can swap it out for a mock implementation. It is an almost endemic problem, but the article is criticizing the only available solution (apart from forking). I am utterly sick of writing fragile scaffolding to deal with these dependencies, to the point I wish that Go provided an implicit interface for every declared type. Testing would be so much easier if I could pass in a MockFoo instead of a Foo, provided MockFoo satisfied the implicit interface.
- sythe2o0 9y agoThe article, from how I read it, is not suggesting single-implementation interfaces are the problem but instead where those interfaces are stored. The package accepting the interface should define the interface it needs (and perhaps that package defines the mock implementation), and the implementation should exist somewhere else.
- ghthor 9y agoI've programmed in both ways and they each have there place. But in general this is the right way, you shouldn't export interfaces, you should export concrete types and then use the documentation and tests to example what types of interfaces the types can cover.
- seanmcdirmid 9y agoTypeScript, having a purely structural type system, means any class has an interface. Unfortunately, if the class has any private or protected members, its interface becomes unimplementable.
- dahauns 9y agoYeah, tricky one, that. At least you have the workaround type PublicPart<T> = {[K in keyof T]: T[K]}; //keyof only sees public properties
- XR0CSWV3h3kZWg 9y agoI generally start with making something that accomplishes some small part of what I am trying to eventually accomplish, then iteratively add things. It's awful to do in Java, but I felt fine doing it in go.
- catnaroek 9y ago> Most interfaces have one implementation. Instead, they are about abstraction, design, and so on. Then what you want is an abstract data type. Unlike interfaces, which can be implemented anywhere, an abstract data type is implemented in a single place. This has obvious benefits for performance (clients don't need to dynamically dispatch on operations), control (implementors don't need to worry about malicious clients supplying impostors) and precision (a single module can export several abstract types, whose implementations are mutually dependent). Go's interfaces are pretty terrible at all of the above. Not that Java provides anything substantially better.
- kuschku 9y agoLuckily, Kotlin does provide much better ADTs, and Java can be swapped out for Kotlin 1:1. While Go is reinventing the languages of the 80s in the 2010s.
- catnaroek 9y agoAbstract data types have nothing to do with either algebraic data types or abstract classes.
- seanmcdirmid 9y agoI have only seen abstract data types used to describe stacks and queues, and never in the context of a larger design where the underlying implementations weren’t data structures but objects. Do you have any good examples?
- deleted 9y ago[deleted]
- catnaroek 9y agoIf you want data abstraction to do at least a minimum of static invariant enforcement, you pretty much have to use abstract data types. But if you're fine with littering logs with “exception was caught here, such and such data structure was corrupted”, suit yourself.
- chewxy 9y agoAh, that's why you write your abstract implementations in Haskell, and then rewrite them in Go ;P
- peterevans 9y agoAbstractions are not a bad thing, but early abstraction often lacks understanding of the problem to solve. Abstraction is complexity. We often need complexity, but we should not add complexity without cause. And I don't think this is a Java vs. Go thing. Let the program, or the problem if you will, prove to you where you need abstraction.
- frou_dh 9y agoAn interface whose purpose is to reduce known coupling is not really "abstraction". It's a mechanism for indirection. IIRC Zed Shaw had an essay on this.
- chewxy 9y agohttps://zedshaw.com/archive/indirection-is-not-abstraction/ https://zedshaw.com/archive/indirection-is-not-abstraction/
- balefrost 9y agoTo be fair, that Zed Shaw essay isn't particularly good. 1. He uses lay (i.e. non-technical) definitions of "indirect" and "abstract" to argue that the technical definitions are somehow flawed. Words have different meanings in different contexts, but he rejects those different contexts. 2. He argues that abstract classes in Java are actually a form of indirection because the abstract modifier "creates an indirect path to the real implementation". This is after he covers that "abstract" can mean "a concept or idea not associated with any specific instance". 3. He claims that no lay definition of abstraction permits the idea of "swapping out the implementation". He fails to understand that, from the client code's point of view, "swapping out the implementation" is a direct consequence of coding to the abstraction. 4. In the words of Rich Hickey, I think Zed is conflating Simple and Easy. I don't doubt that Zed saw something legitimately objectionable and that drove him to write the essay. But in trying to formulate his point, I think he ended up conflating ideas, relying on false premises, and built an argument that didn't have much of a logical progression. He's essentially saying "I'm right and everybody else is wrong because I looked it up in a dictionary."
- Pxtl 9y agoDirty secret: do that in Java too.
- ploxiln 9y agoThere's a difference though: in Java, the class declaration must mention the interface, but in Go it does not. In Go, you can declare an interface for a subset of a type's methods, in some other package, and use it there, without any change to the original type. You can create a new interface which a standard-library type satisfies!
- pjmlp 9y agoYes, and then be amazed that certain types match by accident interfaces with completely different semantics, just because the methods happen to be called the same by accident. Then Start() will throw a rocket instead of starting the coffee machine. just because RocketLauncher and CoffeeMachine happen to have the same set of methods.
- frou_dh 9y agoSo not only does a type have to "accidentally" implement an interface, but also a programmer has to explicitly write code that has a value of that type in scope, and pass it to a function in a wildly different domain. Sounds very much like only a theoretical gotcha.
- pjmlp 9y agoIt is a theoretical gotcha like writing unsafe code in C is. Sure it is easy to avoid such gotchas in small code bases, handled by two or three programmers. Scale it up to the typical sizes of enterprise projects, maintained by generation of programmers in distributed teams, with a goal of at least 10 years deployed in production and those gotchas happen all the time.
- frou_dh 9y ago
- eadmund 9y agoPremature abstraction is a smell in every language, but it's particularly bad in Go. If you're building out a giant hierarchy of directories & packages at the beginning of your project — you're probably wrong. If you're building out a giant hierarchy at the middle of your project — you're probably wrong. If you're building it out at the end of your project — your project is over now, why are you spending time abstracting stuff when you can deliver already? It's not that Go is against abstractions, really; it's just that we programmers love, absolutely love to build a system with operators and binary operators and summing binary operators and addition and numbers and real numbers and integers and complex numbers and imaginary numbers — when all we really need to do is add 2 & 2 to get 4. Go says, 'hey there, slow down, hang on a sec.' You're free to abstract if you need to — but there's a good chance you're wrong to do so. Write the simplest thing that could possibly work. Maybe, maybe once you've written it three or thirty times, you'll have a good sense of the problem and be able to write a sane abstraction for it. But if you're spending time fleshing out interface or package hierarchies before you even write any code: you're probably wrong.
- googlemike 9y agoSpoken like someone who has never worked on a long-lived project. There is no beginning, middle or end. You joined when it has been around for 7 years. It needs to be around 10 more years. Early and careful abstraction of absolutely every layer of your program is something you will always thank yourself for. If your classes only talk to interfaces at every single layer (no concrete classes talking to concrete classes, except for maybe a factory here or there to vend out said interfaces), your project will be much easier to test, refactor, and dynamically change.
- dnomad 9y agoYes I find these sorts of comments as GP to be hopelessly naieve. It speaks to somebody who has never worked on a large project (think 4 teams of 10-20 developers spread across NYC, London, Tokyo, and Hong Kong) or a long-lived project (think 10 years). That sort of minimial, incremental approach does not scale at all. Software engineering in the large requires, as you say, interfaces at every boundary and a near-religious understanding of how all the different components and boundaries fit together.
- hota_mazi 9y agoEvery single thing you say apply to Java. And C#. And C++. And pretty much any language in existence. It's just healthy engineering practices. Whether developers apply them or not has nothing to do with the language.
- tepidandroid 9y agoThe difference is that other languages (like the ones you mentioned) are actually quite accommodating when it comes to indulging developer abstraction fetishes, whereas you will be punished for trying to do so in Go.
- closeparen 9y ago... until you write unit tests, at which point you need either interfaces or global mutable function pointers to swap out underlying implementations with mocks.
- tepidandroid 9y agoI don't think we're in disagreement here. I have no qualms with abstractions/interfaces. It's simply a matter of putting the horse before the cart. The implementations should not have to tip-toe around pre-conceived abstractions.
- flukus 9y agoWe really need some better solutions to this. Aside from abstraction it also comes with the performance penalty of virtual functions everywhere. I think we had some good tools to solve this but either threw them away in newer languages or just forgot they were possible. Working with c recently I wanted to mock something and the language obviously couldn't help me so I came up with something along the lines of: #ifdef TEST #define foo mock_foo #endif This worked for mocking single functions but would need to evolve for more complex code. There are a number of ways to do this from optional includes to compiling test fixtures to their own binaries with the mock implementation being the actual implementation. Both options should work with just about any language to varying degrees of effort. The biggest barrier to approaches like this is the reliance on IDE's and what they can do. I've never seen an IDE that can handle rules like this, they want to compile the project and control it's structure. In c# for instance, it would be pretty easy to have a mock library with concrete alternative implementations and compile the tests individually, something like: csc.exe MyTest.cs MyClassUnderTest.cs /reference MyMockAssembly.dll This is perhaps even easier in c/c++ where the includes are more explicit. Once you take back control of the compilation process a lot more options open up though.
- guelo 9y agoI don't know much about Go but in Java and C# mocking frameworks use runtime bytecode manipulation to make it really easy to swap out concrete implementations without needing to create separate interfaces.
- nostalgeek 9y ago> IMHO, if you're coding Java style interfaces up-front ("Repository", "Service", etc) you're already doing it wrong. Then how do you unit test your controllers in a web app? or anything that depends on a database connection without decoupling service and repository? > Go is all about minimalism. No, it isn't. It's your opinion about Go. Unit testing doesn't become irrelevant because you are using Go. > you should be writing the concrete implementations first, then defining interfaces in the client code That's not how you do test driven development via unit testing. Just because Go is relatively new it doesn't mean software engineering best practices don't apply anymore.
- tsavola 9y agoThe distinction is that you don't first declare a few all-encompassing interfaces and then their concrete implementations, but piecemeal interfaces for each API that takes those implementations - only the bits of the interface that the API in question needs. That makes it actually even easier to do unit testing, because for a narrow test you don't need to implement irrelevant methods.
- DougBTX 9y ago> That's not how you do test driven development via unit testing Unit tests test concrete implementation "units", so this is completely compatible with writing unit tests.
- reificator 9y ago> Then how do you unit test your controllers in a web app? or anything that depends on a database connection without decoupling service and repository? From the post you responded to: > you should be writing the concrete implementations first, then defining interfaces in the client code. When you need to take an argument that you can swap out for another type, that's when you define your interface. Not earlier. > Unit testing doesn't become irrelevant because you are using Go. Unit testing doesn't become irrelevant because you don't architecture astronaut from the start. When you need to test, define an interface. But only the interface that you need when you need it. > Just because Go is relatively new it doesn't mean software engineering best practices don't apply anymore. By providing the client the ability to define the interface it removes the need to pretend you can plan for all circumstances upfront. Instead, at the moment you need to be able to mock things, you make an interface and mock them. That's not to say that you should never provide interfaces in a library, just that it's no longer the only option available. I'm not sure why you think that unit testing becomes harder with this approach, because to me it seems far easier.
- notheguyouthink 9y agoThat's a nice perspective. With that said though, what do you think about centralized data types? I admit it, I make Repository and Service quite frequently. With that said, I also have data types shared among implementations. Normally the data types are defined within the parent packages to the implementations, the same packages that hold the Service definition. I actually agree with your assertion that Service is bad form, and think I'll change it. I still have the issue though where a shared data type is commonly used among many implementations. Where do you think that will live? Perhaps in the same location, just without a formal Service interface?