austin1244 / MI-449-SS17-740-js-objects-T4W3KQ

0 stars 0 forks source link

Project Feedback #1

Open austin1244 opened 7 years ago

austin1244 commented 7 years ago

@chrisvfritz Can you take a look at this? It's hosted here and meets the following criteria:

I added a new property to store the jokes like you showed me in the previous module. The only thing I did not like about my solution is any reference to the variable now has to load from local memory every time. I'm sure the difference is minuscule, especially in a small script like this, but in terms of clock cycles, a cache reference compared to a ram access is in the order of 3x less efficient. Which in the case of a event driven program, you are waiting on input, so it's practically 0x and doesn't really matter. I guess the way to solve it in this case would be to store it in a temp variable. Which is another thing I noticed. The defined property does not reference the object. The property instead calls the object copy constructor which would make the solution feasible. Anyways, probably just rambling at this point, but I thought i would explain some of the decisions.

Also, is it ok if I use code comments to explain some design decisions or should I only comment on the functionality.

chrisvfritz commented 7 years ago

Great work here! 🎉 🎉 🎉

Responding to comments

I added a new property to store the jokes like you showed me in the previous module. The only thing I did not like about my solution is any reference to the variable now has to load from local memory every time. I'm sure the difference is minuscule, especially in a small script like this, but in terms of clock cycles, a cache reference compared to a ram access is in the order of 3x less efficient. Which in the case of a event driven program, you are waiting on input, so it's practically 0x and doesn't really matter. I guess the way to solve it in this case would be to store it in a temp variable.

That assessment is 💯% correct! But while we're on the topic, there's actually a pair of utilities you can use for simple benchmarking in JavaScript: console.time and console.timeEnd. For example, to compare a cached vs non-cached version of your app:

jokes.stored = {
  foo: {
    setup: 'baoiear stoiear stoien arstoien aioers tr',
    punchline: 'baoiear stoia rstoiean rstoien arosietz'
  }
}

console.time('reads without cache')
for (var i = 0; i < 10000; i++) {
  jokes.stored
}
console.timeEnd('reads without cache')

var cachedJokes

console.time('reads with cache')
for (var i = 0; i < 10000; i++) {
  if (cachedJokes) {
    cachedJokes
  } else {
    cachedJokes = jokes.stored
    cachedJokes
  }
}
console.timeEnd('reads with cache')

In the console, you'd then see something like this:

reads without cache: 80.077ms
reads with cache: 0.597ms

Those are actual times I just measured. The non-cached version is much slower in this extreme case of fetching jokes 10,000 times in a row. And what's worse, operations with localStorage are actually blocking, meaning the browser window will just freeze up while it's working. Even at 80ms though, this would probably still be tolerable performance with that many fetches.

Variation between JavaScript engines

Due to the way JavaScript engines work, these times are also likely to vary a fair amount - probably between 40ms and 150ms in this case, though it should remain less than 1ms for the cached version. Different engines also optimize in different ways, so you'll get different results depending on your browser.

Why use a for loop in benchmarks?

There are a lot of different ways to perform loops in JavaScript. For example, recursive functions and Array.prototype.forEach, which I usually recommend using in application code, since it reads nicer to most people. All of these have different performance characteristics though and also different levels of performance stability.

While for loops aren't the nicest to look at, they're exceptionally low overhead and their performance is more stable than many other looping methods, so I favor them in performance-critical scenarios like this.

Testing different edge cases

In the case of our app, 10,000 reads in a row is unlikely to ever happen - but if it gets really complex, maybe we could get to 100 at a time? Since users input their own jokes however, it could be more difficult to predict how many they might want to save. A pretty extreme case might be 5000 jokes, so let's load up our app with 5000 jokes:

var testJokes = {}
for (var i = 0; i < 5000; i++) {
  testJokes[Math.random().toString()] = {
    setup: 'baoiear stoiear stoien arstoien aioers tr',
    punchline: 'baoiear stoia rstoiean rstoien arosietz'
  }
}
jokes.stored = testJokes

Then run the same test for 100 reads in a row:

reads without cache: 1656.112ms
reads with cache: 11.963ms

Ouch - now we're getting into dangerous territory. At this point, rendering the UI would still be an even bigger bottleneck, not to mention a usability nightmare the way it's currently designed. So much scrolling. 😣

When to write a benchmark

I think in cases like this, it's definitely worth doing a very quick benchmark if you're not familiar with the performance of a particular feature. If you find some low-hanging fruit that's easy to implement, then I usually go for the optimization. In this particular app, there's only one way new data is added or removed, respectively, so invalidating/updating the cache reliably wouldn't be too hard.

Application code vs library code

While we're talking about performance and other edge cases, the times I pay the most attention to these are in library code (rather than application code), where you're building a tool that will be used on many different projects and it's much more difficult to predict exactly how.

Since we're focused more on applications in our projects, I just put together a more generic localStorage adapter to give you a taste of some differences when working on library code. I added a bunch of comments, but if you have any questions, please don't hesitate. I hope it's helpful! 🙂

The defined property does not reference the object. The property instead calls the object copy constructor which would make the solution feasible.

Yep, right again! Nice observation. 😄

Is it ok if I use code comments to explain some design decisions or should I only comment on the functionality.

