The Perils of Write-Only File Formats
So last night I downloaded the new Preview 3 release of MEF and incorporated into Comicster (you can read the posts leading up to this here and here). Right now I can run Comicster and see “.cmxx” files and “.xml” files in the Open File dialog. if I then close the program and drop a DLL into the “My Documents\Comicster” folder, the next time I run Comicster I see “.cmx” files in the dialog as well. It works a treat and I’ll devote a post to the implementation soon (I don’t have the source handy here at work).
As you saw in my previous posts on the topic, I have defined the “file format plug-in” system using two interfaces, ICollectionReader and ICollectionWriter. That lets me create a “read-only” format to provide migration paths from earlier versions of Comicster or from other programs. The problem, though, is that it also lets people create “write-only” formats. For example, someone might define a “.csv” writer but not an equivalent reader.
Why is this a problem? I’m worried that Joe User might download Comicster for the first time, along with some file-format plug-ins, and spend a few hours creating his collection only to save it as “joe.csv”. He knows he has saved it, but doesn’t realise that he can’t re-open the file, and has essentially lost his work.
The way I’ll address this is slightly rearrange my interfaces so that ICollectionWriter descends from ICollectionReader. That means that if you want to implement a read-only file format you can just implement ICollectionReader, but if you want to implement a format that you can save to, you have to provide a way to read the collection back from that file. I might end up having to rename the ICollectionWriter interface to be more explicit in the fact that it’s both a writer and a reader.
But what, you ask, about exporting a collection to a file format that you don’t plan on reading from? A CSV export, for example, would be quite handy. I’ve got that base covered (at least in my head). I’ll be defining an ICollectionExporter interface, like this:
public interface ICollectionExporter { public void ExportCollection(Collection collection, DirectoryInfo folder); }Notice that this interface accepts a DirectoryInfo parameter rather than a Stream to write to. That’s because I want to be able to export into a folder and let the plug-in writer decide whether it’s a single-file export (like CSV) or a multiple-file export (like a series of HTML files). Heck, I might even define two different interfaces so that there’s a choice of exporting to a file or a folder, depending on the plug-in. Then I can surface these exporters under an “Export” button, which will make it much more obvious to Joe User that he’s writing his collection to a format that Comicster may not be able to read.
Comments
# Bil Simser
27/11/2008 3:27 PM
Further to the brief chat on twitter (140 characters, ugh) if you want them to be able to do various functions then one interface per set of capabilties seems to way to go.
ICanExportCollections
ICanWriteColletions
ICanReadCollections
Then your host checks the incoming plug in and asks if it implements ICanReadCollections. If it does, then you enable menu options or do whatever it is you do to let the user read using that plugin (maybe adding the file format extension to the open file dialog or whatever). If they don't implement the other interfaces, you don't enable those options and users don't accidently blow someting away.
# mabster
27/11/2008 3:32 PM
Thanks for the comment, Bil.
My worry is that since they're separate interfaces, there's nothing stopping the plug-in writer from only implementing ICanWriteCollections, leaving the user with the ability to write a file that he can't read.
I could check at runtime that the plug-in class also implements ICanReadCollections, but the plug-in author may have decided to implement that interface using a separate class, so a simple test like "writer is ICanReadCollections" isn't going to cover that.
So yeah - I want to be able to enforce that both Read and Write are implemented, and the easiest way I can think of to do that is to include both functionalities in a single interface, and not allow writing by itself.
# Bil Simser
27/11/2008 3:36 PM
That's the way to go, but it's a design choice not a language restriction.
You said "I want to be able to enforce that both Read and Write are implemented". The only way you can do that is to include it in one interface.
However just remember that while I have to implement a method called Read and Write (or whatever you call them) I don't have to make it work. I can still write the code to write out the format, but just have the method noop on the read.
There's no way to force someone to actually implement it, correctly.
# mabster
27/11/2008 4:39 PM
Yeah the ol' NotImplementedException may yet come back to bite me. It's more about doing as much as I can to nudge the plug-in author in the right direction.
All that's left, then, is to decide exactly what to call this interface that can read and write. I think I'll start it out as ICollectionReaderWriter and refactor it to something better when I think of it.