Pregunta

I have a very large class of 59 methods and about 3000 lines of code. I know that's far larger than most people would want, but the class represents a virtual machine, and most of the methods are run time functions, so I think this is one of the times when a large class is warranted.

The question I have is how can I best organise the source code of this class, as I would like to split it up into multiple files.

I originally used John Resig's class pattern. I would export an object literal from each file, combine them together, and then create the class. This works very adequately, but I want to move to ES classes.

I've experimented with the Mixin pattern from MDN. Each file could export a function, and then I can combine them together with reduce.

export default Base => class extends Base { ... }
const inherit = ( ...fns ) => fns.reduce( ( v, f ) => f( v ) )

const Glk = inherit( GlkAPI, DateTime, Fref, Misc, Stream, Style, Window )

This is functional, but it's convoluted, and it adds a lot of layers to the inheritance tree, which is not good for performance.

So I'm wondering if there are any better options.

Things I've considered:

  1. Doing a build time concatenation. The resulting file could be very nice, as if it were a single file to start with, but I'd lose the ability to lint the source code, as the inside of a class is not valid outside of that context. Or I could add a dummy class wrapper around each file, but then I'd need to trim it out when combining, and that's not a simple concatenation.

  2. Use Typescript declaration merging. This sounds good in principle, but I don't think you can merge classes together, and merging a class with a namespace wouldn't work either as the namespace properties would become static class properties.

  3. Create one simple class and then manually augment the prototype with functions exported from the other files. I guess this is my leading option, but I'm wondering what other projects with big classes have done.

¿Fue útil?

Solución

In situations like this, when I have one part of my program that needs to stand alone as a single, self-contained module but contains many thousands of lines of code, I prefer to take a compositional approach to keep things clean and manageable.

Instead of trying to keep all of your functionality in one class, why not break it out into several "internal" classes that your main VirtualMachine class instantiates in its constructor and delegates to? Each class would be in its own file, and you can give your VirtualMachine class a facade interface to abstract the fact that it delegates to multiple objects internally to do what it needs to do (i.e. you can keep its interface the same as it is now even though its functionality is broken up into multiple classes internally).

I know you said that it makes sense for all of the code in question to be in one class, but even so large classes often have discreet subsystems that are responsible for specific things. Maybe the examples you provided (Stream, Style, Window, etc) handle specific concerns in your larger VirtualMachine class? In my opinion it's fine to break these out into separate classes that are only meant to be instantiated and used together in your main class.

If your classes all need access to some shared state, break that out into a separate class as well, instantiate it in the VirtualMachine class constructor, and pass it to all the other classes via their constructors when you create them.

The main benefit of taking this approach over the others you mentioned is that it can be done with a simple and well-understood language construct (a class) and common design patterns (delegation and composition), instead of relying on build magic or esoteric language features to work.

Otros consejos

In case, when you cannot apply composition, you can add mixins into YourClass like this:

class YourClass {

  ownProp = 'prop'

}

class Extension {

  extendedMethod() {
    return `extended ${this.ownProp}`
  }

}

addMixins(YourClass, Extension /*, Extension2, Extension3 */)
console.log('Extended method:', (new YourClass()).extendedMethod())

function addMixins() {
  var cls, mixin, arg
  cls = arguments[0].prototype
  for(arg = 1; arg < arguments.length; ++ arg) {
    mixin = arguments[arg].prototype
    Object.getOwnPropertyNames(mixin).forEach(prop => {
      if (prop == 'constructor') return
      if (Object.getOwnPropertyNames(cls).includes(prop))
        throw(`Class ${cls.constructor.name} already has field ${prop}, can't mixin ${mixin.constructor.name}`)
      cls[prop] = mixin[prop]
    })
  }
}
Licenciado bajo: CC-BY-SA con atribución
scroll top