1

I'm working on a PHP framework called Carbon CMS. This framework will be used as base to build larger web applications on top of it. This framework should provide all kinds of interfaces such as database access, caching and so on.
Currently I'm working on the IO and file handling part of the framework. This question will be about the design pattern I should use, whether I should use a single, or multiple classes that extend each other. In this case, the question will be asked with file systems in mind, because this applies to the problem I'm having, and should make it easier to understand the question itself because an example is given. So keep in mind, that this design-pattern question also applies to other cases.

My idea is to built a class, or multiple classes that reference to a file, directory or symbolic link on the file system by it's path. Then, these class(es) should also provide an interface to read/write files, to list directories and so on. Think of the File class in Java, or the File, Dir classes in Qt.

The question I'm having is what kind of structure or design pattern I should use in this case. I could create a single File class that has all the functionality included, not just for files, but also for directories and symbolic link. But I could also create multiple classes, such as Path, File, Directory and SymbolicLink. So, there are two possible ways I can think of:

  1. Using a single File class. This class holds the reference (path) to a file system entry such as a file or directory. This class includes functionality both for files, directories and symbolic links, such as reading/writing files or listing directory contents.
  2. Using multiple classes, which extend each other. A Path class (or something similar) holds the reference (path) to the file system entry such as a file or directory. And only contains the basic functionality which could be used on all three different file system entry types (files, directories and symbolic links). Think of features such as Path::isFile(), Path::getPath() or Path::exists(). Then three other classes are used for the different file system entry types called File, Directory and SymbolicLink, all of these classes extend the Path class to implement it's basic functionality and the path reference. These three classes contain the functionality for that specific entry type. So, methods to read and write files would be found in the File class; File::getContents();, and a method to list the contents of a directory would be placed in the Directory class; Directory::list();.

So, to clarify my question. Should I go for the first option, and create a single File class which handles everything regarding IO and file handling. Or should I go with the second option and create multiple classes. Where each class is for a specific type of file system entry (except the Path class, which will be used for all three types).

Now, if going for the first option. Functions such as listContents() (to list directory contents) could easily be called on a path that references a file. It would be impossible to list the directory contents of a file, thus calls like this would return null, or something similar indicating the directory couldn't be listed. When using the second option, this would be a bit different. The listContents() method would only be available for Directory instances, and these instances should reference to a directory anyway. Of course, it would be possible that the referenced path doesn't exist, but that could be the case with both options.

There are a few pro's and con's for both options. Here are some I can think of:

  • Going for the first option, with a single class:
    1. Casting instances isn't required to use entry specific operations when using File's.
    2. Creates one huge interface for everything, which makes it messier and allows certain operations to be used on incompatible file system entry types, such as listing directory contents on a file.
  • Going for the second option, with multiple classes:
    1. Casting instances, and instance type checking is required to use entry specific operations when using Path's.
    2. Creates a separate interface for each file system entry type, which makes it less messy, and also prevents the use of most operations on incompatible file system entry types.

The framework will be open to the public and will be used by multiple people, thus I'm looking for the option that is the easiest to use, doesn't have a performance impact if possible and the option that suits this ground the best.

Here's some sample code, to show off the very basic structure of both options.
When using a single class, with option one:

class File {
    private $path;

    public function __construct($path) {
        $this->path = $path;
    }

    public function getPath() {
        return $this->path;
    }

    public function getContents() {
        // Return the contents of a file
    }

    public function listContents() {
        // Return the contents of a directory
    }
}

When using multiple classes, with option two:

class Path {
    private $path;

    public function __construct($path) {
        $this->path = $path;
    }

    public function getPath() {
        return $this->path;
    }
}

class File extends Path {
    public function getContents() {
        // Return the contents of a file
    }
}

class Directory extends Path {
    public function listContents() {
        // Return the contents of a directory
    }
}

I should note that all the methods noted in this question, such as Path::getPath(); or Directory::listContents(); are non-static and have to be called on the instance of the class.

Java uses a single File class, which would be similar to first option I gave with a single class. There are some classes available such as a DirectoryIterator but classes like this would be created anyway, and don't depend on the first or second option. On the other hand, Qt uses something similar to the second option I gave, as far as I know.

So finally; What would be the best option, and why?

1 Answer 1

3

I think you are touching two often discussed topics here:

  • Monolithic classes vs small components
  • Composition vs inheritance

The following is a conclusion from what I've read and learned when I had similar problems to solve.

Monolithic vs components

Usually you should go for components that provide a model of the reality instead of building big monolithic blocks. Small components are easier to test and debug and harder to break.

Composition vs Inheritance

If you can go without inheritance you should. In your case the only common piece of File and Directory is that it has a path. Note the HAS A relation here. In fact the path is a property. Neither a directory nor a file is a path.

So - what you should go for is composition instead of inheritance. Please see http://en.wikipedia.org/wiki/Composition_over_inheritance for an introduction.

How to hold things together

  • Look for the factory pattern for returning a file or a directory depending on the type of file system object
  • Use an interface to define methods that both File and Path need to implement.
  • If both need the same functionality define a class for it. Usually the methods in this class are static. A trivial example would be a class FilesystemPermission that provides a method ::show($file_or_directory_object)

By the way - reading 'The Pragmatic Programmer' (look it up in the bookstore of your choice) covers such problems in depth - go for it if you haven't read it yet (every programmer should!).

1
  • Yes, indeed my question does seem to be, or at least for a big part of it, about these two topics. I didn't know how this principal was called in programming, and couldn't find any useful information myself, thus I asked this question. Now about the answer itself, the answer you gave is brilliant as far as I can see. Besides, the suggestions you gave seem to be some great things to think about. Thanks!
    – Tim Visee
    Aug 13, 2014 at 12:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

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