Search code examples
design-patternsinheritancemixins

Matrix-like inheritance pattern


I am looking for a design pattern of - what I call - matrix inheritance. To better explain the problem I'll describe an example.

Given I want to model (linux) system packages in a general way. I have a base class named Package and it's derivatives like Apache2, PHP5, etc. Now these classes contain all the methods to ensure this packages are installed and configured the way I want to them to be. But these classes should be usable on several different operating systems like Debian, Ubuntu, Fedora, CentOS, other Linux distributions and (if possible) Windows.

It seems to me like this could be solved with some kind of a inheritance matrix where the effective class is inherited (including mixins) from the base package class to provide foundational methods and mixins to extend and/or override methods from additional classes/modules/interfaces/protocols to eventually provide the functionality for an automated installation on the specific system used to mixin.

I am trying to build this in ruby, but what I am more interested in is the general design pattern to apply to the system to realize this.

Also I am sure that there is already an existing design pattern or a combination of those to get what I want, I just can't seem to find clean solution to this problem on my own.

As an added bonus (or even alternative) I am wondering if the problem is probably easier to solve using a functional point of view instead of an object oriented approach and how this would look like.


Solution

  • The question is, whether the packages should know how to install themselves, or just provide information to a general installer. This depends on which procedures have most in common, installing the same package on two platforms or installing two packages on the same platform.

    In both cases I would recommend the Strategy Pattern

    Platform operations as the Strategy

    Use this when

    • The operations a given package must perform are (almost) identical across platforms
    • Different packages on the same platform has a different install procedure.

    Provide each package class with a Platform class. This class will serve as a strategy pattern, and provide your class with the operations that varies between platforms, e.g.:

    (Sorry the code is in java. I don't know ruby :( )

    interface Platform {
      File getInstallDir()
    }
    
    class PHP5 {
      private Platform platform;
      public PHP5(Platform platform) {
        this.platform = platform
      }
      public void install() {
         // Let the platform provide information
         File destDir = platform.getInstallDir()
         ...
      }
    }
    

    or alternatively:

    interface Platform {
      void copyPackageFiles(File[] files)
    }
    
    class PHP5 {
      private Platform platform;
      private File[] php5Files = ...
      public PHP5(Platform platform) {
        this.platform = platform
      }
      public void install() {
         // Let the perform perform specific tasks
         platform.copyPackageFiles(php5Files);
         ...
      }
    }
    

    This approach is the most flexible of the two, but will most likely make you write more boilerplate code.

    Packages as Strategy

    Use this when:

    • Different packages on the same platform has a (almost) identical install procedures.
    • The operations a given package must perform are different across platforms

    This is very similar to the other pattern, just reversed. In this you will use the Package as the strategy and pass it to a Platform that controls the install procedure.

    interface Package {
      File[] getFiles();
      Properties getProperties();
    }
    
    class Ubuntu {
      public installPackage(Package package) {
        // Use the package to provide input for you operations:
        copyFiles(package.getFiles());
        updateConfiguration(package.getProperties);
      }
    }
    

    Using the package as the strategy, will make your package classes short - almost only getters. On the other hand you must follow a strict install procedure for a given platform, so it is not so flexible