Great question. I personally don't mind - as long as it doesn't get so cluttered that it's difficult to read the program.

Project feedback

Now for some areas for improvement:

Complex getter for jokes.stored

I think this getter might be a little overcomplicated:

var jokes = window.localStorage.getItem('jokes')
try {
  jokes = JSON.parse(jokes) || {}
  if (typeof (jokes) !== 'object') {
    throw new JokeException('Invalid Object')
  }
} catch (e) {
  // set local storage with empty object on failed parse Exception
  // or Invalid Object Exception
  jokes = {}
  window.localStorage.setItem('jokes', JSON.stringify(jokes))
}
return jokes

Altogether, these optimizations would shorten the code to just:

var jokes = JSON.parse(window.localStorage.getItem('jokes'))
if (typeof jokes !== 'object') {
  jokes = {}
}
return jokes

Upgrading the generic adapter

Try incorporating my generic adapter into your code, using it as you might a library, with the generic code in a separate file loaded before your app. After that, I want you to upgrade it. 🙂

1. Checking for a valid object on writes

With localStorage data privately scoped in the generic adapter, the only way that we're ever likely to get a non-object is when the user writes to a resource. Right now though, there's zero validation. To fix this, create a new isObject helper function that we can use in the setter to if the new value we're given is really an object, then abort with a custom error if it's not.

Note that there's a little gotcha with typeof myVar !== 'object': it will also return true for arrays, since arrays are really a subtype of objects. There are a few different ways to check that this object is a plain object, not an array, but I'll let you do some research. 😄

2. Resetting data for all resources

For the generic adapter, let's say we found many users have a need to reset all the saved resources for an app. They want to be able to run data.$reset(), then any initialized resources will be removed from localStorage and the cache, with no trace of their existence.

Once you've built this into the adapter, use this upgrade to add a new Reset app button in the jokes app.

Note about in operator

This is not something you were expected to know and I'm not sharing this extra information with most students, but a quick note about the in operator, like used here:

requestedJokeKey in jokes.stored

That also looks at properties inherited from the prototype chain, meaning this:

'toString' in {}

Will return true. So in your app, if you look for a toString joke, it should actually display:

undefined

undefined

An alternative, which will only check for locally defined properties, is the hasOwnProperty method:

jokes.stored.hasOwnProperty(requestedJokeKey)

Updating the entire UI when data changes

Right now, you're just calling updateJokesMenu when adding or removing a joke. That means if I request a joke called foo, realize it's not there and add it, I'll have to update my requested joke to something else, then change it back to foo in order to see the new joke that was just created.

Try using the updatePage function instead. One of the advantages is that we don't have to think about where inconsistencies might arise - we can just call it and guarantee that the UI will be in sync with the latest data. With the amount of data we're dealing with in our app, I think the very slight performance hit is probably worth the increased predictability.

Clearing inputs after submitting data

After adding or removing a joke, it's typically best practice to clear the associated inputs, as it's unlikely they'll want to submit with the same data twice.


I kept smiling looking at your code. 🙂 Keep up the great work here! I can see you pushing yourself and trying new things, which the exact right strategy for growing your skills to the max. 💥 ⚡️ 💥

austin1244 commented 7 years ago

@chrisvfritz I fixed the issues and implemented it with your adapter (app2.js). It finished it a couple weeks ago, but did not have time to submit. I got slammed with homework and midterms from my other CSE classes. Also, surge was giving me some problems, so I hope everything transferred this time. This was definitely more challenging and I appreciate you taking the time to make it. One note about my code: I implemented two reset functions. One the resets just removed the loaded data from local storage, and the other removes all possible data in local storage. I only have one questions that is not really related.

egillespie commented 7 years ago

Hi @austin1244, I'm jumping in for Chris. I like what you've got here and I'll second what Chris said about pushing yourself and trying new things. Taking a project beyond the basic requirements to apply something new is a solid way to keep challenging yourself and advancing your knowledge. I've found it to be a good way to learn new libraries and languages in a more practical way than basic "hello world" examples. 🤘

Re: ES6 Classes

I have a strong background in object-oriented programming and can see the appeal of classes. My first attempts to create my own types in JavaScript using prototypes were pretty tough. If classes had been around, they would have been sufficient for what I needed, but I probably would not have learned as much about what's going on "behind the curtain."

So I feel that classes are a great way for some developers to make the transition to JavaScript, but they can also be a crutch. I think it's inevitable that JS developers will have to use functions such as hasOwnProperty at some point, so it will eventually become apparent that classes are just a more convenient way of doing something that has always been in JavaScript.

Of course, I'm saying this as a type of developer who spends more time writing functionality for websites than I do creating data types. There are organizations and projects that spend much more time working with data in ways that make it important to easily create and extend types. I can see how classes would be appealing to them. Classes are easy to write and I think they are also easy to enforce patterns that ensure high standards of quality.

Does that help? Is there anything else you are hoping to accomplish or try with this project that I can help with?

austin1244 commented 7 years ago

@egillespie Yes, that helps. Thanks for the explanation. I think that's all I was hoping to accomplish. Is there any thing I am supposed to change? It still says Changes Requested?

egillespie commented 7 years ago

Nope, you don't have to make any changes. I just wanted to make sure you got what you wanted out of this project before closing it. :)

:shipit: