
SOLID Go Design - davecheney
http://dave.cheney.net/2016/08/20/solid-go-design
======
skywhopper
Definitely good stuff to think about in the piece, but even as a Go fan, I
found the rhetoric to be insufferable. "Go obviously doesn’t have
classes—instead we have the far more powerful notion of composition". Go's
syntax may frame things differently than Java or Ruby or C++, and I don't
discount the impact of syntax and abstractions on code design, _but_ just
because you don't _call_ them "classes" doesn't mean they are something
entirely different.

Go types can have public and private fields, a set of associated functions
which can also be public or private, and ways to extend other structs or pose
as other types. So you've got encapsulation, inheritance, and polymorphism.
Pretending you don't have "classes" is just silly.

~~~
dilap
It's perhaps subtle, but there is a real difference -- traditional inheritance
allows the base class to call methods in the derived class; Go embedded fields
don't have this property, which is a huge simplification. (You also don't have
all the complexities of constructors.)

------
tunesmith
For middle of the road companies with middle of the road developers, I find
SOLID is not really a great set of principles to focus on.

'S' is pretty good because it helps encourage developers to get away from huge
classes that have dozens of methods.

'O' is basically useless when applied to a single codebase, because one of the
worst parts of middle-of-the-road developers is when they start to realize
they can extend classes. You end up with horrendously complicated and deep
class hierarchies. I find that when it's time to add functionality, it's far
better to just refactor and create new classes for composition.

'L' is deeply confusing, and the difficulty in understanding it is what leads
to the deep and brittle class structures above.

'I' drives intermediate developers to create more and more interfaces that
have 1-2 methods, which when combined with their difficulties with 'O' and
'L', just leads to confusion and high cognitive load when trying to maintain
the codebase.

'D' is good.

~~~
qyv
Could not agree more, I find SOLID often creates more harm than good with a
lot of devs. For example:

S: Each class can only do one thing, ever! Gotta do something else? Lets add
more classes that all inherit from each other!

O: Heh, no one even remembers this one, like, ever.

L: Ya, thats how inheritance works! Right? Right...

I: Hey, I know how to create interfaces! Interface all the classes!

D: OK, just stick the DB connection into the constructer and we've got the D
covered, yesssssh!

Really though, IMO SOLID is somewhat overblown in many cases. It is just a set
of guidelines for good OO design not any kind of iron-clad rules.

~~~
rimantas
Your comment just shows that SOLID is not understood, not overblown.

~~~
raverbashing
"No True Scotsman" indeed

And I want to slap the "S" people whenever I see a private method inserted
merely to call a library

So it's "single responsability" except for a set of arbitrary cases pulled out
of who knows where.

~~~
hnbroseph
> "No True Scotsman" indeed

that doesn't seem applicable.

------
kasey_junk
I am fairly consistent in my criticism of Go. I am also fairly consistent in
my insistence that the Dependency Inversion Principle is one of the harder
concepts to wrap your head around of the SOLID principles (not least because
several other concepts use terms similar).

But golang's structural typing actually allows for some of the easiest
demonstrations of the implementation of the DIP of any of the strongly typed
languages I've used.

I find Martin's definition of the DIP counterintuitive. A description that
makes more sense to me is "interfaces should be defined by the 'users' of the
interface, not by the implementers".

With a language like C++ or Java, to achieve this requires either lots of
adapter layers or compiler tricks. With golang it is simple.

As a for instance, let's say I'm writing a restful client library for a
specific application. It is tempting to to hard code a dependency on the std
lib http.Client.Do method. If instead with 3 lines of code, I define my own
interface for Do that matches the std library definition. I can use the std
library, or any other library that matches it (one that is faster say or
implements back off).

That is one of the few areas where the golang type system is pretty cool in
comparison to its peers.

