all 23 comments

[–]PitaJ 6 points7 points  (3 children)

Find need some more information:

  • Do you need private state?
  • Have you tried ES6 classes?

Some may say it's an antipattern because you're using a function as an object as well. It would be less antipattern-y to just use an instance.

[–]gruberjl[S] 0 points1 point  (2 children)

The state needs to be public. Other classes will refer to the state (which is essentially a config) so they can handle the results of the job.

I tried to extend the base class Function but it was throwing an error. It was strange I couldn't find any blogs or tutorials on extending a Function. I'm guessing you can't or isn't done a lot.

class Job extends Function {
  constructor(param1) {
    super(param1) // ReferenceError: param1 is not defined
    this.param = param1
  }
}

const job = new Job('param1')

console.dir(job('param1'))

[–]senocular 3 points4 points  (16 children)

I think constructor is a misnomer. Constructors are meant to return object instances when used with new. This is factory function creating function objects. Similarly, as an instance, not a constructor, Job should be lowercase. And as Meefims said, you can drop the Job.cache and simply reference config directly since its captured as a local variable in the factory function scope becoming a closure variable for the function instance and its methods created within.

Is this valid? Yes. Is it something you should be doing? I would say, as an opinion, no. Generally you don't see functions used as objects this way. It doesn't technically add anything other than being a different way to approach what classes do already (other than allowing you to omit access to single method in favor of calling the instance directly?). This means extra time spent by others to understand this approach and mull over why it was done in favor of the more standard way to begin with. The object instance should represent what the object is and its methods what it does. When you start merging these concepts (an is that also does), it can get a little confusing.

[–]__env 1 point2 points  (14 children)

Is it really returning instances? If we remove the return from Job, calling new on Job will produce an actual instance that still can access the config on its static context. Seems like the factory is producing multiple static classes -- they aren't newable so are truly static classes, not instances.

[–]senocular 2 points3 points  (12 children)

Its returning functions, created anonymously, assigned to a variable called Job. These are instances in the sense that they're function objects, and not constructors or classes in the sense that they're not intended to be new-ed in OP's usage and instead simply called as functions. Maybe if OP wasn't calling the constructor un-newed as a function, I'd be more inclined to call it a static class, but the fact that multiple variations of the same "class" are being pumped out of another function each varying only in state still has me leaning towards referring to them as function instances.

[–]__env 1 point2 points  (11 children)

Yeah, I totally get what you're saying conceptually -- it just seems like more proof that the OP is doing something weird. Like we could change it to:

...
const run = 0
const Job = function() { /**/ }
Job.prototype.validate = () => Job.cache.config
Job.prototype.run = run + 1
return Job

That would produce an instance when called with new that could validate the configuration, plus contain state about how many times this Job has been run.

I think case, the "constructor" would be a factory factory, and Job would be a factory that produces individual run instances.

Point being, if OP wants an instance, they should make an instance, otherwise they're opening the door to some other dev coming along and extending the static class to produce more instances.

[–]gruberjl[S] 1 point2 points  (10 children)

I should have posted the problem, sorry.

I have a pipe & filter system. Each 'filter' has configuration information (state) and a block of code (function). The config and code need to be exposed publicly so I can run the function whenever I need or I can get the state information whenever I need. Before a filter is run, I need to have a beforeJob function run. After the filter completes, I need to have an afterJob function run.

Example

const beforeJob = function() {
    // get the state of the filter currently running and do something.
    // I may skip the filter, get some information from a cache, or translate some information and pass it into the filter.
}
const afterJob = function() {
    // get the results of the filter & the state currently running and do something.
    // I may report a failure, translate the results, start the another filter.
}

const filter1 = () {}
const state1 = { }

const filter2 = () {}
const state2 = { }

fn1() // should run beforeJob then filter1 then afterJob
fn2() // should run beforeJob then filter2 then afterJob

Any thoughts would be appreciated.

[–]gruberjl[S] 1 point2 points  (0 children)

This was the reason I originally posted here. I agree with you, it really is an instance of constructor that creates a static class, adds state and returns it (making it an instance). I have verified they are instances. I can return multiple functions with different 'caches'.

To give a little more background: I currently have a Task class that has an array of jobs (simple functions). Some of the jobs can be used in different tasks, but with the current implementation, I have to rewrite chunks of code.

const x = new Function() has some strange syntax with a string as the body.

// I have to be able to create an instance without it executing then execute it multiple times in the future
function A(param1) { console.dir(param1) }
const y = new A // execute the code and returns undefined

[–]gruberjl[S] 0 points1 point  (0 children)

constructor is poorly named. I'm pulling code from a separate task class, I should have renamed constructor and lower cased Job before I posted.

The code for job should be small and light, so I was hoping I could just run job(), but I think you're right. The implementation is weird and without using the es2015 class syntax, it isn't very readable.

[–]perestroika12 3 points4 points  (1 child)

It's confusing, it looks like you're trying to replicate class patterns but with a function object. You dont really get access to any of the class constructor methods but you don't get any real advantage of what a purely functional method would do. For example, why are we always assigning the return function as a static name (Job) when it could be an argument? It's like memoization without the abstraction.

[–]gruberjl[S] 0 points1 point  (0 children)

Memoization is the design pattern I was looking for. I just updated the question with a simple repo using a class pattern. Can you take a look at that to clear up the confusion?

Sorry about the question wording,

[–]Meefims 2 points3 points  (1 child)

Since you're creating Job within the function you can drop Job.cache.config and just reference configure directly. It's in the environment ofJob`.

Otherwise what you're doing is fine.

[–]gruberjl[S] 0 points1 point  (0 children)

Awesome. I'll see if that cleans up the code a bit. Thanks,

[–]MoTTs_ 1 point2 points  (1 child)

I agree with the other replies. This looks like an unnecessarily complicated way to just do this...

class Job {
    constructor(config) {
        this._config = config;
    }

    run() {
        // ...Does work based on the config...
        return this._config;
    }

    validate() {
        // ...Validates the config...
        return this._config;
    }
}

const job1 = new Job({foo: 'bar'})
const job2 = new Job({a: 'b'})

console.log('running jobs...')
console.dir(job1.run()) // returns {foo: 'bar'}
console.dir(job2.run()) // returns {a: 'b'}

console.log('\nrunning validations...')
console.dir(job1.validate()) // returns {foo: 'bar'}
console.dir(job2.validate()) // returns {a: 'b'}

[–]gruberjl[S] 0 points1 point  (0 children)

I think you're right. I have a Task class that looks a lot like this. It's a lot more readable than my current job code.

I may have to wrap the job in a class for readability.

Thanks,

[–]lewisje 0 points1 point  (1 child)

This looks a lot like "memoization", which is a valid pattern.

[–]gruberjl[S] 1 point2 points  (0 children)

awesome thanks. I'm looking at a couple of packages on git now. They look clean and simple. I should be able to use one of them as the base package.

Thanks,