diff --git a/exercises/01.classes/01.problem.class-basics/README.mdx b/exercises/01.classes/01.problem.class-basics/README.mdx index e76e8fa..20e60b0 100644 --- a/exercises/01.classes/01.problem.class-basics/README.mdx +++ b/exercises/01.classes/01.problem.class-basics/README.mdx @@ -7,9 +7,37 @@ system. 🐨 Open and: -1. Create a `Product` class with fields and methods -2. Create a `ShoppingCart` class that holds products -3. Instantiate and use these classes +1. Create and export a `Product` class with: + - Public fields: `name` (`string`), `price` (`number`) + - A constructor that sets both from parameters `(name, price)` + - A `getDescription()` method that returns a string in this exact form: + `Product: {name} - ${price}` (include the dollar sign before the price) +2. Create and export a `ShoppingCart` class with: + - A public `items` field typed as `Array` + - New carts start with `items` as an empty array + - `addItem(product: Product)` that appends the product to `items` + - `getTotal()` that returns the sum of every item's `price` +3. Export both classes: `export { Product, ShoppingCart }` + +## Fixtures and success criteria + +With these fixtures: + +```ts +const laptop = new Product('Laptop', 999.99) +const mouse = new Product('Mouse', 29.99) +const cart = new ShoppingCart() +cart.addItem(laptop) +cart.addItem(mouse) +``` + +you should observe: + +- `laptop.name === 'Laptop'` and `laptop.price === 999.99` +- `laptop.getDescription() === 'Product: Laptop - $999.99'` +- A fresh cart starts with `items.length === 0` +- After the two `addItem` calls above, `items` has length `2` in insertion order +- `cart.getTotal() === 1029.98` πŸ’° A class includes fields, a constructor, and methods. diff --git a/exercises/01.classes/01.problem.class-basics/index.ts b/exercises/01.classes/01.problem.class-basics/index.ts index 1ed717c..1da8eb5 100644 --- a/exercises/01.classes/01.problem.class-basics/index.ts +++ b/exercises/01.classes/01.problem.class-basics/index.ts @@ -2,26 +2,29 @@ // 🐨 Create a Product class with: // - Fields: name (string), price (number) -// - Constructor to initialize both -// - Method: getDescription() returns "Product: {name} - ${price}" +// - Constructor(name, price) that initializes both +// - Method: getDescription() returns exactly: +// "Product: {name} - ${price}" +// Example: new Product('Laptop', 999.99).getDescription() +// β†’ "Product: Laptop - $999.99" -// Test Product +// Optional smoke test: // const laptop = new Product('Laptop', 999.99) // const mouse = new Product('Mouse', 29.99) -// console.log(laptop) -// console.log(mouse) +// console.log(laptop.getDescription()) +// console.log(mouse.getDescription()) // 🐨 Create a ShoppingCart class with: -// - Field: items (Array) -// - Constructor to initialize empty items array -// - Method: addItem(product: Product) adds to items -// - Method: getTotal() returns sum of all product prices +// - Field: items (Array), starts empty +// - Method: addItem(product: Product) appends to items +// - Method: getTotal() returns the sum of all item prices +// Example: laptop (999.99) + mouse (29.99) β†’ 1029.98 -// Test ShoppingCart +// Optional smoke test: // const cart = new ShoppingCart() // cart.addItem(laptop) // cart.addItem(mouse) -// console.log(cart.getTotal()) -// console.log(cart) +// console.log(cart.getTotal()) // 1029.98 +// 🐨 Export both classes // export { Product, ShoppingCart } diff --git a/exercises/01.classes/01.solution.class-basics/index.test.ts b/exercises/01.classes/01.solution.class-basics/index.test.ts index 2969790..328e8e8 100644 --- a/exercises/01.classes/01.solution.class-basics/index.test.ts +++ b/exercises/01.classes/01.solution.class-basics/index.test.ts @@ -35,7 +35,7 @@ await test('Product getDescription should return formatted string', () => { assert.strictEqual( sampleLaptop.getDescription(), 'Product: Laptop - $999.99', - '🚨 getDescription() should return "Product: Laptop - $999.99" - check your method implementation and formatting', + '🚨 getDescription() should return "Product: Laptop - $999.99"', ) }) @@ -89,6 +89,6 @@ await test('ShoppingCart getTotal should calculate sum of all item prices', () = assert.strictEqual( sampleCart.getTotal(), 1029.98, - '🚨 getTotal() should return 1029.98 (sum of 999.99 + 29.99) - check your method implementation and price calculation', + '🚨 getTotal() should return 1029.98 for Laptop (999.99) + Mouse (29.99)', ) }) diff --git a/exercises/01.classes/02.problem.private-fields-and-defaults/README.mdx b/exercises/01.classes/02.problem.private-fields-and-defaults/README.mdx index af78d2f..1788104 100644 --- a/exercises/01.classes/02.problem.private-fields-and-defaults/README.mdx +++ b/exercises/01.classes/02.problem.private-fields-and-defaults/README.mdx @@ -53,11 +53,48 @@ const newCar = new Car('Toyota', 'Camry') // year defaults to 2024 const oldCar = new Car('Ford', 'Mustang', 1965) // year explicitly set ``` -🐨 Open and: +🐨 Open and create/export three classes: -1. Use `#` prefix to create private fields -2. Use default parameter values in constructors -3. Create classes that encapsulate their internal state +### `User` + +- Public fields: `name`, `email`, `role` (all `string`) +- Constructor `(name, email, role = 'user')` +- Omitting `role` must leave it as `'user'` + +### `BankAccount` + +- Public field: `accountNumber` (`string`) +- A private balance field (use `#` so it is inaccessible outside the class) +- New accounts start with balance `0` +- `deposit(amount: number)` increases the balance by `amount` (it accumulates) +- `getBalance()` returns the current balance + +### `Config` + +- Public fields: `host` (`string`), `port` (`number`), `debug` (`boolean`) +- Constructor defaults: `host = 'localhost'`, `port = 3000`, `debug = false` +- `new Config()` must use all three defaults; custom args override them + +Export all three: `export { User, BankAccount, Config }` + +## Fixtures and success criteria + +```ts +const user = new User('Alice', 'alice@example.com') +const admin = new User('Bob', 'bob@example.com', 'admin') +const account = new BankAccount('12345') +account.deposit(100) +account.deposit(50) +const config = new Config() +const customConfig = new Config('example.com', 8080, true) +``` + +- `user.role === 'user'` and `admin.role === 'admin'` +- `account.accountNumber === '12345'` +- A fresh account has `getBalance() === 0` +- After the deposits above, `getBalance() === 150` +- Default config: `host === 'localhost'`, `port === 3000`, `debug === false` +- Custom config: `'example.com'`, `8080`, `true` πŸ’° Use `#` to declare a private fieldβ€”it's truly private, not just a convention. diff --git a/exercises/01.classes/02.problem.private-fields-and-defaults/index.ts b/exercises/01.classes/02.problem.private-fields-and-defaults/index.ts index 1f35ad1..d9e400b 100644 --- a/exercises/01.classes/02.problem.private-fields-and-defaults/index.ts +++ b/exercises/01.classes/02.problem.private-fields-and-defaults/index.ts @@ -1,39 +1,39 @@ // Private Fields and Defaults -// 🐨 Create a User class with these fields: +// 🐨 Create a User class with: // - name: string // - email: string -// - role: string (default: 'user') +// - role: string (default: 'user' when omitted) // Initialize fields in the constructor -// Test User +// Optional smoke test: // const user = new User('Alice', 'alice@example.com') // const admin = new User('Bob', 'bob@example.com', 'admin') -// console.log(user) -// console.log(admin) +// console.log(user.role) // 'user' +// console.log(admin.role) // 'admin' // 🐨 Create a BankAccount class with: // - accountNumber: string -// - #balance: number (private field, default: 0) -// - Method: deposit(amount: number) -// - Method: getBalance() returns the balance -// πŸ’° #balance is a private field - can only be accessed inside the class - -// Test BankAccount +// - a private balance field using # (starts at 0) +// - deposit(amount: number) increases the balance by amount +// - getBalance() returns the current balance +// πŸ’° Private fields can only be read/written inside the class +// +// Example: // const account = new BankAccount('12345') +// account.getBalance() // 0 // account.deposit(100) -// console.log(account) -// console.log(account.getBalance()) - -// 🐨 Create a Config class with optional default values: -// - host: string (default: 'localhost') -// - port: number (default: 3000) -// - debug: boolean (default: false) +// account.deposit(50) +// account.getBalance() // 150 -// Test Config -// const config = new Config() -// const customConfig = new Config('example.com', 8080, true) -// console.log(config) -// console.log(customConfig) +// 🐨 Create a Config class with constructor defaults: +// - host: string = 'localhost' +// - port: number = 3000 +// - debug: boolean = false +// +// Example: +// new Config() β†’ localhost / 3000 / false +// new Config('example.com', 8080, true) β†’ those custom values +// 🐨 Export all three classes // export { User, BankAccount, Config } diff --git a/exercises/01.classes/02.solution.private-fields-and-defaults/index.test.ts b/exercises/01.classes/02.solution.private-fields-and-defaults/index.test.ts index a07a64b..22171ea 100644 --- a/exercises/01.classes/02.solution.private-fields-and-defaults/index.test.ts +++ b/exercises/01.classes/02.solution.private-fields-and-defaults/index.test.ts @@ -84,12 +84,12 @@ await test('BankAccount deposit should increase balance', () => { assert.strictEqual( balanceAfterFirstDeposit, 100, - '🚨 After depositing 100, getBalance() should return 100 - check your deposit method implementation', + '🚨 After deposit(100), getBalance() should return 100', ) assert.strictEqual( sampleAccount.getBalance(), 150, - '🚨 After depositing another 50, getBalance() should return 150 - check your deposit method accumulates correctly', + '🚨 After deposit(100) then deposit(50), getBalance() should return 150 (deposits accumulate)', ) }) diff --git a/exercises/02.interfaces-and-classes/01.problem.implementing-interfaces/README.mdx b/exercises/02.interfaces-and-classes/01.problem.implementing-interfaces/README.mdx index 67b5b07..227621c 100644 --- a/exercises/02.interfaces-and-classes/01.problem.implementing-interfaces/README.mdx +++ b/exercises/02.interfaces-and-classes/01.problem.implementing-interfaces/README.mdx @@ -7,10 +7,33 @@ interfaces to ensure all payment methods follow the same contract. 🐨 Open and: -1. Create a `PaymentMethod` interface with a `pay(amount: number)` method -2. Create a `CreditCard` class that implements `PaymentMethod` -3. Create a `PayPal` class that implements `PaymentMethod` -4. Each class should have its own implementation of `pay()` +1. Create a `PaymentMethod` interface with: + - `pay(amount: number): string` +2. Create and export a `CreditCard` class that `implements PaymentMethod`: + - Public field: `cardNumber` (`string`) + - Constructor `(cardNumber: string)` + - `pay(amount)` returns exactly: + `Paid $${amount} with credit card ${cardNumber}` +3. Create and export a `PayPal` class that `implements PaymentMethod`: + - Public field: `email` (`string`) + - Constructor `(email: string)` + - `pay(amount)` returns exactly: + `Paid $${amount} with PayPal ${email}` +4. Export the classes: `export { CreditCard, PayPal }` + (`PaymentMethod` itself does not need to be exported) + +## Fixtures and success criteria + +```ts +const creditCard = new CreditCard('1234-5678-9012-3456') +const paypal = new PayPal('user@example.com') +``` + +- `creditCard.cardNumber === '1234-5678-9012-3456'` +- `creditCard.pay(100) === 'Paid $100 with credit card 1234-5678-9012-3456'` +- `paypal.email === 'user@example.com'` +- `paypal.pay(50) === 'Paid $50 with PayPal user@example.com'` +- Both instances expose a callable `pay` method πŸ’° Classes that implement an interface must define all its methods. diff --git a/exercises/02.interfaces-and-classes/01.problem.implementing-interfaces/index.ts b/exercises/02.interfaces-and-classes/01.problem.implementing-interfaces/index.ts index 7b32ebe..0dd6412 100644 --- a/exercises/02.interfaces-and-classes/01.problem.implementing-interfaces/index.ts +++ b/exercises/02.interfaces-and-classes/01.problem.implementing-interfaces/index.ts @@ -1,26 +1,31 @@ // Implementing Interfaces // 🐨 Create a PaymentMethod interface with: -// - Method: pay(amount: number) returns string +// - pay(amount: number): string // 🐨 Create a CreditCard class that implements PaymentMethod: // - Field: cardNumber (string) -// - Constructor that takes cardNumber -// - Implement pay(amount: number) returns "Paid $${amount} with credit card ${cardNumber}" +// - Constructor(cardNumber) +// - pay(amount) returns exactly: +// "Paid $${amount} with credit card ${cardNumber}" +// Example: new CreditCard('1234-5678-9012-3456').pay(100) +// β†’ "Paid $100 with credit card 1234-5678-9012-3456" -// Test CreditCard +// Optional smoke test: // const creditCard = new CreditCard('1234-5678-9012-3456') // console.log(creditCard.pay(100)) -// console.log(creditCard) // 🐨 Create a PayPal class that implements PaymentMethod: // - Field: email (string) -// - Constructor that takes email -// - Implement pay(amount: number) returns "Paid $${amount} with PayPal ${email}" +// - Constructor(email) +// - pay(amount) returns exactly: +// "Paid $${amount} with PayPal ${email}" +// Example: new PayPal('user@example.com').pay(50) +// β†’ "Paid $50 with PayPal user@example.com" -// Test PayPal +// Optional smoke test: // const paypal = new PayPal('user@example.com') // console.log(paypal.pay(50)) -// console.log(paypal) +// 🐨 Export the classes (PaymentMethod does not need to be exported) // export { CreditCard, PayPal } diff --git a/exercises/02.interfaces-and-classes/01.solution.implementing-interfaces/index.test.ts b/exercises/02.interfaces-and-classes/01.solution.implementing-interfaces/index.test.ts index 7536967..160d00e 100644 --- a/exercises/02.interfaces-and-classes/01.solution.implementing-interfaces/index.test.ts +++ b/exercises/02.interfaces-and-classes/01.solution.implementing-interfaces/index.test.ts @@ -26,7 +26,7 @@ await test('CreditCard should implement PaymentMethod interface', () => { assert.strictEqual( creditCard.pay(100), 'Paid $100 with credit card 1234-5678-9012-3456', - '🚨 pay() should return "Paid $100 with credit card 1234-5678-9012-3456" - check your PaymentMethod interface implementation', + '🚨 CreditCard.pay(100) should return "Paid $100 with credit card 1234-5678-9012-3456"', ) }) @@ -40,7 +40,7 @@ await test('PayPal should implement PaymentMethod interface', () => { assert.strictEqual( paypal.pay(50), 'Paid $50 with PayPal user@example.com', - '🚨 pay() should return "Paid $50 with PayPal user@example.com" - check your PaymentMethod interface implementation', + '🚨 PayPal.pay(50) should return "Paid $50 with PayPal user@example.com"', ) }) diff --git a/exercises/02.interfaces-and-classes/02.problem.programming-to-abstractions/README.mdx b/exercises/02.interfaces-and-classes/02.problem.programming-to-abstractions/README.mdx index e571f9a..56bc954 100644 --- a/exercises/02.interfaces-and-classes/02.problem.programming-to-abstractions/README.mdx +++ b/exercises/02.interfaces-and-classes/02.problem.programming-to-abstractions/README.mdx @@ -6,11 +6,30 @@ This is called "programming to abstractions"β€”using interfaces instead of concrete classes. +The `PaymentMethod` interface plus `CreditCard` and `PayPal` are already +defined in . You only need to add +`processPayment`. + 🐨 Open and: -1. Create a `processPayment` function that accepts a `PaymentMethod` parameter -2. The function should call `method.pay(amount)` and return the result -3. Test it with both `CreditCard` and `PayPal` instances +1. Create a `processPayment` function with this contract: + - Parameters: `method: PaymentMethod`, `amount: number` + - Return type: `string` + - Behavior: return the payment result string for that method and amount +2. Type the first parameter as `PaymentMethod` (not `CreditCard` or `PayPal`) +3. Export it with the existing classes: + `export { CreditCard, PayPal, processPayment }` + +## Fixtures and success criteria + +```ts +const creditCard = new CreditCard('1234-5678-9012-3456') +const paypal = new PayPal('user@example.com') +``` + +- `processPayment(creditCard, 100) === 'Paid $100 with credit card 1234-5678-9012-3456'` +- `processPayment(paypal, 50) === 'Paid $50 with PayPal user@example.com'` +- The same function works for both implementations without special-casing πŸ’° Use the interface type (`PaymentMethod`) for the parameter instead of a concrete class type. This way, the function works with any class that diff --git a/exercises/02.interfaces-and-classes/02.problem.programming-to-abstractions/index.ts b/exercises/02.interfaces-and-classes/02.problem.programming-to-abstractions/index.ts index 4098bac..9292080 100644 --- a/exercises/02.interfaces-and-classes/02.problem.programming-to-abstractions/index.ts +++ b/exercises/02.interfaces-and-classes/02.problem.programming-to-abstractions/index.ts @@ -29,11 +29,17 @@ class PayPal implements PaymentMethod { } // 🐨 Create a processPayment function: -// - Parameters: method (PaymentMethod), amount (number) -// - Returns: string -// - Calls method.pay(amount) - -// Test processPayment +// - Parameters: method: PaymentMethod, amount: number +// - Returns: the payment result string for that method and amount +// - Type the first parameter as PaymentMethod, not a concrete class +// +// Expected results (using the classes above): +// processPayment(new CreditCard('1234-5678-9012-3456'), 100) +// β†’ "Paid $100 with credit card 1234-5678-9012-3456" +// processPayment(new PayPal('user@example.com'), 50) +// β†’ "Paid $50 with PayPal user@example.com" + +// Optional smoke test: // const creditCard = new CreditCard('1234-5678-9012-3456') // const paypal = new PayPal('user@example.com') // console.log(processPayment(creditCard, 100)) diff --git a/exercises/03.inheritance-and-polymorphism/01.problem.extends/README.mdx b/exercises/03.inheritance-and-polymorphism/01.problem.extends/README.mdx index e046970..5a56071 100644 --- a/exercises/03.inheritance-and-polymorphism/01.problem.extends/README.mdx +++ b/exercises/03.inheritance-and-polymorphism/01.problem.extends/README.mdx @@ -7,10 +7,27 @@ it with specific shapes like `Circle` and `Rectangle`. 🐨 Open and: -1. Create a `Shape` base class with a `color` field -2. Create a `Circle` class that extends `Shape` -3. Add a `radius` field to `Circle` -4. Create a `Rectangle` class that extends `Shape` -5. Add `width` and `height` fields to `Rectangle` +1. Create a `Shape` base class with: + - Public field: `color` (`string`) + - Constructor `(color: string)` +2. Create a `Circle` class that `extends Shape` with: + - Public field: `radius` (`number`) + - Constructor `(color, radius)` that initializes the parent via `super(color)` +3. Create a `Rectangle` class that `extends Shape` with: + - Public fields: `width` (`number`), `height` (`number`) + - Constructor `(color, width, height)` that calls `super(color)` +4. Export all three: `export { Shape, Circle, Rectangle }` + +## Fixtures and success criteria + +```ts +const circle = new Circle('red', 5) +const rectangle = new Rectangle('blue', 10, 20) +``` + +- `circle.color === 'red'` and `circle.radius === 5` +- `rectangle.color === 'blue'`, `width === 10`, `height === 20` +- `circle instanceof Shape` and `rectangle instanceof Shape` are both `true` + (each subclass is substitutable as a `Shape`) πŸ“œ [TypeScript Handbook - Inheritance](https://www.typescriptlang.org/docs/handbook/2/classes.html#inheritance) diff --git a/exercises/03.inheritance-and-polymorphism/01.problem.extends/index.ts b/exercises/03.inheritance-and-polymorphism/01.problem.extends/index.ts index 43bd8f6..7565e5e 100644 --- a/exercises/03.inheritance-and-polymorphism/01.problem.extends/index.ts +++ b/exercises/03.inheritance-and-polymorphism/01.problem.extends/index.ts @@ -2,28 +2,30 @@ // 🐨 Create a Shape base class with: // - Field: color (string) -// - Constructor that takes color +// - Constructor(color) // 🐨 Create a Circle class that extends Shape: // - Field: radius (number) -// - Constructor that takes color and radius -// - Call super(color) to initialize parent +// - Constructor(color, radius) that calls super(color) +// +// Example: new Circle('red', 5) +// β†’ color 'red' (from Shape), radius 5 +// β†’ instanceof Shape === true -// Test Circle +// Optional smoke test: // const circle = new Circle('red', 5) -// console.log(circle.color) // βœ… Should work - inherited from Shape -// console.log(circle.radius) // βœ… Should work - defined in Circle -// console.log(circle) +// console.log(circle.color, circle.radius) // 🐨 Create a Rectangle class that extends Shape: // - Fields: width (number), height (number) -// - Constructor that takes color, width, and height -// - Call super(color) to initialize parent +// - Constructor(color, width, height) that calls super(color) +// +// Example: new Rectangle('blue', 10, 20) +// β†’ color 'blue', width 10, height 20 -// Test Rectangle +// Optional smoke test: // const rectangle = new Rectangle('blue', 10, 20) -// console.log(rectangle.color) // βœ… Should work - inherited from Shape -// console.log(rectangle.width, rectangle.height) // βœ… Should work -// console.log(rectangle) +// console.log(rectangle.color, rectangle.width, rectangle.height) +// 🐨 Export all three classes // export { Shape, Circle, Rectangle } diff --git a/exercises/03.inheritance-and-polymorphism/02.problem.method-overriding/README.mdx b/exercises/03.inheritance-and-polymorphism/02.problem.method-overriding/README.mdx index d948af5..dda3d8f 100644 --- a/exercises/03.inheritance-and-polymorphism/02.problem.method-overriding/README.mdx +++ b/exercises/03.inheritance-and-polymorphism/02.problem.method-overriding/README.mdx @@ -10,8 +10,24 @@ already defined. You'll add and override the `getArea()` method. 🐨 Open and: -1. Add a `getArea()` method to `Shape` that returns 0 -2. Override `getArea()` in `Circle` to return Ο€ Γ— radiusΒ² -3. Override `getArea()` in `Rectangle` to return width Γ— height +1. Add `getArea(): number` on `Shape` that returns `0` +2. Override `getArea()` on `Circle` so a circle's area is Ο€ Γ— radiusΒ² + (use `Math.PI`) +3. Override `getArea()` on `Rectangle` so a rectangle's area is width Γ— height +4. Keep exporting: `export { Shape, Circle, Rectangle }` + +## Fixtures and success criteria + +```ts +const shape = new Shape('red') +const circle = new Circle('red', 5) +const rectangle = new Rectangle('blue', 10, 20) +``` + +- `shape.getArea() === 0` +- `circle.getArea()` is about `78.54` (within `0.01` of `Math.PI * 25`) +- `rectangle.getArea() === 200` +- Circle and rectangle areas are both greater than `0` and not equal to each + other πŸ“œ [TypeScript Handbook - Overriding Methods](https://www.typescriptlang.org/docs/handbook/2/classes.html#overriding-methods) diff --git a/exercises/03.inheritance-and-polymorphism/02.problem.method-overriding/index.ts b/exercises/03.inheritance-and-polymorphism/02.problem.method-overriding/index.ts index 26ae848..1077081 100644 --- a/exercises/03.inheritance-and-polymorphism/02.problem.method-overriding/index.ts +++ b/exercises/03.inheritance-and-polymorphism/02.problem.method-overriding/index.ts @@ -7,7 +7,7 @@ class Shape { this.color = color } - // 🐨 Add a getArea() method that returns 0 + // 🐨 Add getArea(): number that returns 0 } class Circle extends Shape { @@ -18,7 +18,8 @@ class Circle extends Shape { this.radius = radius } - // 🐨 Override getArea() to return Math.PI * radius ** 2 + // 🐨 Override getArea() so area is Ο€ Γ— radiusΒ² (use Math.PI) + // Example: new Circle('red', 5).getArea() β‰ˆ 78.54 } class Rectangle extends Shape { @@ -31,13 +32,15 @@ class Rectangle extends Shape { this.height = height } - // 🐨 Override getArea() to return width * height + // 🐨 Override getArea() so area is width Γ— height + // Example: new Rectangle('blue', 10, 20).getArea() === 200 } -// Test getArea methods +// Optional smoke test: // const circle = new Circle('red', 5) // const rectangle = new Rectangle('blue', 10, 20) -// console.log(circle.getArea()) // Should print ~78.54 -// console.log(rectangle.getArea()) // Should print 200 +// console.log(circle.getArea()) // ~78.54 +// console.log(rectangle.getArea()) // 200 +// 🐨 Keep exporting the classes // export { Shape, Circle, Rectangle } diff --git a/exercises/03.inheritance-and-polymorphism/02.solution.method-overriding/index.test.ts b/exercises/03.inheritance-and-polymorphism/02.solution.method-overriding/index.test.ts index f4aaca4..a758ac3 100644 --- a/exercises/03.inheritance-and-polymorphism/02.solution.method-overriding/index.test.ts +++ b/exercises/03.inheritance-and-polymorphism/02.solution.method-overriding/index.test.ts @@ -28,7 +28,7 @@ await test('Shape getArea should return 0', () => { assert.strictEqual( baseShape.getArea(), 0, - '🚨 Shape.getArea() should return 0 - check your base class method implementation', + '🚨 Shape.getArea() should return 0', ) }) @@ -37,11 +37,11 @@ await test('Circle should override getArea to calculate circle area', () => { const circleArea = sampleCircle.getArea() assert.ok( Math.abs(circleArea - Math.PI * 25) < 0.01, - '🚨 Circle.getArea() should be approximately Math.PI * 25 - check that you override getArea() with the circle area formula', + '🚨 Circle.getArea() for radius 5 should be within 0.01 of Math.PI * 25', ) assert.ok( Math.abs(circleArea - 78.54) < 0.01, - '🚨 Circle.getArea() should be approximately 78.54 - check your circle area calculation (Ο€ * radiusΒ²)', + '🚨 Circle.getArea() for radius 5 should be about 78.54', ) }) @@ -51,7 +51,7 @@ await test('Rectangle should override getArea to calculate rectangle area', () = assert.strictEqual( rectangleArea, 200, - '🚨 Rectangle.getArea() should return 200 (10 * 20) - check that you override getArea() with the rectangle area formula', + '🚨 Rectangle.getArea() for 10Γ—20 should return 200', ) }) @@ -61,17 +61,14 @@ await test('Different shapes should have different area calculations', () => { const circleArea = sampleCircle.getArea() const rectangleArea = sampleRectangle.getArea() - assert.ok( - circleArea > 0, - '🚨 Circle area should be greater than 0 - check your getArea() override implementation', - ) + assert.ok(circleArea > 0, '🚨 Circle.getArea() should be greater than 0') assert.ok( rectangleArea > 0, - '🚨 Rectangle area should be greater than 0 - check your getArea() override implementation', + '🚨 Rectangle.getArea() should be greater than 0', ) assert.notStrictEqual( circleArea, rectangleArea, - '🚨 Circle and Rectangle areas should be different - check that each class overrides getArea() with its own calculation', + '🚨 Circle and Rectangle getArea() results should differ for these fixtures', ) }) diff --git a/exercises/03.inheritance-and-polymorphism/03.problem.substitutability/README.mdx b/exercises/03.inheritance-and-polymorphism/03.problem.substitutability/README.mdx index fd1b419..3cfff77 100644 --- a/exercises/03.inheritance-and-polymorphism/03.problem.substitutability/README.mdx +++ b/exercises/03.inheritance-and-polymorphism/03.problem.substitutability/README.mdx @@ -5,15 +5,36 @@ πŸ‘¨β€πŸ’Ό Let's build a media player that can play different types of media files. We'll use polymorphism to handle all media types uniformly. -The `MediaFile`, `AudioFile`, and `VideoFile` classes are already defined. -You'll create a `MediaPlayer` that works with any of them. +The `MediaFile`, `AudioFile`, and `VideoFile` classes are already defined with +these `play()` results: + +- `new MediaFile('file.mp3').play()` β†’ `'Playing file.mp3'` +- `new AudioFile('song.mp3').play()` β†’ `'Playing audio: song.mp3'` +- `new VideoFile('movie.mp4').play()` β†’ `'Playing video: movie.mp4'` + +You'll create a `MediaPlayer` that works with any of them through the base type. 🐨 Open and: -1. Create a `MediaPlayer` class with a `playFile()` method -2. The method should accept a `MediaFile` parameter -3. Call `media.play()` and return the result -4. Test it with both `AudioFile` and `VideoFile` instances +1. Create a `MediaPlayer` class with `playFile(media: MediaFile): string` +2. `playFile` must accept the **base** `MediaFile` type (not only subclasses) +3. Return that media file's play result string +4. Export everything: + `export { MediaFile, AudioFile, VideoFile, MediaPlayer }` + +## Fixtures and success criteria + +```ts +const player = new MediaPlayer() +``` + +- `player.playFile(new MediaFile('file.mp3')) === 'Playing file.mp3'` +- `player.playFile(new AudioFile('song.mp3')) === 'Playing audio: song.mp3'` +- `player.playFile(new VideoFile('movie.mp4')) === 'Playing video: movie.mp4'` + +Because the parameter is typed as `MediaFile`, `AudioFile` and `VideoFile` +instances are valid arguments (substitutability). You should not need separate +overloads or type checks for each subclass. πŸ’° The key insight: `playFile()` accepts `MediaFile`, but it works with any subclass (`AudioFile`, `VideoFile`) because of substitutabilityβ€”subclasses diff --git a/exercises/03.inheritance-and-polymorphism/03.problem.substitutability/index.ts b/exercises/03.inheritance-and-polymorphism/03.problem.substitutability/index.ts index a94869d..622ab5c 100644 --- a/exercises/03.inheritance-and-polymorphism/03.problem.substitutability/index.ts +++ b/exercises/03.inheritance-and-polymorphism/03.problem.substitutability/index.ts @@ -25,14 +25,19 @@ class VideoFile extends MediaFile { } // 🐨 Create a MediaPlayer class with: -// - Method: playFile(media: MediaFile) returns string -// - Calls media.play() and returns the result - -// Test MediaPlayer (polymorphism) -// const audio = new AudioFile('song.mp3') -// const video = new VideoFile('movie.mp4') +// - playFile(media: MediaFile): string +// - Parameter type must be MediaFile (the base class) +// - Returns that media file's play result string +// +// Expected results: +// playFile(new MediaFile('file.mp3')) β†’ "Playing file.mp3" +// playFile(new AudioFile('song.mp3')) β†’ "Playing audio: song.mp3" +// playFile(new VideoFile('movie.mp4')) β†’ "Playing video: movie.mp4" + +// Optional smoke test: // const player = new MediaPlayer() -// console.log(player.playFile(audio)) // Should work - AudioFile is substitutable for MediaFile -// console.log(player.playFile(video)) // Should work - VideoFile is substitutable for MediaFile +// console.log(player.playFile(new AudioFile('song.mp3'))) +// console.log(player.playFile(new VideoFile('movie.mp4'))) +// 🐨 Export MediaFile, AudioFile, VideoFile, and MediaPlayer // export { MediaFile, AudioFile, VideoFile, MediaPlayer } diff --git a/exercises/03.inheritance-and-polymorphism/03.solution.substitutability/index.test.ts b/exercises/03.inheritance-and-polymorphism/03.solution.substitutability/index.test.ts index 1e2d4b5..beab766 100644 --- a/exercises/03.inheritance-and-polymorphism/03.solution.substitutability/index.test.ts +++ b/exercises/03.inheritance-and-polymorphism/03.solution.substitutability/index.test.ts @@ -36,7 +36,7 @@ await test('MediaPlayer should accept MediaFile instances', () => { assert.strictEqual( basePlayer.playFile(base), 'Playing file.mp3', - '🚨 playFile() should return "Playing file.mp3" - check your MediaPlayer.playFile method accepts MediaFile type', + '🚨 playFile(new MediaFile("file.mp3")) should return "Playing file.mp3"', ) }) @@ -46,7 +46,7 @@ await test('MediaPlayer should accept AudioFile instances (polymorphism)', () => assert.strictEqual( basePlayer.playFile(audio), 'Playing audio: song.mp3', - '🚨 playFile() should return "Playing audio: song.mp3" - check that AudioFile extends MediaFile and playFile accepts the base type', + '🚨 playFile(new AudioFile("song.mp3")) should return "Playing audio: song.mp3" (AudioFile must be accepted as MediaFile)', ) }) @@ -56,7 +56,7 @@ await test('MediaPlayer should accept VideoFile instances (polymorphism)', () => assert.strictEqual( basePlayer.playFile(video), 'Playing video: movie.mp4', - '🚨 playFile() should return "Playing video: movie.mp4" - check that VideoFile extends MediaFile and playFile accepts the base type', + '🚨 playFile(new VideoFile("movie.mp4")) should return "Playing video: movie.mp4" (VideoFile must be accepted as MediaFile)', ) }) diff --git a/exercises/04.composition-vs-inheritance/01.problem.when-to-use/README.mdx b/exercises/04.composition-vs-inheritance/01.problem.when-to-use/README.mdx index 6319e9c..3ac7161 100644 --- a/exercises/04.composition-vs-inheritance/01.problem.when-to-use/README.mdx +++ b/exercises/04.composition-vs-inheritance/01.problem.when-to-use/README.mdx @@ -7,11 +7,44 @@ inheritance or composition for different scenarios. 🐨 Open and: -1. Create a `Logger` class with a `log(message: string)` method -2. Create an `EmailService` class that uses composition (has-a Logger) -3. Create a `FileLogger` class that uses inheritance (is-a Logger) -4. Create a `ConsoleLogger` class that uses inheritance (is-a Logger) -5. Demonstrate when each approach is appropriate +### Inheritance (`is-a`) + +1. Create a `Logger` base class with `log(message: string): void` that prints + exactly `Log: {message}` +2. Create a `FileLogger` that `extends Logger` and overrides `log` to print + exactly `File Log: {message}` +3. Create a `ConsoleLogger` that `extends Logger` and overrides `log` to print + exactly `Console Log: {message}` + +### Composition (`has-a`) + +4. Create an `EmailService` that **has** a logger (composition), not is a logger: + - Accept a `Logger` in the constructor + - Keep that logger as private class state (use `#`) + - `sendEmail(to: string, subject: string): void` must produce the send-action + message `Sending email to ${to}: ${subject}` **through the injected logger + and on the console** (both) +5. Export all four: + `export { Logger, FileLogger, ConsoleLogger, EmailService }` + +## Fixtures and success criteria + +```ts +const fileLogger = new FileLogger() +const consoleLogger = new ConsoleLogger() +const emailService = new EmailService(fileLogger) +emailService.sendEmail('user@example.com', 'Welcome') +``` + +- `fileLogger instanceof Logger` and `consoleLogger instanceof Logger` +- `fileLogger.log('hi')` prints `File Log: hi` +- `consoleLogger.log('hi')` prints `Console Log: hi` +- `new Logger().log('hi')` prints `Log: hi` +- `EmailService` can be constructed with either logger subclass +- After `sendEmail('user@example.com', 'Welcome')` with a `FileLogger`, console + output includes both: + - `File Log: Sending email to user@example.com: Welcome` + - `Sending email to user@example.com: Welcome` πŸ’° Think about the relationship: Is it "is-a" or "has-a"? diff --git a/exercises/04.composition-vs-inheritance/01.problem.when-to-use/index.ts b/exercises/04.composition-vs-inheritance/01.problem.when-to-use/index.ts index 8f13d84..e123322 100644 --- a/exercises/04.composition-vs-inheritance/01.problem.when-to-use/index.ts +++ b/exercises/04.composition-vs-inheritance/01.problem.when-to-use/index.ts @@ -1,37 +1,35 @@ // When to Use Composition vs Inheritance // 🐨 Create a Logger base class with: -// - Method: log(message: string) returns void -// - Prints "Log: {message}" to console +// - log(message: string): void +// - Prints exactly "Log: {message}" // 🐨 Create a FileLogger class using INHERITANCE (is-a): // - Extends Logger -// - Override log() to print "File Log: {message}" +// - Override log() to print exactly "File Log: {message}" -// Test FileLogger +// Optional smoke test: // const fileLogger = new FileLogger() -// fileLogger.log('File system initialized') -// console.log(fileLogger) +// fileLogger.log('File system initialized') // File Log: File system initialized // 🐨 Create a ConsoleLogger class using INHERITANCE (is-a): // - Extends Logger -// - Override log() to print "Console Log: {message}" +// - Override log() to print exactly "Console Log: {message}" -// Test ConsoleLogger +// Optional smoke test: // const consoleLogger = new ConsoleLogger() -// consoleLogger.log('Console ready') -// console.log(consoleLogger) +// consoleLogger.log('Console ready') // Console Log: Console ready // 🐨 Create an EmailService class using COMPOSITION (has-a): -// - Field: #logger (Logger) - private field -// - Constructor that takes a Logger -// - Method: sendEmail(to: string, subject: string) returns void -// - Uses this.#logger.log() to log the email action -// - Prints "Sending email to {to}: {subject}" - -// Test EmailService -// const emailService = new EmailService(fileLogger) -// emailService.sendEmail('user@example.com', 'Welcome') -// console.log(emailService) - +// - Constructor takes a Logger and stores it privately (use #) +// - sendEmail(to, subject): void +// - Must produce "Sending email to {to}: {subject}" through the injected +// logger AND on the console (both) +// +// With a FileLogger, sendEmail('user@example.com', 'Welcome') should result in +// console output that includes: +// File Log: Sending email to user@example.com: Welcome +// Sending email to user@example.com: Welcome + +// 🐨 Export all four classes // export { Logger, FileLogger, ConsoleLogger, EmailService } diff --git a/exercises/04.composition-vs-inheritance/01.solution.when-to-use/index.test.ts b/exercises/04.composition-vs-inheritance/01.solution.when-to-use/index.test.ts index 3d706ca..eb7ff01 100644 --- a/exercises/04.composition-vs-inheritance/01.solution.when-to-use/index.test.ts +++ b/exercises/04.composition-vs-inheritance/01.solution.when-to-use/index.test.ts @@ -2,6 +2,20 @@ import assert from 'node:assert/strict' import { test } from 'node:test' import * as solution from './index.ts' +function captureConsoleLog(run: () => void): Array { + const lines: Array = [] + const original = console.log + console.log = (...args: Array) => { + lines.push(args.map(String).join(' ')) + } + try { + run() + } finally { + console.log = original + } + return lines +} + await test('Logger class should be exported', () => { assert.ok( 'Logger' in solution, @@ -46,6 +60,31 @@ await test('ConsoleLogger should extend Logger (inheritance)', () => { ) }) +await test('Logger log formats should match the required prefixes', () => { + const baseLines = captureConsoleLog(() => { + new solution.Logger().log('hello') + }) + const fileLines = captureConsoleLog(() => { + new solution.FileLogger().log('hello') + }) + const consoleLines = captureConsoleLog(() => { + new solution.ConsoleLogger().log('hello') + }) + + assert.ok( + baseLines.includes('Log: hello'), + '🚨 Logger.log("hello") should print "Log: hello"', + ) + assert.ok( + fileLines.includes('File Log: hello'), + '🚨 FileLogger.log("hello") should print "File Log: hello"', + ) + assert.ok( + consoleLines.includes('Console Log: hello'), + '🚨 ConsoleLogger.log("hello") should print "Console Log: hello"', + ) +}) + await test('EmailService should accept any Logger (composition)', () => { const fileLogger = new solution.FileLogger() const consoleLogger = new solution.ConsoleLogger() @@ -62,13 +101,26 @@ await test('EmailService should accept any Logger (composition)', () => { ) }) -await test('EmailService should use logger in sendEmail method', () => { +await test('EmailService sendEmail should use the injected logger and print the send action', () => { const fileLogger = new solution.FileLogger() - const emailService1 = new solution.EmailService(fileLogger) + const emailService = new solution.EmailService(fileLogger) assert.ok( - typeof emailService1.sendEmail === 'function', + typeof emailService.sendEmail === 'function', '🚨 EmailService.sendEmail should be a function - check your method definition', ) + + const lines = captureConsoleLog(() => { + emailService.sendEmail('user@example.com', 'Welcome') + }) + + assert.ok( + lines.includes('File Log: Sending email to user@example.com: Welcome'), + '🚨 After sendEmail("user@example.com", "Welcome") with FileLogger, output should include "File Log: Sending email to user@example.com: Welcome"', + ) + assert.ok( + lines.includes('Sending email to user@example.com: Welcome'), + '🚨 After sendEmail("user@example.com", "Welcome"), output should also include "Sending email to user@example.com: Welcome"', + ) }) await test('EmailService should work with different logger types', () => { @@ -85,4 +137,14 @@ await test('EmailService should work with different logger types', () => { emailService2 instanceof solution.EmailService, '🚨 emailService2 should be an instance of EmailService - check your class definition', ) + + const consoleLines = captureConsoleLog(() => { + emailService2.sendEmail('admin@example.com', 'Alert') + }) + assert.ok( + consoleLines.includes( + 'Console Log: Sending email to admin@example.com: Alert', + ), + '🚨 After sendEmail with ConsoleLogger, output should include "Console Log: Sending email to admin@example.com: Alert"', + ) }) diff --git a/exercises/04.composition-vs-inheritance/02.problem.dependency-injection/README.mdx b/exercises/04.composition-vs-inheritance/02.problem.dependency-injection/README.mdx index bc886b8..5619d97 100644 --- a/exercises/04.composition-vs-inheritance/02.problem.dependency-injection/README.mdx +++ b/exercises/04.composition-vs-inheritance/02.problem.dependency-injection/README.mdx @@ -6,12 +6,44 @@ implementations without changing `EmailService`. This is dependency injectionβ€”a key benefit of composition. +`Logger` and `EmailService` from the previous step are already provided. +`EmailService.sendEmail(to, subject)` produces +`Sending email to ${to}: ${subject}` through whatever logger you inject (and +also on the console). + 🐨 Open and: -1. Create a `MockLogger` class that extends `Logger` and stores logs in memory (useful for testing) -2. Create a `SilentLogger` class that extends `Logger` and does nothing (useful for production) -3. Both should extend the `Logger` class from step 01 -4. Demonstrate that `EmailService` works with any logger implementation +1. Create and export a `MockLogger` that `extends Logger`: + - Override `log(message)` to **store** each message in order (do not print) + - Expose `getLogs(): Array` that returns the stored messages +2. Create and export a `SilentLogger` that `extends Logger`: + - Override `log(message)` to do nothing (no print, no storage, no throw) +3. Demonstrate that the existing `EmailService` works with both loggers +4. Export: + `export { Logger, EmailService, MockLogger, SilentLogger }` + +## Fixtures and success criteria + +```ts +const mockLogger = new MockLogger() +mockLogger.log('Test message 1') +mockLogger.log('Test message 2') + +const emailService = new EmailService(mockLogger) +emailService.sendEmail('test@example.com', 'Test Subject') + +const silentLogger = new SilentLogger() +const silentEmail = new EmailService(silentLogger) +silentEmail.sendEmail('user@example.com', 'Welcome') +``` + +- `mockLogger.getLogs()` starts as `['Test message 1', 'Test message 2']` before + `sendEmail` +- After `sendEmail('test@example.com', 'Test Subject')`, `getLogs()` includes + `'Sending email to test@example.com: Test Subject'` +- `silentLogger instanceof Logger` is `true` +- Using `SilentLogger` with `EmailService` does not throw +- The same `EmailService` API works unchanged with either logger πŸ’° This is the power of dependency injection: `EmailService` doesn't care which logger it getsβ€”it just uses whatever logger is provided. This makes testing easy diff --git a/exercises/04.composition-vs-inheritance/02.problem.dependency-injection/index.ts b/exercises/04.composition-vs-inheritance/02.problem.dependency-injection/index.ts index 72109b8..7468721 100644 --- a/exercises/04.composition-vs-inheritance/02.problem.dependency-injection/index.ts +++ b/exercises/04.composition-vs-inheritance/02.problem.dependency-injection/index.ts @@ -22,31 +22,18 @@ class EmailService { } // 🐨 Create a MockLogger class that extends Logger: -// - Extends Logger -// - Override log() to store messages in an array: #logs (private field) -// - Stores messages instead of printing them -// - Method: getLogs() returns Array to retrieve stored messages - -// Test MockLogger -// const mockLogger = new MockLogger() -// mockLogger.log('Test message') -// console.log(mockLogger.getLogs()) // Should show ['Test message'] - -// Test EmailService with MockLogger (dependency injection) -// const mockLogger = new MockLogger() -// const emailService = new EmailService(mockLogger) -// emailService.sendEmail('test@example.com', 'Test Subject') -// console.log(mockLogger.getLogs()) // Should show the email log -// console.log(emailService) +// - Override log(message) to store messages in order (do not print) +// - getLogs(): Array returns the stored messages +// +// Expected: +// mockLogger.log('Test message 1'); mockLogger.log('Test message 2') +// β†’ getLogs() is ['Test message 1', 'Test message 2'] +// After EmailService(mockLogger).sendEmail('test@example.com', 'Test Subject') +// β†’ getLogs() includes 'Sending email to test@example.com: Test Subject' // 🐨 Create a SilentLogger class that extends Logger: -// - Extends Logger -// - Override log() to do nothing (silent logging for production) - -// Test SilentLogger -// const silentLogger = new SilentLogger() -// const emailService2 = new EmailService(silentLogger) -// emailService2.sendEmail('user@example.com', 'Welcome') -// console.log(emailService2) +// - Override log(message) to do nothing (no print / no throw) +// - EmailService + SilentLogger should not throw on sendEmail +// 🐨 Export Logger, EmailService, MockLogger, and SilentLogger // export { Logger, EmailService, MockLogger, SilentLogger } diff --git a/exercises/04.composition-vs-inheritance/02.solution.dependency-injection/index.test.ts b/exercises/04.composition-vs-inheritance/02.solution.dependency-injection/index.test.ts index d5060c1..74d8954 100644 --- a/exercises/04.composition-vs-inheritance/02.solution.dependency-injection/index.test.ts +++ b/exercises/04.composition-vs-inheritance/02.solution.dependency-injection/index.test.ts @@ -36,6 +36,11 @@ await test('MockLogger should store logs', () => { 'Test message 1', '🚨 First log should be "Test message 1" - check your log storage', ) + assert.strictEqual( + logs[1], + 'Test message 2', + '🚨 Second log should be "Test message 2" - check that log() stores messages in order', + ) }) await test('MockLogger should work with EmailService (dependency injection)', () => { @@ -45,12 +50,8 @@ await test('MockLogger should work with EmailService (dependency injection)', () const logs = mockLogger.getLogs() assert.ok( - logs.length > 0, - '🚨 MockLogger should have logs after sendEmail - check that EmailService uses the logger', - ) - assert.ok( - logs.some((log) => log.includes('test@example.com')), - '🚨 Logs should contain email address - check that EmailService logs the email action', + logs.includes('Sending email to test@example.com: Test Subject'), + '🚨 After sendEmail("test@example.com", "Test Subject"), getLogs() should include "Sending email to test@example.com: Test Subject"', ) }) @@ -93,7 +94,7 @@ await test('EmailService should work with different logger implementations', () const logs = mockLogger.getLogs() assert.ok( - logs.length > 0, - '🚨 MockLogger should capture logs from EmailService - demonstrating dependency injection', + logs.includes('Sending email to test1@example.com: Test 1'), + '🚨 MockLogger should capture "Sending email to test1@example.com: Test 1" from EmailService', ) }) diff --git a/extra/01.practice-everything/README.mdx b/extra/01.practice-everything/README.mdx index 891dd87..94dac66 100644 --- a/extra/01.practice-everything/README.mdx +++ b/extra/01.practice-everything/README.mdx @@ -1,3 +1,58 @@ # Practice Everything -🐨 Open and work through each section. +🐨 Open and work through each section. There are +no automated tests hereβ€”use the examples below as your success criteria. + +## Section 1: Class Basics + +- `Product.getDescription()` returns `{name}: ${price}` + (example: `Mug: $12`) +- `ShoppingCart` starts empty; `addItem` appends; `getTotal` sums prices +- Example: Mug `12` + Notebook `8` β†’ total `20` + +## Section 2: Private Fields & Defaults + +- `BankAccount` starts at balance `0` when constructed with no args +- `deposit` / `withdraw` change balance; `getBalance` returns it +- Example: deposit `50`, withdraw `10` β†’ balance `40` +- `Config` defaults: `host = 'localhost'`, `port = 3000`, `role = 'user'` + +## Section 3: Interfaces + +- `CreditCard.pay(25)` β†’ `Paid $25 with card 1234` (for card `'1234'`) +- `PayPal.pay(25)` β†’ `Paid $25 with PayPal user@example.com` + +## Section 4: Programming to Abstractions + +- `processPayment(method, amount)` returns that method's payment result for the + amount +- `GiftCard` with code `'GC-001'`: `pay(40)` β†’ + `Paid $40 with gift card GC-001` + +## Section 5: Inheritance + +- `Package.getLabel()` β†’ `{label} ({weight}kg)` +- Example: `new Box('Box A', 5, 10).getLabel()` β†’ `Box A (5kg)` +- Example: `new Crate('Crate B', 20, 'wood').getLabel()` β†’ `Crate B (20kg)` + +## Section 6: Method Overriding + +- `Shape.getArea()` β†’ `0` +- `new Circle(2).getArea()` is about `12.57` (Ο€ Γ— radiusΒ²) +- `new Rectangle(3, 4).getArea()` β†’ `12` + +## Section 7: Substitutability + +- `MediaPlayer.playFile` accepts `MediaFile` and returns that file's play result +- Audio example: `Playing audio song.mp3` +- Video example: `Playing video movie.mp4` + +## Section 8: Composition & Dependency Injection + +- `Logger.log(message)` **returns** the prefixed string `Log: {message}` +- `InMemoryLogger` **stores the raw `message` argument** (not the prefixed + return value); `getLogs()` returns a copy of those stored messages +- `ReportService.generateReport('Weekly Summary')` calls the injected logger + with `Report: Weekly Summary` +- After that call with an `InMemoryLogger`, `getLogs()` includes + `Report: Weekly Summary` (the argument that was logged) diff --git a/extra/01.practice-everything/index.ts b/extra/01.practice-everything/index.ts index ce1a503..fd08317 100644 --- a/extra/01.practice-everything/index.ts +++ b/extra/01.practice-everything/index.ts @@ -9,13 +9,14 @@ // - public fields: name (string), price (number) // - a constructor that takes name and price // - a method `getDescription()` that returns "{name}: ${price}" +// Example: new Product('Mug', 12).getDescription() β†’ "Mug: $12" // 🐨 Create a class `ShoppingCart` with: // - a public field `items` initialized to an empty array of Product // - a method `addItem(item: Product)` that adds to items // - a method `getTotal()` that returns the total price of all items -// Test Section 1: +// Optional smoke test: // const cart = new ShoppingCart() // cart.addItem(new Product('Mug', 12)) // cart.addItem(new Product('Notebook', 8)) @@ -26,23 +27,21 @@ // ============================================================================ // 🐨 Create a class `BankAccount` with: -// - a private field `#balance` (number) -// - a constructor that takes an initial balance (default 0) -// - a method `deposit(amount: number)` that adds to balance -// - a method `withdraw(amount: number)` that subtracts from balance -// - a method `getBalance()` that returns the balance +// - a private balance field (use #; default 0) +// - deposit(amount) / withdraw(amount) / getBalance() +// Example: deposit(50), withdraw(10) β†’ getBalance() === 40 // 🐨 Create a class `Config` with: // - public fields: host (string), port (number), role (string) -// - default values: host = 'localhost', port = 3000, role = 'user' +// - defaults: host = 'localhost', port = 3000, role = 'user' -// Test Section 2: +// Optional smoke test: // const account = new BankAccount() // account.deposit(50) // account.withdraw(10) // console.log(account.getBalance()) // 40 // const config = new Config() -// console.log(config.host, config.port, config.role) +// console.log(config.host, config.port, config.role) // localhost 3000 user // ============================================================================ // SECTION 3: Interfaces & Implementations @@ -54,12 +53,15 @@ // 🐨 Create a class `CreditCard` that implements PaymentMethod // - public field: cardNumber (string) // - pay returns "Paid $${amount} with card ${cardNumber}" +// Example: new CreditCard('1234').pay(25) β†’ "Paid $25 with card 1234" // 🐨 Create a class `PayPal` that implements PaymentMethod // - public field: email (string) // - pay returns "Paid $${amount} with PayPal ${email}" +// Example: new PayPal('user@example.com').pay(25) +// β†’ "Paid $25 with PayPal user@example.com" -// Test Section 3: +// Optional smoke test: // const card = new CreditCard('1234') // const paypal = new PayPal('user@example.com') // console.log(card.pay(25)) @@ -71,7 +73,7 @@ // 🐨 Create a function `processPayment` that: // - takes a PaymentMethod and an amount -// - returns the result of calling pay on the method +// - returns that method's payment result for the amount // 🐨 Create a class `GiftCard` that implements PaymentMethod // - public field: code (string) @@ -112,13 +114,15 @@ // 🐨 Create a class `Circle` that extends Shape // - field: radius (number) -// - override getArea to return Math.PI * radius * radius +// - override getArea for a circle (Ο€ Γ— radiusΒ²; use Math.PI) +// Example: new Circle(2).getArea() β‰ˆ 12.57 // 🐨 Create a class `Rectangle` that extends Shape // - fields: width (number), height (number) -// - override getArea to return width * height +// - override getArea for a rectangle +// Example: new Rectangle(3, 4).getArea() === 12 -// Test Section 6: +// Optional smoke test: // console.log(new Circle(2).getArea()) // console.log(new Rectangle(3, 4).getArea()) @@ -132,14 +136,17 @@ // 🐨 Create a class `AudioFile` that extends MediaFile // - override play to return "Playing audio {filename}" +// Example: "Playing audio song.mp3" // 🐨 Create a class `VideoFile` that extends MediaFile // - override play to return "Playing video {filename}" +// Example: "Playing video movie.mp4" // 🐨 Create a class `MediaPlayer` with: -// - a method `playFile(file: MediaFile)` that returns file.play() +// - playFile(file: MediaFile) returns that file's play result string +// (parameter type must be the base MediaFile) -// Test Section 7: +// Optional smoke test: // const player = new MediaPlayer() // console.log(player.playFile(new AudioFile('song.mp3'))) // console.log(player.playFile(new VideoFile('movie.mp4'))) @@ -149,20 +156,26 @@ // ============================================================================ // 🐨 Create a class `Logger` with: -// - a method `log(message: string)` that returns `Log: {message}` +// - log(message: string) returns the prefixed string `Log: {message}` // 🐨 Create a class `ConsoleLogger` that extends Logger -// - override log to call console.log and return the same string +// - override log to print and return that same prefixed string // 🐨 Create a class `InMemoryLogger` that extends Logger -// - a private field `#logs` (array of strings) -// - override log to store messages in #logs -// - a method `getLogs()` that returns a copy of #logs +// - store the raw message argument privately (not the prefixed return value) +// - still return the prefixed `Log: {message}` string from log() +// - getLogs() returns a copy of the stored raw messages // 🐨 Create a class `ReportService` that takes a Logger in its constructor -// - method `generateReport(title: string)` that logs "Report: {title}" +// - generateReport(title) logs "Report: {title}" through the injected logger +// +// Example: +// const logger = new InMemoryLogger() +// new ReportService(logger).generateReport('Weekly Summary') +// logger.getLogs() includes "Report: Weekly Summary" +// (the argument passed to log β€” not "Log: Report: Weekly Summary") -// Test Section 8: +// Optional smoke test: // const logger = new InMemoryLogger() // const service = new ReportService(logger) // service.generateReport('Weekly Summary') diff --git a/extra/02.inventory-manager/README.mdx b/extra/02.inventory-manager/README.mdx index 8026779..2fed225 100644 --- a/extra/02.inventory-manager/README.mdx +++ b/extra/02.inventory-manager/README.mdx @@ -30,15 +30,63 @@ npm run dev ### In `src/app.tsx` -1. Verify the UI updates once class methods are implemented -2. Check the TODO callouts to confirm you've completed each piece +1. Replace the placeholder item objects with real class instances +2. Wire `InventoryManager` + loggers so the Inventory Actions panel shows + live data + +## Concrete examples (suggested fixtures) + +Use these field values (constructor shape is up to you) so the UI has something +concrete to show: + +### Electronics β€” Laptop + +Fields: `name: 'Laptop'`, `basePrice: 1000`, `brand: 'Nova'`, `model: 'X15'`, +`serialNumber: 'SN-100'`, `location: 'Aisle 3'`, `discountPercent: 10` + +Observable results: + +- `getDescription()` includes the name, brand, and model +- `calculatePrice(2)` β†’ `1800` with those price/discount values +- `getTrackingInfo()` includes serial and location details +- `updateLocation('Warehouse')` is reflected in later tracking info + +### Clothing β€” Hoodie + +Fields: `name: 'Hoodie'`, `basePrice: 40`, `size: 'M'`, `color: 'Navy'`, +`discountPercent: 0` + +- `getDescription()` includes the name, size, and color +- `calculatePrice(1)` β†’ `40` when discount is `0` + +### Perishable β€” Apples + +Fields: `name: 'Apples'`, `basePrice: 3`, `expirationDate: '2026-08-01'` + +- `getDescription()` includes the name and expiration date + +### Logging + InventoryManager + +`Logger.log(message: string): string` returns the same `message` it received +(no required prefix). `ConsoleLogger` also prints that message. +`InMemoryLogger` stores that same message for `getLogs()`. + +```ts +const manager = new InventoryManager(new ConsoleLogger()) +manager.receiveStock('Laptop', 5) +// log output mentions receiving Laptop / 5 + +const memoryLogger = new InMemoryLogger() +new InventoryManager(memoryLogger).shipStock('Hoodie', 2) +memoryLogger.getLogs().length // >= 1 +``` ## Tips πŸ’° Start with the base `InventoryItem` class. Many other classes depend on it. -πŸ’° When you implement pricing, be consistent about using `basePrice`, -`quantity`, and `discountPercent`. +πŸ’° For sellable pricing, combine `basePrice`, order `quantity`, and +`discountPercent` so the Laptop example above yields `1800` for quantity `2`. πŸ’° Use the logger classes to see the effects of dependency injection. diff --git a/extra/02.inventory-manager/src/classes.ts b/extra/02.inventory-manager/src/classes.ts index 91e2a8c..2d9d5ee 100644 --- a/extra/02.inventory-manager/src/classes.ts +++ b/extra/02.inventory-manager/src/classes.ts @@ -18,9 +18,10 @@ // ============================================================================ // 🐨 Implement InventoryItem with: -// - private fields #id and #quantity +// - private fields for id and quantity (use #) // - public fields name and basePrice // - methods: getId, getQuantity, adjustQuantity, getDescription +// Example: getDescription() can start as just the item name // ============================================================================ // Inventory Item Types @@ -28,17 +29,23 @@ // 🐨 Implement Electronics to extend InventoryItem and implement Sellable, Trackable // - fields: brand, model, serialNumber, warrantyMonths, location, discountPercent -// - methods: calculatePrice, applyDiscount, getTrackingInfo, updateLocation -// - override getDescription to include brand/model +// - calculatePrice / applyDiscount +// Example check: basePrice 1000, discountPercent 10, calculatePrice(2) β†’ 1800 +// - getTrackingInfo() includes serialNumber and location +// - updateLocation(location) is reflected in later tracking reads +// - override getDescription to include name, brand, and model +// (exact wording is up to you β€” e.g. it might mention Laptop / Nova / X15) // 🐨 Implement Clothing to extend InventoryItem and implement Sellable // - fields: size, color, discountPercent -// - methods: calculatePrice, applyDiscount -// - override getDescription to include size/color +// - same pricing behavior as Electronics +// - override getDescription to include name, size, and color +// (exact wording is up to you) // 🐨 Implement Perishable to extend InventoryItem // - field: expirationDate -// - override getDescription to include expiration date +// - override getDescription to include name and expiration date +// (exact wording is up to you) // ============================================================================ // Composition & Dependency Injection @@ -46,17 +53,19 @@ // 🐨 Create a Logger class with: // - log(message: string): string +// - Returns the same message it received (no required prefix) // 🐨 Extend Logger with ConsoleLogger -// - override log to call console.log +// - override log to print the message and return that same message // 🐨 Extend Logger with InMemoryLogger -// - private field #logs -// - override log to store logs -// - getLogs(): Array +// - store each message privately (the same string passed to log) +// - return that same message from log() +// - getLogs(): Array returns the stored messages // 🐨 Implement InventoryManager using dependency injection for logging -// - constructor takes Logger -// - receiveStock and shipStock log actions +// - constructor(logger: Logger) +// - receiveStock(name, quantity) logs a receive action that mentions name/qty +// - shipStock(name, quantity) logs a ship action that mentions name/qty export {}