Given the very simple requirements we just outlined, we can start designing interfaces for the system. Files and directories have something in common: they have a name and both files and directories can be contained in directories. This suggests a design that uses a base type that provides the common functionality, and derived types that provide the functionality specific to directories and files, as shown in
Figure 5.1.
Next, we need to think about what operations should be provided by each interface. Seeing that directories and files have names, we can add an operation to obtain the name of a directory or file to the
Node base interface:
The File interface provides operations to read and write a file. For simplicity, we limit ourselves to text files and we assume that
read operations never fail and that only
write operations can encounter error conditions. This leads to the following definitions:
Note that read and
write are marked idempotent because either operation can safely be invoked with the same parameter value twice in a row: the net result of doing so is the same has having (successfully) called the operation only once.
The write operation can raise an exception of type
GenericError. The exception contains a single
reason data member, of type
string. If a
write operation fails for some reason (such as running out of file system space), the operation throws a
GenericError exception, with an explanation of the cause of the failure provided in the
reason data member.
Directories provide an operation to list their contents. Because directories can contain both directories and files, we take advantage of the polymorphism provided by the
Node base interface:
The NodeSeq sequence contains elements of type
Node*. Because
Node is a base interface of both
Directory and
File, the
NodeSeq sequence can contain proxies of either type. (Obviously, the receiver of a
NodeSeq must down-cast each element to either
File or
Directory in order to get at the operations provided by the derived interfaces; only the
name operation in the
Node base interface can be invoked directly, without doing a down-cast first. Note that, because the elements of
NodeSeq are of type
Node* (not
Node), we are using pass-by-reference semantics: the values returned by the
list operation are proxies that each point to a remote node on the server.
These definitions are sufficient to build a simple (but functional) file system. Obviously, there are still some unanswered questions, such as how a client obtains the proxy for the root directory. We will address these questions in the relevant implementation chapter.