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.
IIRC Zed Shaw had an essay on this.
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."
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.
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.
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.
Fundamentally they are very different practice of software development and I think what OP talks about are those many cases where there is no scale need, never will be, yet we see this design approach that is suited for the type of projects you talk about.
Right tool for the right job after all...
If you knew what you were doing when you made those early and careful abstractions, then I agree completely (the Go standard library, after all, is a set of careful & well-reasoned abstractions). But if you made the wrong choices three years ago, because you didn't understand the problem you have today back then (either because you didn't understand the problem then, or because the problem changed), then you're going to hate yourself.
If you're attacking a well-known problem which will be the same problem you'll be attacking in a decade, then sure: early abstraction isn't premature. But how often do we do that, and how often are we exploring an unexplored problem space? At least for the projects I've worked on, each one has been terra nova.
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.
Close, with the exception of mocking interfaces for unit testing, which is very common in Java.
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.
At least you have the workaround
type PublicPart<T> = {[K in keyof T]: T[K]}; //keyof only sees public propertiesIt's awful to do in Java, but I felt fine doing it in go.
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.
Guess what all nontrivial programs manipulate in the end.
> objects that don’t have such well defined invariants
... are either stateless or useless. It is impossible to define useful algorithms that manipulate objects of which you cannot say anything.
> No major language has adopted them for that reason.
The real reason is “programmers cannot reason about invariants anyway”.
(0) An ADT has a single internal representation that is known to the implementor, but hidden from users. The upside is that the ADT implementor can use his knowledge of the representation to optimize on n-ary operations, i.e., operations that act on several instances of the ADT. The downside is that ADTs cannot be extended by third parties. You would have to create a different ADT, even if satisfies the same contract.
(1) A Go interface has a list of methods, but does not proscribe any fixed internal representation. The upside is that anyone can create values of an interface type as long as they can implement the methods. The downside is that it is difficult to implement n-ary operations, because, at any point in the program, the implementor only knows the internal representation of the single value they are currently creating.
In other words, ADTs optimize for efficiency and predictability, whereas interfaces optimize for flexibility and extensibility. As a general rule, ADTs require more time than interfaces to plan ahead their design and implementation, but already implemented ADTs are easier to use correctly than already implemented interfaces.
While Go is reinventing the languages of the 80s in the 2010s.
You cannot do that in Go, you need to write interfaces upfront. Or you can't unit test a controller that depends on a repository which concrete implementation relies on a database.
There is no "bytecode manipulation" here, since there is no bytecode.
Getting it to the point actual implementations are replaced by mocks at binary level... might be rather challenging.
(In Golang, tests mainly work like this: foo.go tests are in foo_test.go. "go test" would then run the unit tests for foo in foo_test.)
As for your test example, it doesn't work if you need to mock a library only available in binary form.
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.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.
I understand the concern. I'm not just saying that, the Haskell side of me still feels it. However, the pragmatic side of me says that it's not worth giving up the convenience, power, and decoupling of not having to explicitly declare conformance to interfaces to save myself zero bugs over nearly twenty years.
Now, HN is big and I'm abundantly sure there's a dozen people who can pop up with stories of when this bit them. But it's a question of tradeoffs; literally every day I program in Go I benefit from not having to declare interface conformance. Do you face this bug literally every day? Or even every month?
("What if you did have the problem and didn't notice?" Then apparently the cost wasn't that high, so cost/benefit still tilts in the direction of doing what Go or Python does. "What if it happens as the project gets bigger than what you've worked on?" Well, the dynamically-typed scripting products I've worked on tend to be past the size event horizon for other reasons, so we could never witness this being a huge problem because the code base can't get that large without also adopting practices that would tend to eliminate this anyhow. Statically typed I haven't gotten there, though I'm thinking statistically projects tokens would tend to diverge faster than they could converge here... i.e., even if I do somehow end up with twelve Start() functions that I don't intend to be a single interface, and all manage to have the exact same function signature, if they're separated by enough in the code that they just never cross, it still isn't a problem. With Go, you still have a hard time just accidentally passing them in... they have to traverse the codebase to get to the wrong place, across statically-typed paths.)
Sounds very much like only a theoretical gotcha.
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.
However it plays a role which programming languages I will advise our customers to use.
Currently I only advise the use of Go for those customers that need to work with Docker or K8s.
Edit: on reflection this may actually be duck typing not structural typing.
String perform(Supplier<String> speaker) {
return speaker.get();
}
This is a poor example, but that's what's in the article. Better might a method which is going to do some message broadcasting or something. With interfaces: interface Broadcaster<T> {
void send(T value);
void reset();
void close();
}
void pump(Source<T> source, Broadcaster<T> broadcaster) {
// this logic is nonsense but hopefully it makes the point
T value;
while ((value = source.poll()) != null) {
try {
broadcaster.send(value);
} catch (Exception e) {
broadcaster.reset();
}
}
broadcaster.close();
}
You might write: void pump(Source<T> source, Consumer<T> send, Runnable reset, Runnable close) {
// still nonsense
T value;
while ((value = source.poll()) != null) {
try {
send.accept(value);
} catch (Exception e) {
reset.run();
}
}
close.run();
}
If you had an actual Broadcaster, this is simple to call: pump(source, broadcaster::send, broadcaster::reset, broadcaster::close);
And if you don't have an actual Broadcaster, or if there is no such interface, it still simple to call, because you write references to whatever methods you do have. They could be on different objects. They could be lambdas. They could be no-op lambdas if you don't care about one of the events. In a unit test they could be: List<String> sent = new ArrayList<>();
pump(source, sent::add, sent::clear, () -> sent.add("CLOSED"));
Etc.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.
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?
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.
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.
Unit tests test concrete implementation "units", so this is completely compatible with writing unit tests.
That makes it actually even easier to do unit testing, because for a narrow test you don't need to implement irrelevant methods.
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.