Hacker News new | past | comments | ask | show | jobs | submit login
Designing Good Interfaces (pboyd.io)
100 points by sterasody on March 15, 2023 | hide | past | favorite | 31 comments



A good interface is one that allows the user to accurately assess the tradeoffs involved in using this program over some other solution.

In this vein any interface that requires you instantiate all kinds of library-specific structs just to call the relevant function, and conversely upon return, is hiding the actual surface area of the interaction boundary. This makes the aforementioned asessment harder.

This can of course be a justified tradeoff for intended use cases, and this is brings me to my point: An interface author must conscientiously and deliberately adapt the interface to intended use cases.

The user, on the other hand, cannot peer into the mind of the interface author and must measure the analogous intent and purpose of a given interface in the context of his particular requirements.

A good interface, then, is one which the author has designed and documented so that prospective users

(1) accurately determine its suitability to their specific use case,

and (2) are not suprised if and when they decide to make use of it.

Changing the intended use case by exposing or hiding configurability, inverting control or establishing «sane» defaults is a red herring. It has no bearing on the goodness of the interface. Goodness comes from wether configurabiliy, inversion or defaults are apparent to the user.

Edit: spelling/phrasing


Good interfaces should primarily have reasonable and well documented pre and post conditions. This is very apparently not so in the oil change example in the article.

It's completely stunning to me how frequently this is forgotten in spite of having been a key component of object oriented programming when it was invented (ie smalltalk days, before I was born).


Is this what we refer to as design by contract?

If so, what about invariants? Are invariants related to good interface design?


> Is this what we refer to as design by contract?

No. One need a specification of the intended behaviour. Behind this specification lays the interface (data type, here a Object-class). This data type on the other hand can have different representations.

Design by contract is a term i didn't read as an agreed scientific term. It is used in OO-languages for some pattern, where you "generate" (read imply) a specification. E.g. two unrelated services use the same data type within their communication. This may be injected or included within their dependencies. To me its related to code generation. It may aid the collaborations between multiple developers across multiple projects to 'move faster'. I have limited and bad experience with this.

> If so, what about invariants? Are invariants related to good interface design?

Invariants in my book are then a synonym for mixins. Which in OO-Design would be represented via dependency inversion. Invariants can be necessary at best. Its no measure for a good interface. If your data types are specified such that invariants do not missbehave, they can be used.

But don't trust me on this.


Invariants are not a synonym for mixins, I'm not even sure where that idea might have come from. Invariants are assertions that are true throughout a program or subset of a program (like loop invariants). Where did you get this notion they were synonyms for mixins and what would that even mean?


> Invariants are not a synonym for mixins, I'm not even sure where that idea might have come from. Invariants are assertions that are true throughout a program or subset of a program (like loop invariants). Where did you get this notion they were synonyms for mixins and what would that even mean?

It was my own understanding; Thanks for suggesting clarification. I falsely associated such mixins with encapsulated behaviour. It was my take on mixins and may have translated the term invariant wrong. Appreciated.


Thanks for your reply! I will need more research to reflect on the examples you give.

I discovered the concept from books and documentations, for example:

- https://learn.adacore.com/courses/intro-to-ada/chapters/cont...

- https://www.eiffel.org/doc/solutions/Design_by_Contract_and_...

and it seemed to me the main concern was program correctness/consistency.

Maybe I mix up different concerns and good interface design is more about satisfying use case?


> Thanks for your reply! I will need more research to reflect on the examples you give.

Please note my sibling answer which shows I am opinionated. I have memorized my own takes from such terms so researching about my answer may be time not used well. Sorry.

> - https://www.eiffel.org/doc/solutions/Design_by_Contract_and_...

Does indeed overlap. I only read about contract-based programming; contract by design via API documentation in a java environment.

> and it seemed to me the main concern was program correctness/consistency

It is. There are multiple factors to deploying correct software though.

> Maybe I mix up different concerns and good interface design is more about satisfying use case?

Apparently your are on the right track. But I would strongly agree on the latter. Such formal correctness proves didn't cross my career yet. But I am assuming safety-critical systems or systems haed to update may benefit here the most. Which would explain my lack of certainty.

Keep it up; Sorry.


Can you clarify what you mean by this?


Sure, here are some postconditions for ChangeOil():

- engine has appropriate amount of oil in it

- oil filter in place

- drain plug torqued correctly

- dip stick present

- oil cap on

No matter what the outcome of the operation (ie error paths or happy path), ChangeOil promises to return with the car in a state that fulfills those conditions.

In order to do that, some preconditions need to be fulfilled too:

- car has some amount if oil in engine

- car is able to start

- no major noises


Yes and frame conditions… Wheels should still where they were, nothing stolen from the glovebox, no bodies added to the trunk, brake lines uncut, etc.


For instance, does the old oil need to be drained before filling new oil?

Does the car need to be checked for leaks?

At the end of filling in new oil, how much oil should be in the car? How much left over?


Most of the post resonated with me, but the initial analogy and its concrete example give me pause.

