diff --git a/README.md b/README.md index a2d98a3..6331202 100644 --- a/README.md +++ b/README.md @@ -209,8 +209,60 @@ import D, { constructedObject } from 'flat-diamond' ``` > Note: There is no need to modify it directly, all the properties initialized on the temporary object are going to be transposed on it +> Note: `constructedObject(this)` will return a relevant value _only_ in classes extending `Diamond(...)` after `super(...)` -## Participation +# Protection + +Another big deal of diamond inheritance is variable conflicts. + +## Easy case + +You write all the classes and, when remembering a `wingSpan`, you know that a plane is not a duck. You either have a plane wit a duck inside (property) or a duck that imitates a plane - but you don't confuse your `wingSpan`s. + +Don't make field conflicts. Just don't. + +## Yes, but it's a library I don't write + +Here it is tricky, and that's where _protection_ comes in. Let's speak about protection without speaking of diamond - and, if you wish, the protection works without the need of involving `Diamond`. + +Let's say we want a `DuckCourier` to implement `Plane`, and end up with a conflict of `wingSpan` (the one of the duck and the one of the device strapped on him, the `Plane` one) + +A pure and simple `class DuckCourier extends Plane` would have a field conflict. So, instead, protection will be used : + +```ts +import { Protect } from 'flat-diamond' + +class DuckCourier extends Protect(Plane, ['wingSpan']) { ... } +``` + +As simple as that, methods (as well as accessors) of `Plane` and `DuckCourier` will access two different values when accessing `this.wingSpan` + +## But ... How ? And, how can I ... + +When a protected class is implemented, `this` (so, here, a `DuckCourier`) will be used as the prototype for a `Private`. A Proxy is added between `Protected` and `Plane` to manage who is `this` in method calls (either `DuckCourier` or `Private`) - et voilĂ ! + +Because of prototyping, `Private` has access to all the functionalities of `DuckCourier` (and therefore of `Plane`) while never interfering with `DuckCourier::wingSpan`. Also, having several protected class in the legacy list will only create several "heads" who will share a prototype. + +`DuckCourier` on another hand, _can_ interfere with `Plane::wingSpan` if needed thanks to the `privatePart` exposed by the protected class. + +```ts +import { Protect } from 'flat-diamond' + +class Plane { + wingSpan: number = 200 +} + +const ProtectedPlane = Protect(Plane, ['wingSpan']) + +class DuckCourier extends ProtectedPlane { + wingSpan: number = 80 + get isDeviceSafe(): boolean { + return ProtectedPlane.privatePart(this).wingSpan > 2 * this.wingSpan + } +} +``` + +# Participation The best (even if not only) way to report a bug is to PR a failing unit test. diff --git a/package.json b/package.json index 5fe4796..5e7bdbe 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,6 @@ "class inheritance", "oop" ], - "types": "src/types.d.ts", "main": "src/index.ts", "scripts": { "test": "jest", diff --git a/src/helpers.ts b/src/helpers.ts index fa50a54..8d71431 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -6,8 +6,8 @@ import { fLegs, temporaryBuiltObjects } from './utils' * @param obj `this` - the object being constructed by this constructor * @returns */ -export function constructedObject(obj: object) { - return temporaryBuiltObjects.get(obj) || obj +export function constructedObject(obj: Class): Class { + return (temporaryBuiltObjects.get(obj) as Class) || obj } /** diff --git a/src/protect.ts b/src/protect.ts index 4a3875f..f1c0046 100644 --- a/src/protect.ts +++ b/src/protect.ts @@ -1,6 +1,6 @@ import Diamond, { diamondHandler } from './diamond' import { constructedObject } from './helpers' -import { allFLegs, bottomLeg, nextInLine } from './utils' +import { allFLegs, bottomLeg, fLegs, nextInLine } from './utils' const publicPart = (x: Ctor): Ctor => Object.getPrototypeOf(Object.getPrototypeOf(x)) @@ -9,17 +9,41 @@ export function Protect { const protectedProperties: KeySet = properties.reduce( - (acc, p) => ({ ...acc, [p]: true }) as KeySet, - {} - ) + (acc, p) => ({ ...acc, [p]: true }) as KeySet, + {} + ), + initPropertiesBasket: PropertyDescriptorMap[] = [] + /** + * In order to integrate well in diamonds, we need to be a diamond + * When we create a diamond between the Protected and the base, the private properties of the base *have to* + * be collected before the diamond propagate them to the `constructedObject` + */ + abstract class PropertyCollector extends base { + constructor(...args: any[]) { + super(...args) + const init = initPropertiesBasket[0], + allProps = Object.getOwnPropertyDescriptors(this) + for (const p in protectedProperties) + if (p in allProps) { + init[p] = allProps[p] + delete this[p] + } + } + } const privates = new WeakMap() - const diamond = Diamond(base) as TBase + const diamond = fLegs(base) ? PropertyCollector : (Diamond(PropertyCollector) as TBase) class Protected extends (diamond as any) { static privatePart(obj: TBase): TBase | undefined { return privates.get(obj) } constructor(...args: any[]) { - super(...args) + const init: PropertyDescriptorMap = {} + initPropertiesBasket.unshift(init) + try { + super(...args) + } finally { + initPropertiesBasket.shift() + } const actThis = constructedObject(this) privates.set( actThis, @@ -36,7 +60,8 @@ export function Protect target - }) + }), + init ) ) } @@ -47,7 +72,7 @@ export function Protect fakeCtor.prototype getPrototypeOf: (target) => diamond.prototype }) Object.setPrototypeOf(Protected.prototype, fakeCtor.prototype) diff --git a/test/protect.test.ts b/test/protect.test.ts index eca7734..f19e892 100644 --- a/test/protect.test.ts +++ b/test/protect.test.ts @@ -1,14 +1,46 @@ import Diamond, { Protect } from '../src' +interface Scenario { + pubFld: number + prvFld: number + setPrvFld(v: number): void + getPrvFld(): number + setPubFld(v: number): void + getPubFld(): number + + get accFld(): number + set accFld(v: number) +} + +function testScenario(t: Scenario, P: { privatePart(t: any): any }) { + expect(t.pubFld).toBe(0) + expect(t.accFld).toBe(8) + expect(t.prvFld).toBe(10) + t.pubFld = 1 + expect(t.getPubFld()).toBe(1) + t.prvFld = 2 + t.accFld = 3 + expect(t.prvFld).toBe(2) + expect(t.getPrvFld()).toBe(3) + t.setPubFld(4) + expect(t.pubFld).toBe(4) + t.setPrvFld(5) + expect(t.accFld).toBe(5) + expect(t.prvFld).toBe(2) + const pp = P.privatePart(t) + expect(pp?.prvFld).toBe(5) + expect(pp?.pubFld).toBe(4) +} + test('leg-less', () => { class X { pubFld = 0 - prvFld = 0 + prvFld = 8 setPrvFld(v: number) { this.prvFld = v } getPrvFld() { - return this.prvFld + return this.accFld } setPubFld(v: number) { this.pubFld = v @@ -26,30 +58,16 @@ test('leg-less', () => { } const P = Protect(X, ['prvFld']) class Y extends P { - prvFld = 0 + prvFld = 10 } - const t = new Y() - t.pubFld = 1 - expect(t.getPubFld()).toBe(1) - t.prvFld = 2 - t.accFld = 3 - expect(t.prvFld).toBe(2) - expect(t.getPrvFld()).toBe(3) - t.setPubFld(4) - expect(t.pubFld).toBe(4) - t.setPrvFld(5) - expect(t.accFld).toBe(5) - expect(t.prvFld).toBe(2) - const pp = P.privatePart(t) - expect(pp?.prvFld).toBe(5) - expect(pp?.pubFld).toBe(4) + testScenario(new Y(), P) }) -test('leg-full', () => { +test('leg-half', () => { class X { pubFld = 0 - prvFld = 0 + prvFld = 8 setPrvFld(v: number) { this.prvFld = v } @@ -72,23 +90,46 @@ test('leg-full', () => { } const P = Protect(X, ['prvFld']) class Y { - prvFld = 0 + prvFld = 10 } class D extends Diamond(P, Y) {} + class E extends Diamond(Y, P) {} - const t = new D() - t.pubFld = 1 - expect(t.getPubFld()).toBe(1) - t.prvFld = 2 - t.accFld = 3 - expect(t.prvFld).toBe(2) - expect(t.getPrvFld()).toBe(3) - t.setPubFld(4) - expect(t.pubFld).toBe(4) - t.setPrvFld(5) - expect(t.accFld).toBe(5) - expect(t.prvFld).toBe(2) - const pp = P.privatePart(t) - expect(pp?.prvFld).toBe(5) - expect(pp?.pubFld).toBe(4) + testScenario(new D(), P) + testScenario(new E(), P) +}) + +test('leg-full', () => { + class X extends Diamond() { + pubFld = 0 + prvFld = 8 + setPrvFld(v: number) { + this.prvFld = v + } + getPrvFld() { + return this.prvFld + } + setPubFld(v: number) { + this.pubFld = v + } + getPubFld() { + return this.pubFld + } + + get accFld() { + return this.prvFld + } + set accFld(v: number) { + this.prvFld = v + } + } + const P = Protect(X, ['prvFld']) + class Y { + prvFld = 10 + } + class D extends Diamond(P, Y) {} + class E extends Diamond(Y, P) {} + + testScenario(new D(), P) + testScenario(new E(), P) })