interfaces – Is it ok to inherit a class without adding anything to the child, to respect the Open Closed principle?


Sure, this can be an appropriate way to model your classes. The EncryptedFileSource is not quite empty. It changes the type that the file source object is “tagged” with, and in cases like yours that can be relevant.

However, using the type system to add tags to an object is not always appropriate. Most importantly, the tag must be known at the time when that object is constructed. In most languages, you can’t change the type later. Also, things get complicated when there are multiple tags that can be combined – you’d need a class for each combination of tags.

Instead of modelling such tags in the type system, it can be more appropriate to model them more explicitly. For example, you might separate tags from operations on the data source:

class Source {
  public IDataSource DataSource;
  public bool IsEncrypted;

  ... // and some helper methods that take the IsEncrypted status into account
}

Or perhaps these tags should be modelled explicitly as part of the IDataSource interface.

In some cases, a Decorator Pattern could be more appropriate when you just want to wrap methods in an interface with extra functionality, but not add an externally visible tag:

class EncryptedSource : IDataSource {
  private IDataSource source;
  ... // forward method calls to the inner `source`
}

The idea to have a coordinator object that checks the type of the objects it is passed sounds natural, but results in very fragile programs: this violates most interpretations of the open/closed principle because the coordinator has hard-coded support for specific types. It is usually more appropriate to move those operations into the IDataSource interface so that they can be overridden, or by extending IDataSource to implement the Visitor Pattern. For example:

interface IDataSource {
  T Accept<T>(IVisitor<T> v);
}

interface IVisitor<T> {
  T DoNormalThing(IDataSource s);
  T DoEncryptedThing(IDataSource s);
}

class FileSource : IDataSource {
  ...
  T Accept<T>(IVisitor<T> v) { return v.DoNormalThing(this); }
}

class EncryptedSource : IDataSource {
  T Accept<T>(IVisitor<T> v) { return v.DoEncryptedThing(this); }
}

Of course, if all available data source types are known up front, then the OCP doesn’t really apply and hard-coding support for certain types can be an acceptable tradeoff. Especially if you’re writing an application and not a library, strictly following the OCP can be overkill and it can be more sensible to optimize for refactorability: when you can change the design in a future release, a simple solution will do for now, no fancy design patterns required.