17

A good quarter of a century ago when I was learning C++, I was taught that interfaces should be forgiving and as far as possible not care about the order that methods were called since the consumer may not have access to the source or documentation in lieu of this.

However, whenever I've mentored junior programmers and senior devs have overheard me, they've reacted with astonishment which has got me wondering whether this was really a thing or if it has just gone out of vogue.

As clear as mud?

Consider an interface with these methods (for creating data files):

OpenFile
SetHeaderString
WriteDataLine
SetTrailerString
CloseFile

Now you could of course just go thru these in order, but say you didn't care about the file name (think a.out) or what header and trailer string were included, you could just call AddDataLine.

A less extreme example might be omitting the headers and trailers.

Yet another might be setting the header and trailer strings before the file has been opened.

Is this a principle of interface design that is recognised or just the POLA way before it was given a name?

N.B. don't get bogged down in the minutiae of this interface, it is just an example for the sake of this question.

  • 10
    The "least astonishment" principle is much more prevalent in User interface design than in "Application programmer interface" design. The reason is that a user of a web site or program cannot be expected to read any instructions at all before using it, while a programmer is expected, at least in principle, to read the API docs before programming with them. – Kilian Foth Apr 14 '16 at 8:26
  • 1
  • 7
    @KilianFoth: I am pretty sure Wikipedia is wrong about this - POLA is not only about user interface design, the term "principle of least surprise" (which is quite the same) is also used by Bob Martin for function and class design in his "Clean Code" book. – Doc Brown Apr 14 '16 at 13:06
  • 2
    Often, an immutable interface is better anyway. You can specify all data that you want to set at construction time. No ambiguities left and the class becomes simpler to write. (Sometimes this scheme is not possible, of course.) – usr Apr 14 '16 at 15:27
  • 4
    Disagree completely about POLA not applying to APIs. It applies to anything a human creates for other humans. When things act as expected, they are easier to conceptualize and therefore create a lower cognitive load, allowing people to do more stuff with less effort. – Steven Burnap Apr 14 '16 at 23:50
25

One way in which you can stick to the principle of least astonishment is to consider other principles such as ISP and SRP, or even DRY.

In the specific example you've given, the suggestion seems to be that there's a certain dependency of ordering for manipulating the file; but your API controls both the file access and the data format, which smells a bit like a violation of SRP.

Edit/Update: it also suggests that the API itself is asking the user to violate DRY, because they will need to repeat the same steps every time they use the API.

Consider an alternative API where the IO operations are separate from the data operations. and where the API itself 'owns' the ordering:

ContentBuilder

SetHeader( ... )
AddLine( ... )
SetTrailer ( ... )

FileWriter

Open(filename) 
Write(content) throws InvalidContentException
Close()

With the above separation, the ContentBuilder doesn't need to actually "do" anything apart from store the lines/header/trailer (Maybe also a ContentBuilder.Serialize() method which knows the order) . By following other SOLID principles it no longer matters whether you set the header or trailer before or after adding lines, because nothing in the ContentBuilder is actually written to file until its passed to FileWriter.Write.

It also has the added benefit of being a little more flexible; for example, it might be useful to write the content out to a diagnostic logger, or maybe pass it across a network instead of writing it directly to a file.

