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.