~~~
pcwalton
But with Go's system you can't provide an implementation of an interface for a
type unless you defined that type in your package. You have to use an adapter
layer for that. (For example, say I want to make an interface with a function
"func Show() string" that prints debug information about a type--I can't
implement that function directly on http.Client, only on an e.g. "type
MyClient = http.Client" wrapper.) So that makes it harder to achieve loose
coupling, because you can't make your own interfaces that unify standard
library types and your types unless you make your types take exactly the same
methods with exactly the same signatures (problematic if you want your types
to support slightly different functionality, etc.)

In a system that allows you to implement interfaces for types that you didn't
define in your package, there is a little more boilerplate necessary compared
to Go, but only a few lines necessary to achieve the exact same effect. In
pseudo-Go:

    
    
        type Do interface {
            func Do(req *Request) (*Response, error)
        }
    
        // boilerplate
        implement Do for http.Client {
            func (c *Client) Do(req *Request) (*Response, error) {
                return c.Do(req) // call stdlib implementation
            }
        }
    
        // custom implementation of Do
        implement Do for MockClient { ... }
    

So Go makes a tradeoff with its structural typing: it doesn't require the
small amount of boilerplate code to achieve the pattern you describe, but it
prevents you from providing new implementations for types you didn't define.
Reasonable people can differ as to whether the side of the tradeoff Go makes
encourages or discourages loose coupling.

~~~
n0w
I think you're a little off. The only boilerplate required for your example is
the interface definition. After that you can define your own implementations
of the "Do" interface as suggested by the parent.

    
    
      // boilerplate
      type Do interface {
        func Do(*http.Request) (*http.Response, error)
      }
    
      // Custom Implementation
      type MockClient struct {}
      func (mc *MockClient) Do(req *http.Request) (*http.Response, error) {
        // MockClient type now satisfies the Do interface
      }

~~~
masklinn
You're not taking this issue in account:

> you can't make your own interfaces that unify standard library types and
> your types _unless you make your types take exactly the same methods with
> exactly the same signatures (problematic if you want your types to support
> slightly different functionality, etc.)_

~~~
n0w
The original comment mentioned being able to decouple Go code from specific
implementations of functionality by relying on self defined interfaces instead
of specific functions. pcwalton's example seemed to confuse this point, which
I wanted to clarify.

As to that point, your interface types will only match standard library ones
if you're _trying_ to unify them. In which case you probably want them to have
the same signature.

~~~
masklinn
> The original comment mentioned being able to decouple Go code from specific
> implementations of functionality by relying on self defined interfaces
> instead of specific functions. pcwalton's example seemed to confuse this
> point, which I wanted to clarify.

The problem is that you're clarifying the wrong thing, the example is
confusing because it trivially proxies a built-in method but the remark is
what's important to the reply. Your reply is only confusing things further by
considering the flawed example but ignoring the actual point.

> As to that point, your interface types will only match standard library ones
> if you're trying to unify them.

No, why would you think that? You may want to "compatibilise" multiple third-
party types (that some of them are in the standard library is a complete red
herring) as well as first-party ones (a mock or whatever) e.g. abstract over
stdlib sockets, zmq sockets and sqlite.

~~~
n0w
You're right, I'm confusing the issue. I re-read pcwalton's comment and I
think understand the point they're trying to make. Not sure the last part was
correct though.

> So Go makes a tradeoff with its structural typing: it doesn't require the
> small amount of boilerplate code to achieve the pattern you describe, but it
> prevents you from providing new implementations for types you didn't define.

Shouldn't this be "So Go makes a tradeoff with its structural typing: it
_does_ require a small amount of boilerplate code to achieve the pattern you
describe, but it _allows_ you to provide new implementations for types you
define."?

~~~
masklinn
Well no because "the pattern you describe" in that context was only
implementing stuff on types you own/control, which is basically a function
declaration in Go, the problem is that doesn't extend to types you _don 't_
control, Go only lets you define methods on local types (types from the same
package).

By comparison the code pcwalton shows has more upfront boilerplate, you need
to explicitly `implement $Interface for $Type`, but it works for both local
and non-local types.

That's what e.g. Rust does, you can implement a trait on a type if either the
trait or the type is local (in theory you could also do so if neither is local
but IIRC that's not allowed so conflicts don't have to be handled). Which
means you can do this:

    
    
        trait Do {
            fn do(&self, req: &Request) -> Result<Response>
        }
    
        impl Do for hyper::http::Client {
            fn do(&self, req: &Request) -> Result<Response> {
                ...
            }
        }
    
        impl Do for MockClient {
            fn do(&self, req: &Request) -> Result<Response> {
                ...
            }
        }
    

There's a higher up-front syntactic cost (in that you have to declare the
trait and you have to explicitly implement it even on the local MockClient
type) but in return you also can implement it on the non-local "actual" http
client.

------
praptak
There's something strange about application of the Liskov Substitution
Principle in the article.

Original formulation of LSP was: subtypes should not break supertypes, i.e.
caller expecting supertype should not be able to tell the difference when
getting an instance of subtype. That was expressed in terms of an explicit
subtype/supertype relation, like inheritance in OOP. When there's no such
thing in the language, the LSP becomes (quote article):

"Two types are substitutable if they exhibit behaviour such that the caller is
unable to tell the difference."

This sounds almost like a tautology. I understand that it isn't, since
"substitutable" is "you substitute class X for interface Y in your code" and
"observably same behaviour" is "X behaves like all implementors of Y are
expected to".

In practice, X doesn't declare it implements Y. If I review a change to X I
might miss that fact. If that change breaks behavior expected from Y, it's
bad.