While designing an API you should also consider error reporting, whether that's a state, a return value, an exception, a callback, or something else. The user of the API will probably expect to be able to programmatically detect any violations of its contracts, or even other errors which it can't control such as file I/O errors.

  • Exactly what I was looking for - thank you! From the ISP article: "(ISP) states that no client should be forced to depend on methods it does not use" – Robbie Dee Apr 14 '16 at 12:52
  • 5
    This is not a bad answer, nevertheless still the content builder might be implemented in a manner where order of the calls of SetHeader or AddLine matters. To eliminate this order dependency is neither ISP nor SRP, it is simply POLA. – Doc Brown Apr 14 '16 at 13:04
  • When order matters, you can still satisfy POLA by defining the operations such that performing later steps requires a value returned from prior steps, thereby enforcing order with the type system. FileWriter could then require the value from the last ContentBuilder step in the Write method to ensure all input content is complete, making InvalidContentException unnecessary. – Dan Lyons Apr 14 '16 at 17:38
  • @DanLyons I feel that's rather close to the situation the asker is trying to avoid; where the user of the API needs to know or care about the order. Ideally, the API itself should enforce the order, otherwise it's potentially asking the user to violate DRY. That's the reason for splitting out ContentBuilder and allowing FileWriter.Write to encapsulate that bit of knowledge. The exception would be necessary in case anything happens to be amiss with the content,(e.g. such as a missing header). A return could also work, but I'm not a fan of turning exceptions into return codes. – Ben Cottrell Apr 14 '16 at 21:42
  • But definitely worth adding more notes about DRY and ordering to the answer. – Ben Cottrell Apr 14 '16 at 21:50
12

This is not only about POLA, but also about preventing invalid state as a possible source of bugs.

Let's see how we can provide some constraints to your example without providing a concrete implementation:

First step: Don't allow anything to be called, before a file was opened.

CreateDataFileInterface
  + OpenFile(filename : string) : DataFileInterface

DataFileInterface
  + SetHeaderString(header : string) : void
  + WriteDataLine(data : string) : void
  + SetTrailerString(trailer : string) : void
  + Close() : void

Now it should be obvious that CreateDataFileInterface.OpenFile must be called to retrieve a DataFileInterface instance, where the actual data can be written.

Second step: Make sure, headers and trailers are always set.

CreateDataFileInterface
  + OpenFile(filename : string, header: string, trailer : string) : DataFileInterface

DataFileInterface
  + WriteDataLine(data : string) : void
  + Close() : void

Now you have to provide all required parameters upfront to get a DataFileInterface: filename, header and trailer. If the trailer string is not available until all lines are written, you could also move this parameter to Close() (possibly renaming the method to WriteTrailerAndClose()) so that the file at least cannot be finished without a trailer string.


To reply to the comment:

I like separation of the interface. But I'm inclined to think that your suggestion about enforcement (e.g. WriteTrailerAndClose()) is verging on a violation of SRP. (This is something that I have struggled with on a number of occasions, but your suggestion seems to be a possible example.) How would you respond?

True. I didn't want to concentrate more on the example than necessary to make my point, but it's a good question. In this case I think I would call it Finalize(trailer) and argue that it does not do too much. Writing the trailer and closing are mere implementation details. But if you disagree or have a similar situation where it's different, here's a possible solution:

CreateDataFileInterface
  + OpenFile(filename : string, header : string) : IncompleteDataFileInterface

IncompleteDataFileInterface
  + WriteDataLine(data : string) : void
  + FinalizeWithTrailer(trailer : string) : CompleteDataFileInterface

CompleteDataFileInterface
  + Close()

I wouldn't actually do it for this example but it shows how to carry through the technique consequently.

By the way, I assumed that the methods actually must be called in this order, for example to sequentially write many lines. If this is not required, I would always prefer a builder, as suggested by Ben Cottrel.

  • 1
    You have alas fallen into the trap I explicitly cautioned you to avoid from the outset. A file name isn't required - neither are the header and trailer. But the general theme of splitting the interface is a good one so +1 :-) – Robbie Dee Apr 14 '16 at 12:55
  • Oh, then I misunderstood you, I thought this was describing the intention of the user, not the implementation. – Fabian Schmengler Apr 14 '16 at 13:05
  • I like separation of the interface. But I'm inclined to think that your suggestion about enforcement (e.g. WriteTrailerAndClose()) is verging on a violation of SRP. (This is something that I have struggled with on a number of occasions, but your suggestion seems to be a possible example.) How would you respond? – kmote Apr 14 '16 at 17:43
  • 1
    @kmote answer was too long for a comment, see my update – Fabian Schmengler Apr 14 '16 at 20:06
  • 1
    If the file name is optional, you can provide an OpenFile overload that doesn't require one. – 5gon12eder Apr 15 '16 at 3:48

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.