7 ms·
The bigger the interface, the weaker the abstraction
- deleted 3y ago[deleted]
- ilrwbwrkhv 3y agoGo indeed has a simplicity and elegance that makes it really easy to write code which is highly performant and easy to maintain. That is the only reason I picked go when moving from common lisp.
- moffkalast 3y ago> from common lisp Is there such a thing as uncommon or rare lisp?
- School-Cotton 3y agoCommon Lisp is the name of a programming language.
- yellowapple 3y agoPossibly even Epic Lisp, Legendary Lisp, or Exotic Lisp.
- kazinator 3y agoYes. In the long history of the Lisp family of languages, many dialects have been created, and most of them are uncommon and rare, becoming more if they are disused and recede into history.
- jonahx 3y agoSlightly OT: I like Go, but the "-er" convention, while useful, has always bothered me because of the following inconsistency, well illustrated by two examples from the article: Writers write. Readers read. type Reader interface { Read(p []byte) (n int, err error) } type Writer interface { Write(p []byte) (n int, err error) } In a Go interface, a reader doesn't read: a reader gets read from. The reader (in the English language sense) is the client code of the Reader interface (in the Go sense). That said, I find the Java "-able" convention even uglier, though it answers my complaint.
- deleted 3y ago[deleted]
- tareqak 3y agoWhy not use both? -er’s do stuff to things while -able’s get stuff done to them and use them as it make sense. Use the one that makes the most sense given the situation.
- jonahx 3y agoYou could, but this is not Go's convention. Go's convention is that when you have an interface with a single method "x", you name the interface "xer". This has the advantage of being simple and mnemonic. It has the downside I already mentioned.
- taeric 3y agoWorks if you consider the reader is what you use to do the reading. You have a source you want to read, so you get a reader that can do that. Literally, you get a reader when you want to read something. You get a writer when you want to write something. Though, beware thinking you can solve this with word choice.
- jonahx 3y ago> Works if you consider the reader is what you use to do the reading. You have a source you want to read, so you get a reader that can do that. This strains ordinary language imo: Say you have a "log" which implements the Reader interface. When I call "log.read()" surely we are reading from the log, no? The log itself isn't the reader. This matches everyday usage. Perhaps your argument is that you can envisage the "log" object as an abstract "reader" of the bytes on disk, or something like that -- so the mental model is not a "log being read from" but instead "bytes on disk, an object able to read them (the Reader), and the code using that object to do the reading". In this worldview one never does anything oneself, but always uses a tool to do that thing -- the "-er" tool. I dislike this as it introduces an unnecessary layer of abstraction, but I grant it resolves the issue consistently. Also in this conception now I have a variable named "log" which does not represent the log but "a reader of the log". So you have to either ignore that inconsistency or name the variable "logReader", which introduces extra verboseness everywhere. The other resolution is simply to say this is a programming language, not ordinary language, and we decided that having a consistent, mnemonic way to name one method interfaces was more important than respecting natural language analogies. So f it, we're using "-er" to mean "-able". I would be curious to know what the actual conversation was during the development of Go.
- account-5 3y agoI'm having a hard time understanding what the author means by interface given the context they're using it in at the start of the article. I remember not thinking I fully understood the term when used in programming either, or why you'd use one. Can someone explain for the idiot?
- saghm 3y agoI _think_ the idea is thinking of an "interface" as "something that you use as a way to interact with something from outside an abstraction". I'd summarize their argument as reasoning that if the goal of an abstraction is to avoid having to care about the internal details of something, an interface is a way to expose a subset of ways to interact with it, and the more you expand it, the more it exposes the internals of the thing being abstracted. I don't think they necessarily mean this only in terms of programming, but you could apply this argument to a programming language interface; if you use an interface for interacting with something instead of its direct functionality, each additional method you add to the interface exposes more details of the inner value, which makes it less of an abstraction. Assuming my interpretation is correct, I'm not sure I totally buy this argument because there doesn't seem to be an obvious way to define the "size" of an interface where it holds true. The naive way to define the size would be number of methods, but I'd argue that methods can vary so much in terms of the amount of cognitive overhead they "expose" to the user that it's not very meaningful. Consider the Movfuscator compiler[0], which compiles code into binaries only using MOV x86 instructions because it happens to be Turing complete; as complex as it might be to learn x86 assembly as a whole and start writing programs directly in it, I'm dubious that trying to do so only with MOV would somehow be easier. Put another way, an x86 instruction set that only contains the MOV instruction is not a "stronger" abstraction than the actual one because it _introduces_ complexity that doesn't exist in the original. Does adding an ADD instruction alongside MOV increase the strength of the abstraction, or weaken it? I don't think there's an answer that we'd immediately all agree on for this sort of thing. Ultimately, I think trying to measure interfaces through the number of methods they expose is similar to trying to measure code by the number of lines in it; while there are some extreme cases where we'd likely all agree (e.g. for a fizzbuzz implementation, having 10 lines of code is probably better than thousands of lines of code[1]), we can't really come up with a good objective metric because the "target" number is based on the complexity of what you're trying to define, and we don't have a way of quantifying that complexity. I think the ideas here are still super interesting though, not because they have definitive right or wrong answers, but because thinking about stuff like this overall improves one's ability to write good software for usage by other programmers. [0]: https://github.com/xoreaxeaxeax/movfuscator https://github.com/xoreaxeaxeax/movfuscator [1]: https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpriseEdition https://github.com/EnterpriseQualityCoding/FizzBuzzEnterpris...
- taeric 3y agoThis runs the risk of being reductionist to the point of useless. Is akin to the idea of "do one thing," or "single responsibility." The danger is these all fail to grow. Or, rather, for them to work, you have to force the growth on either side. Consider, you are using a writer to write data. How much can you write before it has to slow down? How do you signal broken data connections? How do you resume a previously stopped writer? How do you stop a writer? Most of these do not have universal answers. But it gets worse. The main abstraction there was the symbolic byte. How do you convert your code's data to bytes? Is that stable? As in not changing. So, you can have choke points of small interfaces to connect things. But to make that work, you have a lot to do on both sides.
- stinkbutt 3y agoits a misconception that single responsibility is about doing one thing. in fact the principle allows for a system to do many things, but the idea is that it should only serve a single actor.
- taeric 3y agoI assert it is misguided to cling to any view of these as a fundamental truth. They are modeling tools. As such, use them for their usefulness. Don't chase truths with them, at they are tools, not truths.
- stinkbutt 3y agoi agree, and its important that we understand what the tools were designed to do before we use them
- whstl 3y agoCalling it a misconception is not really fair. The "serve a single actor" is a recent revision of the principle by Uncle Bob, from a 2018 book, AFAIK. That's almost 20 years too late (if going by the sources I can find). I can't find mentions of "actors" or similar terms in Agile Software Development or Clean Code. Pretty much every pre-2018 literature says that SRP relates to modules "[having a] single reason to change", or "[not having] too many responsibilities". The examples are always fuzzy and imprecise, but in the books the principle essentially boils down to class/method size. There are no examples in Clean Code or ASD relating to this new meaning, nor mentions of "actors" or "stakeholders", and until recently there's rarely any article contradicting the original meaning of SRP. This revisionism seems to come first from backlash from part of the programming community, and also an adaptation of the more recent "one team per microservice" rule. But the damage of almost 20 years of SRP being taught the way it was in the past was already done.
- mayoff 3y agoThis lesson is applicable to lots of languages, not just Go. For example, looking at Swift (where the equivalent of an interface is a protocol), we find many protocols in the standard library that either have one function requirement, or add one function requirement to a parent protocol: - Equatable requires == - Comparable extends Equatable, requires < - Hashable extends Equatable, requires hash(into:) - Identifiable requires id - Encodable requires encode(to:) - Decodable requires init(from:) - CustomStringConvertible requires description - LosslessStringConvertible extends CustomStringConvertible, requires init?(_:) - CustomDebugStringConvertible requires debugDescription - CaseIterable requires allCases - Sequence requires makeIterator() - IteratorProtocol requires next() - TextOutputStream requires write(_:) - TextOutputStreamable requires write(to:) - CustomReflectable requires customMirror - CustomPlaygroundDisplayConvertible requires playgroundDescription - AsyncSequence requires makeAsyncIterator() - AsyncIteratorProtocol requires next() And Apple’s Swift-only frameworks also have major single-function protocols: - SwiftUI.View requires body - SwiftUI.Animatable requires animatableData - SwiftUI.TimelineSchedule requires entries(from:mode:) - SwiftUI.EnvironmentKey requires defaultValue - Combine.Publisher requires receive(subscriber:) - Combine.Cancellable requires cancel() - Combine.Subscription requires request(_:)
- klabb3 3y agoI sorta kind of agree with where the author is going, but I think that’s because I know Go and this works quite well in that language. But if I pretended to not know Go, this doesn’t make too much sense on its own, in an arbitrary context. Even in Go, there isn’t such a strong correlation with interface size and “abstraction strength”. For instance, io.Reader and io.ReadWriteCloser are subjectively similar in terms of “strength”, it just depends on what you’re building. One of the “strongest” abstractions is net.Conn which has 8 methods, which is a lot. In fact, http.ResponseWriter was too small and an ongoing annoyance for years. Only recently was this flaw addressed with http.ResponseController. Instead, I suspect that Go interfaces are powerful mostly because everyone and their grandma uses the same interfaces, thanks to (1) a std lib with opinionated interface definitions and (2) that the language itself is very focused on “one way” of representing things (no function coloring, for instance). This allows for eg a vast ecosystem of http middleware, that isn’t tied to a specific http server. Indeed, whenever you venture outside of std interfaces, their appeal really starts waning. They’re fine, but the being seamlessly interoperable within the larger ecosystem is, in my view, where the magic is.
- js8 3y agoI like Eric Meijer's take on this. He said that the important property of interface are not the actual method signatures, but rather laws that the methods follow when being called. I think it follows that with bigger interface, there are more laws to follow (for example, read() must be called after open() but not after close()), and as a consequence, utility of that abstraction gets more limited.
- fit2rule 3y ago[dead]
- mikhailfranco 3y agoWith the corollary that the laws are complete. YAGNI takes a back seat to algebraic closure.
- dmvdoug 3y ago> So you list all the things that the ideal version of yourself must have and a long list of things you must do. But to make any progress on your goal, you have the cognitive load of keeping track of all the requirements you’ve put on yourself. I speculate that this is why many New Year’s resolutions fail; the abstraction for building your future self is weak. Or people just pop them off their stack willy-nilly because it’s a vague tradition to do so. Because most people who are old enough to be serious about a “New Year’s resolution” also are old enough to understand that if you really want to accomplish something or change something, it takes time and dedication and not just pinky-swearing at midnight over champagne. Also, not to nitpick, but the reason people make lists of things to get done is so that there is literally no cognitive load in keeping track of them. And it’s a weird pivot to go from people have a lot of things they think they want to accomplish to writers write and runners run (literally cases where people have only one thing they want to accomplish). The framing of this piece was so weird.
- dgan 3y agoI have this analogy stuck with me: «Classes should be deep, not large». Which means: imagine a thin rectangle, but very tall, which stands on its thin side up; now, the X-dimension is the price you have to pay upfront to understand an abstraction ("interface") and Y-dimension is what you get once you understand it. The ratio between the two is the "leverage" you get from that abstraction. Now imagine a perfect square: you gain as much "leverage" with your abstraction as you get functionality out of it: this the prime example of your dumb setter in Java, because it takes you exactly the same effort to learn what it's doing, as for you to do it yourself. Again this is a picturesque analogy.
- diarrhea 3y agoI don’t know if that book originated the idea, but your description fits closely to Philosophy of Software Design.
- henkelibonk 3y agoSimilarly, the leverage or “reusability” equals complexity of the implementation divided by size of the interface
- amelius 3y agoSo the DOM is a really weak abstraction?
- Capricorn2481 3y agoI don't know what you're referring to. I don't think parsing an entire page of text, selecting an element, and returning it as an object you can manipulate represents a weak interface
- deleted 3y ago[deleted]
- naasking 3y agoArguably, yes. You can only use the DOM in contexts where you need exactly that DOM. That said, "weak" is a poor descriptor for this property.
- delusional 3y agoI believe it's time to post this again: https://steve-yegge.blogspot.com/2006/03/execution-in-kingdom-of-nouns.html https://steve-yegge.blogspot.com/2006/03/execution-in-kingdo... It's important to realize that for 90% of the software we write (all the stuff that's not frameworks) we do want weak abstractions. We want to bundle stuff exactly because abstractions create complexity and specificity is simple.
- naasking 3y agoThat's a weird take, abstractions don't create complexity, they simplify because it requires less context to understand what's going on.
- naasking 3y agoI think "big" and "weak" are poor terms as they're vague. I think the real principle he's describing is something more like "context-free knowledge/rules are more flexible and useful". The less context you have to consider when using an abstraction or making a decision, the simpler and more general it is. Like his example of "no drinking", there are no contexts in which he allows himself to drink, so it's simple and can deployed in any situation. In the reader/writer interfaces, there is only a single requirement for reading or writing, the byte array.
- smileykenziekv 3y agoIt's true that overly large interfaces can lead to increased coupling and decreased maintainability, and I appreciate the practical examples and tips shared to keep interfaces focused and robust.