I'm not sure if this scenario is a real problem, I have written like 100 lines
in Go.

~~~
adamlett
In Go you might have an interface _Rectangle "_:

    
    
        type Rectangle interface {
            SetHeight(int)
            SetWidth(int)
            GetHeight() int
            GetWidth() int
        }
    

Then you could conceivably have a type _Square_ which implements this
interface.

The problem is, that clients that use the Rectangle interface make certain
reasonable assumptions that turns out not to hold when the Rectangle in
question is a Square. Case in point: If you call SetHeight(x) and SetWidth(y),
you would assume that GetHeight() and GetWidth() return x and y. But that
obviously violates the invariant of a Square if x and y are not equal to each
other.

This problem is what LSP aims to address.

~~~
dragonwriter
This is sort of a classic example (with the behavioral expectations of the
interface that you have stated in the text, which are not expressable as a
constraint in Go code -- or in most languages where the LSP is a concern --
but which often _would_ be documented as an expectation of the parent class in
a class-based inheritance system, and would usually be expected from the
naming of the class/interface) of why mutability is problematic in terms of
the LSP, as its not problematic at all if the signature is:

    
    
      type Rectangle interface {
            SetHeight(int) Rectangle
            SetWidth(int) Rectangle
            GetHeight() int
            GetWidth() int
        }
    

Though, perhaps more than the LSP, this should be a textbook illustration of
why representations of mutable objects in a program should not be named after
entities in the domain whose essential identity is associated with one or more
attributes that are mutable in the representation.

~~~
adamlett
_perhaps more than the LSP, this should be a textbook illustration of why
representations of mutable objects in a program should not be named after
entities in the domain whose essential identity is associated with one or more
attributes that are mutable in the representation._

Yes, in the example I gave, I agree. Certainly objects such as Rectangles and
Squares should be modeled as immutable value objects. Immutability solves the
problem completely. The example would perhaps be better if instead of
Rectangles and Squares it talked about Windows and FixedAspectWindows.

~~~
dragonwriter
> The example would perhaps be better if instead of Rectangles and Squares it
> talked about Windows and FixedAspectWindows.

But, here, I think it should be quite clear that the solution is to make clear
that _independence of the dimensions on the two axes is not a guarantee
provided by the Window interface or class_. Interdependence of a particular
form is guaranteed by FixedAspectWindow, and there might be a similar subtype
of FlexibleAspectWindow that guarantees independence, but Window itself should
not make any guarantee (and should, if necessary for clarity, expressly
disclaim such a guarantee.)

But one thing I think that it is worth highlighting is that the fact that
_some_ guarantees of a type are expressable and enforceable through code does
not ensure that all are, and the LSP _most critically_ applies to those that
are not (since those that are expressed and guaranteed through code won't ever
be a problem), but to those that are communicated through other mechanisms,
_including those communicated implicitly through naming_.

~~~
dllthomas
> including those communicated implicitly through naming

As an aside, this is an aspect of what I like about choice of names like
"Functor" and "Monoid". There is very little room for ambiguity.

Further aside... In Haskell, there's still "do we include operational
equivalence?" and "are we considering equivalence only up to bottom?". But at
least the guarantees around the semantics for non-bottom values are reliably
uncontroversial.

------
akkartik
_eyeroll_

I wonder how Robert Martin was implementing SOLID before the wonder that is Go
was delivered unto us. And I wonder why all our buzzword-laden codebases are
still knee deep in crap if SOLID is so amazingly awesome. We've been hearing
about coupling and cohesion for over 50 years now. At what point does thinking
in those terms become part of the problem rather than the solution?

Is it possible that Go or any other language _du jour_ just seems cleaner
because it's _newer_ and so the cruft hasn't had a chance to form yet? Come
back in 10 years, and tell me how amazing it is then.

~~~
jimjimjim
it's a nice language BECAUSE the authors push back when asked to add this and
that and all the other latest language feature fads.

They really don't get enough credit for constantly dealing with the negative
hordes. Add features = language crufty. Don't add features = where's my
generics (or whatever).

~~~
al2o3cr
"Add features = language crufty."

Truly remarkable that they got it right on the first pass if that's the case.
Rob Pike must be a frickin SOOPER GENIOUS.

~~~
jalfresi
Dunno about SOOPER GENIOUS, but I reckon he knows his stuff. He's certainly
got the chops that prove it.

------
quickben
What's with all the Go pushing lately?

~~~
bojo
Well, 1.7 just came out, and GolangUK just happened? Not exactly surprising
that a lot of posts pop up around releases or events within a specific
community.

~~~
quickben
Ah that makes sense then. Interesting, I missed that but of news completely it
seems. Thanks

