diff --git a/JavaScript/1-object.js b/JavaScript/1-object.js index 1685232..d4f77a8 100644 --- a/JavaScript/1-object.js +++ b/JavaScript/1-object.js @@ -1,16 +1,22 @@ 'use strict'; -const rome = { name: 'Rome' }; +// Робимо спільний об'єкт `rome` незмінним. +const rome = Object.freeze({ name: 'Rome' }); +// Створюємо об'єкт `marcus` одразу з потрібними даними, без подальших мутацій. const marcus = { id: 1, - name: 'Marcus', + name: 'Marcus Aurelius', city: rome, email: 'marcus@metarhia.com', }; -marcus.name = 'Marcus Aurelius'; -const lucius = Object.assign({}, marcus, { name: 'Lucius Verus' }); -lucius.email = 'lucius@metarhia.com'; +// Створюємо `lucius` на основі `marcus` за допомогою spread-синтаксису +const lucius = { + ...marcus, + name: 'Lucius Verus', + email: 'lucius@metarhia.com', +}; +// Можна "заморозити" і фінальні об'єкти console.log({ marcus, lucius }); diff --git a/JavaScript/2-record.js b/JavaScript/2-record.js index 8fe6821..9359ac0 100644 --- a/JavaScript/2-record.js +++ b/JavaScript/2-record.js @@ -10,24 +10,31 @@ class Record { } static #build(fields, isMutable) { + const fieldSet = new Set(fields); + class Struct { - static fields = fields.slice(); + static fields = Object.freeze(fields.slice()); static mutable = isMutable; - - static create(...values) { - if (fields.length !== values.length) { - throw new Error('Record arity mismatch'); + static create(props) { + for (const field of fields) { + if (!Reflect.has(props, field)) { + throw new Error(`Missing field: ${field}`); + } } - const obj = Object.create(null); - for (let i = 0; i < fields.length; i++) { - obj[fields[i]] = values[i]; + for (const key in props) { + if (!fieldSet.has(key)) { + throw new Error(`Unexpected field: ${key}`); + } } + + const obj = Object.fromEntries( + fields.map(field => [field, props[field]]) + ); return isMutable ? Object.seal(obj) : Object.freeze(obj); } } return Struct; } - static update(instance, updates) { if (Object.isFrozen(instance)) { throw new Error('Cannot mutate immutable Record'); @@ -39,23 +46,35 @@ class Record { } return instance; } - + static fork(instance, updates) { - const copy = Object.create(null); - for (const key of Object.keys(instance)) { - copy[key] = Reflect.has(updates, key) ? updates[key] : instance[key]; - } + const copy = { ...instance, ...updates }; return Object.isFrozen(instance) ? Object.freeze(copy) : Object.seal(copy); } } -// Usage +// Оновлений приклад використання const City = Record.immutable(['name']); -const User = Record.mutable(['id', 'name', 'city', 'email']); -const rome = City.create('Rome'); -const marcus = User.create(1, 'Marcus', rome, 'marcus@metarhia.com'); -Record.update(marcus, { name: 'Marcus Aurelius' }); -const lucius = Record.fork(marcus, { name: 'Lucius Verus' }); -Record.update(lucius, { email: 'lucius@metarhia.com' }); -console.log({ marcus, lucius }); +const User = Record.immutable(['id', 'name', 'city', 'email']); +const rome = City.create({ name: 'Rome' }); +const marcus = User.create({ + id: 1, + name: 'Marcus', + city: rome, + email: 'marcus@metarhia.com' +}); + +const marcusUpdated = Record.fork(marcus, { name: 'Marcus Aurelius' }); +const lucius = Record.fork(marcusUpdated, { + name: 'Lucius Verus', + email: 'lucius@metarhia.com' +}); + +console.log({ marcus, marcusUpdated, lucius }); + +try { + Record.update(marcus, { name: 'FAIL' }); +} catch (err) { + console.error('\nError trying to update immutable record:', err.message); +} diff --git a/JavaScript/3-branch.js b/JavaScript/3-branch.js index 35630d6..6ba6dcc 100644 --- a/JavaScript/3-branch.js +++ b/JavaScript/3-branch.js @@ -4,23 +4,30 @@ class Record { static immutable(fields) { return Record.#build(fields, false); } - static mutable(fields) { return Record.#build(fields, true); } - static #build(fields, isMutable) { + const fieldSet = new Set(fields); class Struct { - static fields = fields.slice(); + static fields = Object.freeze(fields.slice()); static mutable = isMutable; - static create(...values) { - if (fields.length !== values.length) { - throw new Error('Record arity mismatch'); - } + // Приймаємо об'єкт з іменованими полями замість позиційних аргументів. + + static create(props) { const obj = Object.create(null); - for (let i = 0; i < fields.length; i++) { - obj[fields[i]] = values[i]; + for (const field of fields) { + if (!Reflect.has(props, field)) { + throw new Error(`Missing field: ${field}`); + } + obj[field] = props[field]; + } + + for (const key in props) { + if (!fieldSet.has(key)) { + throw new Error(`Unexpected field: ${key}`); + } } return isMutable ? Object.seal(obj) : Object.freeze(obj); } @@ -28,9 +35,11 @@ class Record { return Struct; } + // Оновлює мутабельний екземпляр. Залишається без змін, логіка коректна. + static update(instance, updates) { if (Object.isFrozen(instance)) { - throw new Error('Cannot mutate immutable Record'); + throw new Error('Cannot mutate an immutable Record instance'); } for (const key of Object.keys(updates)) { if (Reflect.has(instance, key)) { @@ -40,26 +49,41 @@ class Record { return instance; } + //Створює новий, незалежний екземпляр (імутабельне оновлення). + static fork(instance, updates) { - const obj = Object.create(null); - for (const key of Object.keys(instance)) { - obj[key] = Reflect.has(updates, key) ? updates[key] : instance[key]; - } - return Object.isFrozen(instance) ? Object.freeze(obj) : Object.seal(obj); + const newInstance = { ...instance, ...updates }; + return Object.isFrozen(instance) + ? Object.freeze(newInstance) + : Object.seal(newInstance); } - static branch(instance, updates) { - const obj = Object.create(instance); - for (const key of Object.keys(updates)) { - Reflect.defineProperty(obj, key, { - value: updates[key], - writable: true, - configurable: true, - enumerable: true, - }); - } - return Object.isFrozen(instance) ? Object.freeze(obj) : Object.seal(obj); - } +// Метод `branch` видалено. + } module.exports = { Record }; + +// Приклад використання оптимізованого коду + +const User = Record.immutable(['id', 'name', 'email']); +const user1 = User.create({ + id: 1, + name: 'Marcus', + email: 'marcus@metarhia.com', +}); + +const user2 = Record.fork(user1, { name: 'Marcus Aurelius' }); + +// `user1` залишився незмінним +const user3 = Record.fork(user2, { email: 'm.aurelius@rome.com', name: 'Marcus Aurelius Antoninus' }); + +console.log('User 1:', user1); +console.log('User 2:', user2); +console.log('User 3:', user3); + +try { + Record.update(user1, { name: 'FAIL' }); +} catch (e) { + console.error('\nSuccessfully caught error:', e.message); +} diff --git a/JavaScript/4-default.js b/JavaScript/4-default.js index 7485543..d44d792 100644 --- a/JavaScript/4-default.js +++ b/JavaScript/4-default.js @@ -1,113 +1,97 @@ 'use strict'; +function getType(value) { + if (Array.isArray(value)) return 'array'; + if (value === null) return 'null'; + return typeof value; +} + +function validateTypes(props, defaults) { + for (const key in props) { + if (Reflect.has(defaults, key)) { + const defaultValue = defaults[key]; + const newValue = props[key]; + const expectedType = getType(defaultValue); + const actualType = getType(newValue); + if (expectedType !== actualType) { + throw new TypeError( + `Invalid type for "${key}": expected ${expectedType}, got ${actualType}` + ); + } + } + } +} + + class Record { static immutable(defaults) { return Record.#build(defaults, false); } - static mutable(defaults) { return Record.#build(defaults, true); } - static #build(defaults, isMutable) { + // Заморожуємо структуру "класу", щоб уникнути її випадкової зміни const fields = Object.keys(defaults); - const defaultValues = Object.create(null); - for (const key of fields) { - defaultValues[key] = defaults[key]; - } + const frozenDefaults = Object.freeze({ ...defaults }); class Struct { - static fields = fields; - static defaults = defaultValues; + static fields = Object.freeze(fields); + static defaults = frozenDefaults; static mutable = isMutable; - static create(data = {}) { - const obj = Object.create(null); - - for (const key of fields) { - const base = defaultValues[key]; - const value = key in data ? data[key] : base; - - if (!Record.#sameType(base, value)) { - const exp = Record.#typeof(base); - const act = Record.#typeof(value); - throw new TypeError( - `Invalid type for "${key}": expected ${exp}, got ${act}`, - ); - } - - obj[key] = value; - } - - return isMutable ? Object.seal(obj) : Object.freeze(obj); + validateTypes(data, frozenDefaults); + + // Створюємо екземпляр декларативно, поєднуючи `defaults` і `data` + const newInstance = { ...frozenDefaults, ...data }; + return isMutable ? Object.seal(newInstance) : Object.freeze(newInstance); } } - return Struct; } - static #typeof(value) { - if (Array.isArray(value)) return 'array'; - if (value === null) return 'null'; - return typeof value; - } - - static #sameType(a, b) { - if (Array.isArray(a)) return Array.isArray(b); - if (a === null) return b === null; - return typeof a === typeof b; - } - - static #validate(instance, updates) { - for (const key of Object.keys(updates)) { - if (!Reflect.has(instance, key)) continue; - const current = instance[key]; - const next = updates[key]; - if (!Record.#sameType(current, next)) { - const exp = Record.#typeof(current); - const act = Record.#typeof(next); - throw new TypeError( - `Invalid type for "${key}": expected ${exp}, got ${act}`, - ); - } - } - } - static update(instance, updates) { if (Object.isFrozen(instance)) { - throw new Error('Cannot mutate immutable Record'); - } - Record.#validate(instance, updates); - for (const key of Object.keys(updates)) { - if (Reflect.has(instance, key)) { - instance[key] = updates[key]; - } + throw new Error('Cannot mutate an immutable Record'); } + validateTypes(updates, instance); + Object.assign(instance, updates); return instance; } static fork(instance, updates) { - Record.#validate(instance, updates); - const obj = Object.create(null); - for (const key of Object.keys(instance)) { - obj[key] = Reflect.has(updates, key) ? updates[key] : instance[key]; - } - return Object.isFrozen(instance) ? Object.freeze(obj) : Object.seal(obj); - } - - static branch(instance, updates) { - Record.#validate(instance, updates); - const obj = Object.create(instance); - for (const key of Object.keys(updates)) { - Reflect.defineProperty(obj, key, { - value: updates[key], - writable: true, - configurable: true, - enumerable: true, - }); - } - return Object.isFrozen(instance) ? Object.freeze(obj) : Object.seal(obj); + validateTypes(updates, instance); + const newInstance = { ...instance, ...updates }; + return Object.isFrozen(instance) + ? Object.freeze(newInstance) + : Object.seal(newInstance); } + + // Метод `branch` видалено. } module.exports = { Record }; + +// Приклад використання оптимізованого коду + +const User = Record.immutable({ + id: 0, + name: 'Guest', + email: null, + roles: ['guest'], +}); + +const user1 = User.create({ + id: 1, + name: 'Marcus', +}); + +console.log('User 1 (defaults applied):', user1); +const user2 = Record.fork(user1, { email: 'marcus@rome.com' }); +console.log('User 2 (forked):', user2); +console.log('User 1 (remains unchanged):', user1); +try { + User.create({ id: 2, name: 'Lucius', roles: 'admin' }); // roles має бути масивом +} catch (e) { + console.error('\nSuccessfully caught type error:', e.message); +}