> The dependency (oil, in this example) is an argument, not because anyone cares to customize it, but to simplify the implementation.

This is a huge leap! I know it’s an example, but it’s perfectly reasonable to satisfy both “don’t make me bring oil to the grease monkeys” and “express oil as an explicit dependency of grease monkey activity.” It will, of course, buck some recent discussions here, but the reasonable solution to that is an additional abstraction. And it will of course not buck another long time favorite here: functional core.

1. Provide the convenience interface which defaults to some way of determining a sensible default. In the example case, bringing your own highly configurable oil to the people who change your oil is an exceedingly idiosyncratic case, but for generalization purposes let’s say that’s optional.

2. For all cases, directly provide oil to the processes explicitly dependent on oil to proceed.

+ 3. Make sure you have your dependencies: if the weirdo with super weird oil opinions supplied their own weird oil, you’re done; otherwise get your sensible defaults ready. This is super cool because,

+ 4. Now you can just put oil in the car without making the oil selection everyone’s business (responsibility).

Maybe that’s “simplify[ing] the implementation”, but not in the ways that’s usually meant. It also has the really awesome property of preventing billion dollar mistakes. This:

> Leave the argument nil, and the function will silently leave the object in a bad state.

Doesn’t have to happen, even if your language/environment is predisposed to it. For the cost of a fairly mundane function boundary, you get a convenient interface for users who don’t care about how the oil sausage is made, and free null safety.

> What the caller probably wanted was more like this

I really don’t think that’s a reasonable assumption at all. What the caller probably wanted was more like this:

  func ChangeOil(c Car, oilType OilType) error {
    // There, that’s the whole function body
  }
They don’t care if you also provide a good interface to the mechanics, they care about you managing a business (abstraction) they hired you to take care of for them.


Go even has a mantra for this: “accept interfaces, return concrete types”. In the example, I would think to design it such that Oil is just an interface with adapters for either user specified oilType or oilInventory or whatever.


That seems like a more specific version of Postel’s law/the robustness principle:

“be conservative in what you do, be liberal in what you accept from others"

https://en.m.wikipedia.org/wiki/Robustness_principle


It seemed to me that the whole example was too contrived. The garage could simply have the option to buy the oil and give it to the mechanic.

    func GetOil(oilType OilType) Oil {
        ...
    }

    func ChangeOil(c Car, oil Oil) error {
        ...
    }


My first question was, where does “inventory.GetOil()” come from. Is this just a package that wasn’t mentioned? Not being a Go expert, are packages typically global like this? Would that be a good way to implement this function/method (in a package that then becomes available everywhere in main() as opposed to passing it)?

Ah, well, maybe I missed the point entirely.


I don’t write Go, so I’m similarly not qualified to address how this particular implementation satisfies the dependency. But my hunch is that it’s implicitly an import of a singleton. Which is basically a global, but a fairly common Java-ish pattern for encapsulation of shared dependencies.

It’s not even a terrible pattern if you’ve already accepted shared dependencies as a thing (which you have to do in reality), but it’s much easier to reason about if you isolate them to something that provides explicit dependencies where your logic happens.


After writing almost entirely functional code for the last 10 years, I find it really hard to read and reason about anything that looks like this.


Totally agree, commented something similar. Both the before and after code shown is impossible to reason about magic. The idea that programming interfaces should be good is good, but this example isn't the best IMO.


Off topic : I love this font. Looks so good, especially for code.

Font is Inconsolata (https://fonts.google.com/specimen/Inconsolata)


An interface is a contract. It shouldn't be concerned with implementation details and it should contain the absolute minimum to do the work.

public ResultType ChangeOil(Car c, Oil oil = null); is enough.

In the actual implementation of the interface we can check if oil is null and if it is, provide own oil which fits car. If oil is not null, we can check if it is enough and if the type fits car type.

We also can return a car with changed oil and an error.

However the user of the interface shouldn't be concerned with actual implementation details. The same way an user of a Web API shouldn't be concerned with actual implementation detail.


> It shouldn't be concerned with implementation details

And then, when some poor programmer finally comes to writing an actual implementation, he finds out that it's literally impossible without magic.


Check this book by David Hanson, C Interfaces and Implementations: Techniques for Creating Reusable Software:

https://drh.github.io/cii/

Example codes:

https://github.com/drh/cii


ChangeOil is an awful example. Your “better” code is untestable.

And ChangeOil(car, nil) makes complete sense to remove all the car’s oil: in Go, a nil slice is (essentially) equivalent to [].


Don't mean to be uncharitable, but the author seems unaware of basic FP concepts. There's a reason why most OO languages are moving more and more towards FP.


Which FP concepts would help here?


Prefer functions that return things over void methods, prefer immutability over mutability, prefer pure functions that declare (and require) their dependencies as inputs etc etc.


why not just check the array if it's empty? post doesn't give a good example.

I argue that the "better way" has more conceptual overhead. if this was a large project. just changing oil would require to understand many ideas.


I always have a taste for good design.


Sounds like something GPT could do. Better not specialize in this.




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: