JSON and OOP

JSON & OOP

Data & Object-Oriented Programming

CSCI 4513

Week 4, Lecture 8

Today's Learning Objectives

Norse Mythology Connection

Odin's Ravens - Huginn and Muninn

Huginn (thought/behavior) and Muninn (memory/data) work together, just as methods and properties combine in OOP.

The Tale:

Every morning, Odin sends two ravens across the Nine Realms. Huginn ("thought") observes patterns and relationships. Muninn ("memory") records specific events and details. At evening, both return to whisper their findings.

Huginn shares patterns, behaviors, and logical connections. Muninn recounts specific details, exact words, precise times. Together they make Odin the most informed beingβ€”neither alone would suffice. Huginn without Muninn has understanding but no facts; Muninn without Huginn has data but no comprehension.

Data + Behavior = Power

// Muninn (Memory) - Data
const ravenReport = {
    "date": "2025-01-15",
    "realm": "Midgard",
    "events": [
        { "time": "09:00", "location": "Village",
          "event": "Market day" }
    ]
};

// Huginn (Thought) - Methods + Behavior
class Raven {
    constructor(name) {
        this.name = name;
        this.observations = []; // Muninn
    }

    analyzePatterns() { // Huginn
        return this.observations.map(
            obs => this.interpret(obs));
    }

    predictOutcome(situation) { // Huginn
        return this.applyLogic(situation);
    }
}

Two halves of OOP - Data (properties) and behavior (methods) belong together in one object.

ravenReport - Pure data (JSON-like). No behavior β€” just stores facts.

class Raven - Combines data (observations) with behavior (analyzePatterns) in one structure.

constructor(name) - Runs when you create a new Raven. Sets up initial properties.

Like Odin's ravens: data alone (Muninn) or logic alone (Huginn) isn't enough β€” you need both!

Reflection Questions

Revisiting Webpack

Improving Your Workflow

Now that you've used Webpack, let's learn some tools to make setup easier and workflow more efficient!

npm Scripts

Stop Typing Long Commands!

// package.json
{
  "scripts": {
    "build": "webpack",
    "dev": "webpack serve",
    "deploy": "git subtree push --prefix dist origin gh-pages"
  }
}
# Now use these shortcuts:
npm run build    # Instead of: npx webpack
npm run dev      # Instead of: npx webpack serve
npm run deploy   # Instead of: git subtree push...

"scripts" - Key-value pairs: name β†’ shell command to execute

npm run <name> - Runs the command mapped to that script name

Why? - Shorter to type, self-documenting, and standardized across projects.

Scripts can run any shell command β€” not just npm/webpack!

Why Use npm Scripts?

πŸ’‘ Convention: npm run build for production, npm run dev for development server

Webpack Modes

Development Mode

  • Readable code
  • Source maps
  • Fast rebuilds
  • Helpful errors

Production Mode

  • Minified code
  • Optimized
  • Smaller file size
  • For deployment

Try changing mode: "development" to mode: "production" and see the difference in dist!

Multiple Config Files

Automate Mode Switching

// webpack.dev.js
module.exports = {
  mode: "development",
  devtool: "eval-source-map",
  // ... dev config
};

// webpack.prod.js
module.exports = {
  mode: "production",
  // ... production config
};
// package.json
"scripts": {
  "build": "webpack --config webpack.prod.js",
  "dev": "webpack serve --config webpack.dev.js"
}

webpack.dev.js - Readable output, source maps, fast rebuilds

webpack.prod.js - Minified, optimized, smaller output for deployment

--config - Tells webpack which config file to use instead of the default

Now npm run dev and npm run build automatically pick the right config!

Template Repositories

Stop Copying Webpack Config!

Create a repository with all your standard Webpack setup, mark it as a template, then use it as a starting point for new projects!

  1. Create a repo with your ideal Webpack setup
  2. In repo settings, check "Template repository"
  3. When creating new repos, select your template!

JSON (JavaScript Object Notation)

The Universal Data Format

JSON is a standardized, text-based format for representing structured data based on JavaScript object syntax.

JSON Syntax Rules

{
  "name": "John Doe",
  "age": 30,
  "isStudent": false,
  "courses": ["HTML", "CSS", "JavaScript"],
  "address": {
    "city": "New York",
    "zip": "10001"
  }
}

Allowed types: strings, numbers, booleans, arrays, objects, null

βœ… Keys must be in double quotes
βœ… Strings in double quotes only
βœ… No trailing commas
❌ No functions or undefined

JSON can nest objects inside objects and arrays inside arrays β€” any depth!

JSON vs JavaScript Objects

JavaScript Object

const obj = {
  name: 'John',  // No quotes on key!
  greet() {      // Functions OK
    return 'Hi';
  }
};

JSON

{
  "name": "John"  // Must quote!
  // NO functions!
}

JS objects: unquoted keys, single quotes OK, functions allowed, lives in memory

JSON: double-quoted keys, double-quoted strings, data only, it's a string

⚠️ Key distinction: JSON is a string format for transmitting data. A JS object is a live data structure in memory.

JSON.parse()

String β†’ JavaScript Object

// JSON string (maybe from an API)
const jsonString = '{"name":"John","age":30}';

// Parse it into a JavaScript object
const user = JSON.parse(jsonString);

console.log(user.name);  // "John"
console.log(user.age);   // 30

// Now we can use it like a normal object!
user.age = 31;
console.log(user.age);   // 31

JSON.parse() - Converts a JSON string into a usable JavaScript object

Before parse: It's just a string β€” you can't access .name on it. After parse: It's a real object with properties.

Mutable - Once parsed, you can modify properties just like any object.

API responses arrive as JSON strings β€” always parse before using!

JSON.stringify()

JavaScript Object β†’ String

// JavaScript object
const user = {
  name: "John",
  age: 30,
  courses: ["HTML", "CSS", "JS"]
};

// Convert to JSON string
const jsonString = JSON.stringify(user);

console.log(jsonString);
// '{"name":"John","age":30,
//   "courses":["HTML","CSS","JS"]}'

// Ready to send to an API or
// save to localStorage!

JSON.stringify() - The reverse of parse: converts an object into a JSON string

Why stringify? - APIs and localStorage only accept strings. You must convert objects before sending or saving.

Functions are dropped! - stringify skips methods and undefined values.

Pretty Printing JSON

const user = { name: "John", age: 30 };

// Compact (default)
console.log(JSON.stringify(user));
// {"name":"John","age":30}

// Pretty print with 2 spaces
console.log(JSON.stringify(user, null, 2));
// {
//   "name": "John",
//   "age": 30
// }

JSON.stringify(obj, replacer, spaces) takes three arguments:

1st: The object to convert
2nd: Replacer function (usually null)
3rd: Indentation spaces (2 or 4)

Pretty printing is great for debugging β€” much easier to read than a single line!

Common JSON Errors

// ❌ Trailing comma
{ "name": "John", }

// ❌ Single quotes
{ 'name': 'John' }

// ❌ Unquoted keys
{ name: "John" }

// ❌ Functions
{ "greet": function() {} }

// βœ… Correct!
{ "name": "John" }

JSON is stricter than JS:
❌ No trailing commas after last item
❌ Must use "double quotes" only
❌ All keys must be quoted
❌ No functions, undefined, or comments

Common cause: Writing JSON like JavaScript. Remember β€” JSON is a data format, not code!

Use jsonlint.com to validate β€” it pinpoints the exact error location!

When Do We Use JSON?

You've already used JSON in package.json!

Object-Oriented Programming

Principles for Better Design

We know HOW to create objects. Now let's learn WHEN and WHY to organize them certain ways.

Single Responsibility Principle

One Job Per Object/Function

A class/object/module should have one reason to change - it should only have one responsibility.

Bad Example: A function that checks game over AND manipulates the DOM

Violating Single Responsibility

// ❌ BAD: Does TOO MUCH
function isGameOver() {
  // Check game over logic
  if (gameOver) {
    // ALSO manipulates DOM!
    const div = document.createElement('div');
    div.classList.add('game-over');
    div.textContent = `${this.winner} won!`;
    document.body.appendChild(div);
  }
}

Two jobs in one function: checking game state AND building DOM elements.

Problems:
- Can't change UI without touching game logic
- Can't reuse game logic without the DOM code
- Harder to test either part independently

A function named "isGameOver" should only answer yes or no β€” not build HTML!

Following Single Responsibility

// βœ… GOOD: Separate concerns

// Game logic module - checks game state
function isGameOver() {
  // Just return true/false
  return gameOver;
}

// DOM module - handles UI
const DOMStuff = {
  gameOver(winner) {
    const div = document.createElement('div');
    div.classList.add('game-over');
    div.textContent = `${winner} won!`;
    document.body.appendChild(div);
  }
};

// Controller decides what to do
if (isGameOver()) {
  DOMStuff.gameOver(winner);
}

isGameOver() - One job: return true/false. Knows nothing about the DOM.

DOMStuff - One job: render UI. Knows nothing about game rules.

Controller - Connects the pieces: checks state, then tells the DOM what to show.

Now you can swap the UI (console β†’ DOM β†’ React) without changing game logic!

SOLID Principles

Five OOP Design Guidelines

πŸ’‘ Focus on S: Single Responsibility is the most important for now!

Tightly Coupled Objects

Avoid This!

Tightly coupled: Objects that depend so heavily on each other that changing one requires changing the other.

⚠️ Problem: Can't change the UI without rewriting game logic!

πŸ’‘ Goal: Objects should be as independent as possible (loosely coupled)

Loosely Coupled Design

// Game logic - knows nothing about UI
class Game {
  checkWinner() {
    return this.winner;
  }
}

// UI module - knows nothing about game internals
class UI {
  displayWinner(winner) {
    console.log(`${winner} wins!`);
  }

  // Could easily switch to DOM instead:
  // displayWinner(winner) {
  //   const div = document.createElement('div');
  //   div.textContent = `${winner} wins!`;
  //   document.body.appendChild(div);
  // }
}

// Controller connects them
const game = new Game();
const ui = new UI();
const winner = game.checkWinner();
ui.displayWinner(winner);
β”Œβ”€β”€β”€β”€β”€β”€β”       β”Œβ”€β”€β”€β”€β”
β”‚ Game │──?──▢│ UI β”‚
β””β”€β”€β”€β”€β”€β”€β”˜  β–²    β””β”€β”€β”€β”€β”˜
          β”‚
    β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
    β”‚ Controller β”‚
    β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Loosely coupled - Game and UI don't know about each other. The controller is the only connection point.

Swappable - Switch from console.log to DOM rendering by only changing the UI class β€” Game stays untouched.

Composition Over Inheritance

Favor "Has-A" Over "Is-A"

Inheritance (Is-A)

class Animal {
  move() {}
}

class Dog extends Animal {
  // Dog IS-A Animal
  bark() {}
}

Composition (Has-A)

const movement = {
  move() {}
};

const dog = {
  // Dog HAS-A movement
  ...movement,
  bark() {}
};

extends - Inheritance: Dog gets everything from Animal's prototype chain

...movement - Composition: spread copies behaviors into the object directly

Composition wins - Mix multiple behaviors freely. No rigid hierarchy to maintain.

A flying fish can have both swimming and flying behaviors β€” no awkward class tree needed!

Why Favor Composition?

Example: A flying fish doesn't fit cleanly in a Fish→Bird hierarchy, but it can have swimming AND flying behaviors!

Project: Todo List

Apply Everything You've Learned!

Project Goals

  • πŸ—οΈ Use OOP principles
  • πŸ“¦ Organize with modules
  • 🎨 Create projects/categories
  • πŸ’Ύ Use localStorage for persistence

Todo List Features

Todo Object Structure

// Use factory function or class
class Todo {
  constructor(title, description, dueDate, priority) {
    this.title = title;
    this.description = description;
    this.dueDate = dueDate;
    this.priority = priority;  // 'low', 'medium', 'high'
    this.notes = '';
    this.checklist = [];
    this.completed = false;
  }

  toggleComplete() {
    this.completed = !this.completed;
  }
}

constructor - Parameters set up each todo's initial state. Some properties have default values.

completed = false - Defaults: new todos start incomplete, with empty notes and checklist.

toggleComplete() - Flips boolean: !false β†’ true, !true β†’ false

Data (properties) + behavior (methods) together β€” that's OOP in action!

Project Organization

class Project {
  constructor(name) {
    this.name = name;
    this.todos = [];
  }

  addTodo(todo) {
    this.todos.push(todo);
  }

  removeTodo(todoId) {
    this.todos = this.todos.filter(
      t => t.id !== todoId);
  }
}

// Default project on app start
const defaultProject = new Project("My Todos");

this.todos = [] - Each project holds its own array of Todo objects

addTodo / removeTodo - Methods to manage the collection. Logic stays inside the class.

filter() - Returns a new array with only items that pass the test (keeps non-matching IDs)

Create a default project on startup so users have somewhere to add todos immediately.

Separate Logic from DOM!

Module Organization

  • todoApp.js - Application logic (create, delete, update todos)
  • domController.js - DOM manipulation (render, UI updates)
  • storage.js - localStorage handling
  • index.js - Entry point, connects everything

localStorage for Persistence

// Save todos to localStorage
function saveTodos(projects) {
  const json = JSON.stringify(projects);
  localStorage.setItem('todos', json);
}

// Load todos from localStorage
function loadTodos() {
  const json = localStorage.getItem('todos');
  if (!json) return [];  // No data?

  const projects = JSON.parse(json);
  // Re-add methods to objects!
  return projects.map(p =>
    Object.assign(new Project(), p));
}

setItem(key, value) - Stores a string in the browser. Persists across page reloads.

getItem(key) - Retrieves the string. Returns null if nothing was stored.

Object.assign(new Project(), p) - Creates a real Project instance and copies parsed data into it, restoring methods!

⚠️ JSON loses methods! You must re-create class instances after parsing.

localStorage Gotchas

⚠️ Important Notes

  • localStorage only stores strings (use JSON)
  • Functions aren't stored - re-add methods after parsing
  • Data only accessible on same computer/browser
  • Check if data exists before trying to use it!
πŸ’‘ DevTools: Inspect localStorage in Application tab!

Using date-fns Library

# Install date-fns
npm install date-fns
// Use date-fns for formatting
import { format, isToday, isPast } from 'date-fns';

const todo = {
  dueDate: new Date(2025, 0, 15)
};

// Format date nicely
console.log(format(todo.dueDate, 'MMM dd, yyyy'));
// "Jan 15, 2025"

// Check if overdue
if (isPast(todo.dueDate)) {
  console.log('Overdue!');
}

date-fns - A regular dependency (not --save-dev) because it runs in production

format(date, pattern) - 'MMM dd, yyyy' β†’ "Jan 15, 2025". Tokens control the output.

isPast() / isToday() - Utility functions that return true/false for date comparisons

Import only what you need β€” Webpack tree-shakes the rest away!

Implementation Tips

  1. Start with data structure (Todo, Project classes)
  2. Build app logic without DOM first
  3. Test in console.log before adding UI
  4. Create DOM module separately
  5. Add localStorage last
  6. Use unique IDs for todos (crypto.randomUUID())
πŸ’‘ Apply Single Responsibility: Keep logic and UI separate!

UI Requirements

Todo App Inspiration

Check Out These Apps:

  • πŸ“± Todoist - Clean, professional design
  • 🎨 Things - Beautiful UI
  • ⚑ any.do - Simple and effective

Look at screenshots and videos for UI ideas - don't copy exactly, but get inspired!

Today's Takeaways

You Can Now:

πŸ“… Next Class: JS in the Real World & Catch-up Lab

πŸ“ Homework: To-do List