Methods on an object created via composition can't access all properties - javascript

I'm building the concept of a family for a product, with members being of different types (accountHolder, payingCustomer, student, and so on). Originally I built these as sub-classes of FamilyMember, but I ended up with some repeated code and eventually bumped into a significant problem: a student of our platform can also be the sole payingCustomer and accountHolder.
Given how object composition is widely touted as a good idea in JS, I decided to go that route. However, the methods of a particular object type (e.g. accountHolder) can't access properties of the instantiated object, if the property belong to another object type (e.g. student).
To make this more objective I've decided to replicate the behaviour using the following code:
const person = (props) => {
let state = {
name: props.name,
}
state.isOfAge = () => {
// state.isAdult is always undefined because
// isAdult doesn't exist in this object
return state.isAdult === true
}
return state
}
const adult = (props) => {
return {
isAdult: true,
}
}
const factory = (props) => {
return Object.assign({}, person(props), adult(props))
}
const john = factory({
name: 'John',
})
console.clear()
console.log(john) // { isAdult: true, name: "John", isOfAge... }
console.log(john.isOfAge()) // false
I was expecting john's method isOfAge to be able to access the property isAdult, since it's in the object. However, conceptually I understand why it doesn't work: isOfAge is a method of state, not the resulting adult instance.
If I were using classes or even a traditional prototype/constructor mechanism I knew how to make it work (e.g. attaching to prototype). With object composition I've no idea how to get there, probably due to lacking experience with FP.
Thanks for the help!

You can use this instead of state inside isOfAge. That way, the this will be deduces when the method isOfAge gets called, it will be bound to whatever object it is called on. Though, you'll have to use a regular function instead of an arrow one for that to work (arrow functions don't have a this):
const person = (props) => {
let state = {
name: props.name,
}
state.isOfAge = function() { // use a regular function
return this.isAdult === true // use this here instead of state
}
return state
}
const adult = (props) => {
return {
isAdult: true,
}
}
const factory = (props) => {
return Object.assign({}, person(props), adult(props))
}
const john = factory({
name: 'John',
})
console.log(john);
console.log(john.isOfAge()); // returns 'true' because 'this' inside 'isOfAge' will be 'john'

Object Composition
All objects made from other objects and language primitives are composite objects.
The act of creating a composite object is known as composition.
...
Concatenation composes objects by extending an existing object with new properties, e.g., Object.assign(destination, a, b), {...a, ...b}.
...                      The Hidden Treasures of Object Composition
So from your pattern and use of a factory function it looks like concatenation? The demo below is a concatenation composition. Note the parenthesis wrapped around the brackets of payment:
const payment = (status) => ({...})
this allows payment to be returned as an object instead of a function. If you have data that's a little more flexible, you'll need less methods. name: string and age: number are the properties I used considering it practical or in your case name: string and adult: boolean.
Demo
const payment = (status) => ({
adult: () => status.age > 17 ? true : false,
account: () => status.adult() ? 'holder' : 'student'
});
const member = (name, age) => {
let status = {
name,
age
};
return Object.assign(status, payment(status));
};
const soze = member('Kaiser Soze', 57);
console.log(soze);
console.log(soze.adult());
console.log(soze.account());
const jr = member('Kaiser Soze Jr.', 13);
console.log(jr);
console.log(jr.adult());
console.log(jr.account());

Related

How to recognize or identify an object made by a factory

What would be the best way to reproduce the instanceof functionality for an object made by a factory ?
Example:
class Cat {
constructor(name) {
this.name = name;
}
}
const tom = new Cat('tom');
tom instanceof Cat // true
VS
const makeCat = (name) => ({
name
})
const garfield = makeCat('garfield');
// ???
What would be the best way to identify the garfield object as a "cat" since it was made by makeCat ?
As it feels while discussing in the comments like there is no "best practice" solution for this (without using a class at least, which I don't want to), this would probably be my implementation for this purpose:
const cat = Symbol('cat');
export const makeCat = (name) => {
return {
[cat]: true,
name
}
};
export const isCat = (obj) => obj[cat];
import { makeCat, isCat } from './CatFactory.js';
const garfield = makeCat('garfield');
const tom = { cat: true, name: 'tom' };
isCat(garfield); // true
isCat(tom); // false
It's the less hacky way I found to recreate any sort of 'certainty' while trying to identify an object made by my factory and without making it possible to create a cat by mistake or even duck-typing and without polluting a console.log of the object created for example.

DeepCopy Object in JavaScript using immer

I am using immer to transform react/redux state. Can I also use immer to just deep copy an object without transforming it?
import produce, {nothing} from "immer"
const state = {
hello: "world"
}
produce(state, draft => {})
produce(state, draft => undefined)
// Both return the original state: { hello: "world"}
This is from the official immer README. Does this mean that passing an empty function to produce returns a deep copy of the original state or actually the original state?
Thank you very much for your help :)!
This is easily testable with
import { produce } from 'immer'
const state = {
hello: 'world',
}
const nextState = produce(state, draft => {})
nextState.hello = 'new world'
console.log(state, nextState)
which outputs
Object { hello: "new world" }
Object { hello: "new world" }
which means that it does NOT create a deep copy of an object.
UPDATE:
So I got interested and tested out the library a lot and here are my findings.
The code snippet I wrote above is simply an optimisation in the library which returns the old state if no changes are made. However, if you make some changes, then the library starts functioning as intended and the mutation later is made impossible. That is,
const state = {
hello: 'world',
}
const nextState = produce(state, draft => {
draft.hello = 'new world';
})
nextState.hello = 'newer world';
console.log(state, nextState)
will result in an error: TypeError: "world" is read-only
Which means that your newState is immutable and you can no longer perform mutations on it.
Another rather interesting thing I found is that immer fails when using class instances. That is,
class Cls {
prop = 10;
}
const instance = new Cls();
const obj = {
r: instance,
};
const newObj = produce(obj, draft => {
draft.r.prop = 15;
});
console.log(obj, newObj);
results in
r: Object { prop: 15 }
r: Object { prop: 15 }
So to get back to the initial question, can you get a deep copy of the initial Object by changing nothing in the draft. No you cannot, and even if you did (by changing a property created just to fool immer perhaps), the resultant cloned object will be immutable and not really helpful.
Solution :
The Immer's produce only provides a new deep cloned object on updation.
you can create your own produce function that behaves just like that of immer's produce but gives a cloned object everytime using loadash
import _ from 'lodash';
export default function produceClone(object, modifyfunction) {
let objectClone = _.cloneDeep(object);
if (!modifyfunction) return objectClone;
modifyfunction(objectClone);
return objectClone;
}
This will give you a deepCloned(or deep copied) object everytime, irrespective of whether you modify the object or not.

ES6 object cloning using spread operator is modifying input too

I have a fairly deep interface declared that looks something like this:
export interface Job {
JobId: JobId; // type JobId = string
UserId: UserId; // type UserId = string
JobName: string;
AudioFile: JobAudioFile; // this is an interface
Status: JobStatus; // this is an enum
Tracks: JobTracks[]; // 'JobTracks' is an enum
Results: JobResults; // this is an interface
Timestamps: JobTimestamps // interface
}
Most of the members of this interface are themselves interfaces, with the general architecture following this pattern of using enums, strings, arrays and more interfaces. All code is written as TypeScript, transpiled down to JS and uploaded to AWS as JS. (Node 8.10 is running on AWS)
At one point in the code, I need to make a deep copy of a Job instantiation which was passed in as a function parameter:
export const StartPipeline: Handler = async (
event: PipelineEvent
): Promise<PipelineEvent> => {
console.log('StartPipeline Event: %o', event);
const newBucket = await copyToJobsBucket$(event.Job);
await deleteFromOriginalBucket$(event.Job);
console.log(`Job [${event.Job.JobId}] moved to Jobs bucket: ${newBucket}`);
event.Job.AudioFile.Bucket = newBucket;
event.Job.Status = Types.JobStatus.Processing;
// update the job status
// VVV PROBLEM OCCURS HERE VVV
const msg: Types.JobUpdatedMessage = new Types.JobUpdatedMessage({ Job: Object.assign({}, event.Job) });
await Send.to$(event.Job.UserId, msg);
return { ...event };
};
The definition of the JobUpdatedMessage:
export class JobUpdatedMessage extends BaseMessage {
constructor(payload: { Job: Types.Job }) {
console.log('Incoming: %o', payload);
const copy: object = { ...payload.Job };
// VVV PROBLEM ON NEXT LINE VVV
const filtered = JobUtils.FilterJobProperties(copy as Types.Job);
super(MessageTypes.JobUpdated, filtered);
}
}
The problem is after the call to JobUtils.FilterJobProperties, payload.Job has also been mutated in an undesirable and unexpected way.
Here's the implementation of JobUtils.FilterJobProperties:
export const FilterJobProperties = (from: Types.Job): Types.Job => {
const fieldsToRemove: string[] = [
'Transcripts.GSTT',
'Transcripts.WSTT',
'Transcripts.ASTT',
'TranscriptTracks',
'Transcripts.Stream.File',
'Transcripts.Stream.State',
'AudioFile.Bucket',
'AudioFile.S3Key',
];
let job: Types.Job = { ...from }; // LINE ONE
fieldsToRemove.forEach(field => _.unset(job, field)); // LINE TWO
return job;
};
(I'm using the lodash library here)
The line market 'LINE TWO' is also mutating the from function parameter, even though on 'LINE ONE' I'm doing what I think is a deep clone of from.
I know that this is the case because if I change 'LINE ONE' to:
// super hard core deep cloning
let job: Types.Job = JSON.parse(JSON.stringify(from));
... everything works as expected. from is not mutated, the resulting JobUpdatedMessage is as expected, and StartPipeline's event parameter doesn't have a bunch of properties removed from event.Job.
I struggled with hours on this, including relearning everything I believed I knew about cloning objects in Es6 using the spread operator.
Why was 'LINE ONE' mutating the input as well?
Spread operator does shallow cloning same as Object.assign()
Shallow-cloning (excluding prototype) or merging of objects is now
possible using a shorter syntax than Object.assign().
Spread operator
An example to understand spread operator and shallow cloning.
let obj = { 'a': { 'b' : 1 },'c': 2}
let copy = {...obj}
copy.c = 'changes only in copy' //shallow-cloned
copy.a.b = 'changed' // still reference
console.log('original\n',obj)
console.log('\ncopy',copy)
Using spread operator object is shallow cloned so all the first level properties will become a copy while all the deeper level properties will still remain the references.
so as you see in example c property doesn't affect the original object since it is one first level depth, on the other hand b property changes affect the parent properties because it is at deep level and is still reference.

Can I use the same function names when using Object.assign() in composition?

Context
I created a new object using a composition paradigm in Javascript.
const canUpdateScore = (state) => ({
update: (spell) => state.score--
})
const mage = (name) => {
let state = {
score: 100,
}
return Object.assign(state, canUpdateScore(state));
}
scorcher = mage()
scorcher.update(); // call the update method
console.log(scorcher.score) // 99
Question
Would it be confusing to name the assiging method the same as the external function that is returning that method(example below)?, or is it better to use different names (in context above)?
const updateScore = (state) => ({
updateScore: (spell) => state.score--
})
const mage = (name) => {
let state = {
score: 100,
}
return Object.assign(state, updateScore(state));
}
scorcher = mage()
scorcher.updateScore(); // call the update method
console.log(scorcher.score) // 99
The convention for using composition is "thing-doer". Your updateScore composant is a "scoreUpdater" and it "updateScore"'s.
Would write out like this:
const scoreUpdater = (state) => ({
updateScore: (spell) => state.score--
})
Now, update on it's own a rather lousy name both for clarity reasons and for design ones as well. Imagine you thing of another composant which will update the health: healthUpdater which also implements an update method. One of them will override the other.
In general, names of properties in composants should in some way, explicitly refer to the composant itself.
canUpdateScore is definitely a poor name for an object regardless are it implies a boolean, which is not the case.

React to nested state change in Angular and NgRx

Please consider the example below
// Example state
let exampleState = {
counter: 0;
modules: {
authentication: Object,
geotools: Object
};
};
class MyAppComponent {
counter: Observable<number>;
constructor(private store: Store<AppState>){
this.counter = store.select('counter');
}
}
Here in the MyAppComponent we react on changes that occur to the counter property of the state. But what if we want to react on nested properties of the state, for example modules.geotools? Seems like there should be a possibility to call a store.select('modules.geotools'), as putting everything on the first level of the global state seems not to be good for overall state structure.
Update
The answer by #cartant is surely correct, but the NgRx version that is used in the Angular 5 requires a little bit different way of state querying. The idea is that we can not just provide the key to the store.select() call, we need to provide a function that returns the specific state branch. Let us call it the stateGetter and write it to accept any number of arguments (i.e. depth of querying).
// The stateGetter implementation
const getUnderlyingProperty = (currentStateLevel, properties: Array<any>) => {
if (properties.length === 0) {
throw 'Unable to get the underlying property';
} else if (properties.length === 1) {
const key = properties.shift();
return currentStateLevel[key];
} else {
const key = properties.shift();
return getUnderlyingProperty(currentStateLevel[key], properties);
}
}
export const stateGetter = (...args) => {
return (state: AppState) => {
let argsCopy = args.slice();
return getUnderlyingProperty(state['state'], argsCopy);
};
};
// Using the stateGetter
...
store.select(storeGetter('root', 'bigbranch', 'mediumbranch', 'smallbranch', 'leaf')).subscribe(data => {});
...
select takes nested keys as separate strings, so your select call should be:
store.select('modules', 'geotools')

Categories

Resources