0

I'm trying to write a JavaScript class constructor that can accept multiple options (just like some command-line tools, e.g. OpenSSL). For example:

class myClass {
  constructor(pathOrSize, isPublic) {
    // Receive an file path
    if (typeof pathOrSize === 'string') {
      if (isPublic) {
        // Read public key file
        this.public = ...
      } else {
        // Read private key file, and then generate public key instance
        this.private = ...
        this.public = ...
      }
    } else if (typeof pathOrSize === 'number') {
      // Create a new key pair based on the given size
      this.private = ...
      this.public = ...
    } else {
      // Throw an error
    }
  }

  // Use this.public (or this.private, if provided) to encrypt/decrypt message
}

When the class is instantiated, this.public is required, but this.private is optional. The conditional logic guarantees that if this.public is not provided, an error will be thrown.

For the first parameter pathOrSize, there are two possible input types so I use an if-else here to check the type. Then if it's string, based on the value of isPublic, we will get two more different scenarios that need to be handled. In my case, there are more options, which make the code snippet above looks even worse. It works, but it's verbose and hard to read :(

Given the fact that JavaScript doesn't support overloading (like Java) and pattern matching (like Rust), what's the most elegant or efficient way to handle situations like this?

Thanks for any solutions or thoughts!

Carrick
  • 25
  • 6
  • This really depends on the actual case. There are so many ways to deal with such situations, but there wouldn't be one that fits all scenarios in the best way. Maybe make your question more focused on an actual case. – trincot Jul 06 '22 at 07:54
  • It seems like you're trying to make `myClass` be responsible for too many things here. constructors don't usually have so much (or any, for that matter) conditional logic in them. Try and see if you can extract this conditional logic into another function, then provide the constructor with things it doesn't have to check in order to know what to do (e.g. a callback you can always call with the same parameters, etc.) – Patrick Roberts Jul 06 '22 at 07:55
  • @trincot Thanks for your advice. I have edited my question and hope it's more clear now. – Carrick Jul 06 '22 at 08:06
  • Yes. What will be the value of `private` or `public` when only one of both is set? Does that "path" case mean the object is not really completely initialised, and another method still needs to be called to complete it? – trincot Jul 06 '22 at 08:07
  • @PatrickRoberts Thanks for your idea and I'll try to extract them into separate functions. The reason for putting conditional logic in the constructor is that I want to make sure at least one public key instance, this.public, is instantiated when the class itself is instantiated. – Carrick Jul 06 '22 at 08:11
  • @trincot the `public` is a must for all cases, but `private` can be omitted if only public key file is provided. The object should be completely initialised. The "path" is just to provide an option for the constructor: if a key file path is provided, you read the file through that path; if not, then you create a new key pair. – Carrick Jul 06 '22 at 08:17
  • So that means that if we get in the inner `else` case we get an incomplete instance, as then your code only sets `private`, not `public`. Can you clarify? – trincot Jul 06 '22 at 08:19
  • @trincot So sorry for missing the code here. In the inner `else` case, the `public` should also be set. Thank you for patiently clarifying the question :) – Carrick Jul 06 '22 at 08:28
  • You could pass an object to the constructor that has a separate property for all the options `{path:"", size:0, public:true}`. Then, in the constructor you only have to check if the property is available and you don't have to check if it's the right data type. – Kokodoko Jul 06 '22 at 08:32
  • @Kokodoko Yes, it's also a good idea to pass an object to the constructor. But: 1. The layers of if-else are not reduced and the main logic is not changed. 2. In my case, the format of the constructor parameter should be similar to existing classes (for consistency purposes), which do not use an object as parameters :( – Carrick Jul 06 '22 at 08:54
  • You can think about https://stackoverflow.com/questions/11796093/is-there-a-way-to-provide-named-parameters-in-a-function-call-in-javascript (or https://stackoverflow.com/questions/50511398/javascript-es6-named-parameters-and-default-values) – tevemadar Jul 06 '22 at 09:27

1 Answers1

1

A typical way to differentiate between parameters, is to provide an alternative, static function to create an instance. This is how it is done for some native classes. For instance, Array has Array.of, Array.from, and Object has Object.fromEntries, Object.create, ...

If we follow that pattern, your example code could become:

class MyClass {
  static fromFile(path, isPublic=true) {
    // read key file
    const content = ...
    // Pass the appropriate arguments to constructor
    return isPublic ? new this(null, content) : new this(content);
  }

  static fromSize(size) {
    // Create new key pairs based on the given size
    const privateKey = ...
    const publicKey = ...
    return new this(privateKey, publicKey);
  }

  // Constructor is dumb: it has little logic
  constructor(privateKey, publicKey) {
    // If no public key is given, produce it from the private key
    if (publicKey === undefined) {
        if (privateKey === undefined) throw Error("must provide at least one key");
        // Generate public key instance from private key
        publicKey = ...
    }
    this.privateKey = privateKey ?? null; // turn undefined into null
    this.publicKey = publicKey;
  }
}

// Example calls:
let x = MyClass.fromFile("/testPublic.key");
let y = MyClass.fromSize(5);
let z = new MyClass("a", "b");
trincot
  • 317,000
  • 35
  • 244
  • 286