Skip to content

Commit

Permalink
- .secluded(...)
Browse files Browse the repository at this point in the history
  • Loading branch information
eddow committed Nov 6, 2024
1 parent e572ad1 commit 77d35bf
Show file tree
Hide file tree
Showing 12 changed files with 102 additions and 75 deletions.
11 changes: 10 additions & 1 deletion .prettierrc
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,14 @@
"singleQuote": true,
"trailingComma": "none",
"printWidth": 100,
"semi": false
"semi": false,
"overrides": [
{
"files": "*.md",
"options": {
"useTabs": false,
"tabWidth": 4
}
}
]
}
3 changes: 3 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"cSpell.words": ["unproxied"]
}
63 changes: 29 additions & 34 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,12 @@ class C extends Diamond(A, B) { ... }
Yes, `super` still works and will have a dynamic meaning depending on where it is used.

```ts
import D from `flat-diamond`
import Diamond from `flat-diamond`

class X extends D() { method() {} }
class A extends D(X) { method() { [...]; super.method() } } // Here will be the change
class B extends D(X) { method() { [...]; super.method() } }
class C extends D(A, B) {}
class X extends Diamond() { method() {} }
class A extends Diamond(X) { method() { [...]; super.method() } } // Here will be the change
class B extends Diamond(X) { method() { [...]; super.method() } }
class C extends Diamond(A, B) {}
let testA = new A(), // A - X
testC = new C() // C - A - B - X
testA.method()
Expand Down Expand Up @@ -84,10 +84,10 @@ Ie., this is the difference between these two situations :

```ts
class A { constructor() {...} }
class Xa extends D(A) { constructor() { super(); ...} }
class Xa extends Diamond(A) { constructor() { super(); ...} }

class B extends D() { constructor() { super(); ...} }
class Xb extends D(B) { constructor() { super(); ...} }
class B extends Diamond() { constructor() { super(); ...} }
class Xb extends Diamond(B) { constructor() { super(); ...} }

let testA = new Xa(),
testB = new Xb()
Expand All @@ -97,15 +97,15 @@ When constructing `Xa`, the constructor of `A` will be invoked with `this` being

When constructing `Xb`, the constructor of `B` will be invoked with `this` being (after `super()`) the constructed diamond (here, `Xb`)

> Note: This is the only difference made by using `extend D()` for root classes
> Note: This is the only difference made by using `extend Diamond()` for root classes
## What are the limits ?

We can of course `extend Diamond(A, B, C, D, E, ...theRest)`.

### `instanceof` does not work anymore!?

Yes, it does. Classes involved in the `Diamond` process even have their `[Symbol.hasInstance]` overridden in order to be sure.
Yes, it does. Classes involved in the `Diamond` process even have their `[Symbol.hasInstance]` overridden in order to take the new structure into account.

### But I modify my prototypes dynamically...

Expand All @@ -118,28 +118,23 @@ Resolved by stating that the argument order of the function `Diamond(...)` is _c
```ts
class X1 { ... }
class X2 { ... }
class D1 extends D(X1, X2) { ... }
class D1 extends Diamond(X1, X2) { ... }
class X3 { ... }
class X4 { ... }
class D2 extends D(X1, X3, D1, X4, X2)
class D2 extends Diamond(X1, X3, D1, X4, X2)
```
Here, the flat legacy of `D2` will be `D1 - X1 - X3 - X2 - X4`. The fact that `D1` specifies it inherits from `X1` is promised to be kept, the order in the arguments is surely going to happen if the situation is not too complex.
A real order conflict would imply circular reference who is impossible.
> For the details if interested - using the vertical analogy that "classes are built upon an ancestor" (high=descendant) :
>
> - Inheritance is promised (if a class is built upon another one, it will appear higher in the flat legacy)
> - When two classes are given in a list (knowing each classes are flat lineage), the highest class of the first lineage will appear before the lowest class of the second one.
### Dealing with non-`Diamond`-ed classes
```ts
class X extends D() { ... }
class X extends Diamond() { ... }
class Y extends X { ... }
class Z extends X { ... }
class A extends D(X, Y) { ... }
class A extends Diamond(X, Y) { ... }
```
Well, the constructor and `super.method(...)` of `X` will be called twice.... Like if it did not extend `D()`
Expand Down Expand Up @@ -168,10 +163,10 @@ When a `Diamond`-ed class constructor passes an argument to `super`, this argume

```ts
class X1 { constructor(n: number) { ... } }
class D1 extends D(X1) { constructor(n: number) { super(n+1); ... } }
class D1 extends Diamond(X1) { constructor(n: number) { super(n+1); ... } }
class X2 { constructor(n: number) { ... } }
class X3 extends X2 { constructor(n: number) { super(n+2); ... } }
class D2 extends D(X3, D1) { constructor(n: number) { super(n+1); ... } }
class D2 extends Diamond(X3, D1) { constructor(n: number) { super(n+1); ... } }

let test = new D2(0)
```
Expand All @@ -188,7 +183,7 @@ D2(0)
#### Non diamond vs diamond constructors
> Diamond' constructors du not really matter if a class is "descendant" somehow in the 2D hierarchy, only in the 1D (flat) one. It means that any constructor transforming the arguments transform it for _all_ the classes that comes afterward in the flat hierarchy. Thus, classes that are no `Diamond`ed will not modify the constructor' arguments, but only for their direct descendants. Also, if their descendants appear twice (inherited directly twice in the hierarchy), their constructors will be called twice, each time for each argument.
> Diamond' constructors do not really matter if a class is "descendant" somehow in the 2D hierarchy, only in the 1D (flat) one. It means that any constructor transforming the arguments transform it for _all_ the classes that comes afterward in the flat hierarchy. Thus, classes that are no `Diamond`ed will not modify the constructor' arguments, but only for their direct descendants. Also, if their descendants appear twice (inherited directly twice in the hierarchy), their constructors will be called twice, each time for each argument.
### Construction concern
Expand All @@ -208,7 +203,7 @@ Don't make field conflicts. Just don't.
Here it is tricky, and that's where _seclusion_ comes in. Let's speak about seclusion without speaking of diamond - and, if you wish, the seclusion works without the need of involving `Diamond`. (though it is also completely integrated)
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 mounted him, the `Plane` one)
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 mounted on him, the `Plane` one)
A pure and simple `class DuckCourier extends Plane` would have a field conflict. So, instead, seclusion will be used :
Expand All @@ -218,30 +213,30 @@ import { Seclude } from 'flat-diamond'
class DuckCourier extends Seclude(Plane, ['wingSpan']) { ... }
```
As simple as that, methods (as well as accessors) of `Plane` and `DuckCourier` will access two different values when accessing `this.wingSpan`
Et voilà, 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 secluded class is implemented (here, a `Plane`), the instance prototype will be replaced by `this` (so, here, a `DuckCourier`) mixed with some `Proxy` voodoo to manage who is `this` in method/accessor calls (either `DuckCourier` or `Secluded<Plane>`) - et voilà!
Because of prototyping, `Secluded<Plane>` has access to all the functionalities of `DuckCourier` (and therefore of `Plane`) while never interfering with `DuckCourier::wingSpan`. Also, having several secluded 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 `secluded` exposed by the `Secluded` class.
`DuckCourier` on another hand, _can_ interfere with `Plane::wingSpan` if needed thanks to the fact a `Secluded` class is also a function to retrieve a private part.
```ts
import { Seclude } from 'flat-diamond'

class Plane {
wingSpan: number = 200
wingSpan: number = 200
}

const MountedPlane = Seclude(Plane, ['wingSpan'])

class DuckCourier extends MountedPlane {
wingSpan: number = 80
get isDeviceSafe(): boolean {
return MountedPlane.secluded(this).wingSpan > 2 * this.wingSpan
}
wingSpan: number = 80
get isDeviceSafe(): boolean {
return MountedPlane(this).wingSpan > 2 * this.wingSpan
}
}
```
Expand All @@ -267,10 +262,10 @@ In some production environment, these solutions might be preferred.
Knowing that the whole documentation here is about rarely occurring edge cases and that [two chapters](#super) are usually enough to understand and use the library, `flat-diamond` here wish to offer:
- A clean and working way to approach multiple inheritance - theoretically and practically
- A tool to write a highly readable, maintainable and dynamic code. Mainly for prototyping but that can fit in most production case.
> Some times, it's cheaper to just buy another RAM stick than to spend a week on an optimization
- Automate bookkeeping of types - task that can be tedious in TypeScript
- A clean and working way to approach multiple inheritance - theoretically and practically
- A tool to write a highly readable, maintainable and dynamic code. Mainly for prototyping but that can fit in most production case.
> Some times, it's cheaper to just buy another RAM stick than to spend a week on an optimization
- Automate bookkeeping of types - task that can be tedious in TypeScript
`flat-diamond` does not create security issues nor performance bottlenecks. It might add ~5Kb of code to load, it might add a little overtime on some function calls - but if a code does a bit more than calling NOOP billions of times, it is negligible.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "flat-diamond",
"version": "1.0.8",
"version": "1.0.9",
"types": "./lib/index.d.ts",
"exports": {
".": {
Expand Down
8 changes: 7 additions & 1 deletion src/diamond.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,11 @@ const diamondHandler: {
}
}

/**
* When secluding a class whose linear legacy ends on a diamond, this is used not to seclude further than the diamond
*/
export let lastDiamondProperties: PropertyDescriptorMap | null

export default function Diamond<TBases extends Ctor[]>(
...baseClasses: TBases
): Newable<HasBases<TBases>> {
Expand All @@ -71,6 +76,7 @@ export default function Diamond<TBases extends Ctor[]>(
const myResponsibility: Ctor[] = []
class Diamond {
constructor(...args: any[]) {
lastDiamondProperties = null
const responsibility = buildingDiamond
? buildingDiamond!.strategy.get(this.constructor as Ctor)!
: myResponsibility
Expand Down Expand Up @@ -112,6 +118,7 @@ This might happen if a diamond is created from another constructor before its 's
//In the constructor method and in the field initializers, we can build diamonds, but not *this* diamond
buildingDiamond = null
}
lastDiamondProperties = Object.getOwnPropertyDescriptors(locallyStoredDiamond.built)
// Value used by `this` on `super(...)` return
return locallyStoredDiamond.built
}
Expand All @@ -131,6 +138,5 @@ This might happen if a diamond is created from another constructor before its 's
}

Object.setPrototypeOf(Diamond.prototype, new Proxy(Diamond, diamondHandler))

return <new (...args: any[]) => HasBases<TBases>>(<unknown>Diamond)
}
28 changes: 19 additions & 9 deletions src/seclude.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import Diamond from './diamond'
import Diamond, { lastDiamondProperties } from './diamond'
import { Ctor, KeySet, Newable } from './types'
import {
allFLegs,
Expand Down Expand Up @@ -26,7 +26,7 @@ export type Secluded<
TBase extends Ctor,
Keys extends (keyof InstanceType<TBase>)[]
> = SecludedClass<TBase, Keys> & {
secluded(obj: InstanceType<SecludedClass<TBase, Keys>>): InstanceType<TBase> | undefined
(obj: InstanceType<SecludedClass<TBase, Keys>>): InstanceType<TBase> | undefined
}
export function Seclude<TBase extends Ctor, Keys extends (keyof InstanceType<TBase>)[]>(
base: TBase,
Expand All @@ -38,6 +38,8 @@ export function Seclude<TBase extends Ctor, Keys extends (keyof InstanceType<TBa
{}
),
initPropertiesBasket: BasketBall[] = []
const privates = new WeakMap<GateKeeper, TBase>(),
diamondSecluded = !fLegs(base)
/**
* In order to integrate well in diamonds, we need to be a diamond
* When we create a diamond between the Secluded and the base, the private properties of the base *have to*
Expand All @@ -52,17 +54,18 @@ export function Seclude<TBase extends Ctor, Keys extends (keyof InstanceType<TBa
for (const p in secludedProperties)
if (p in allProps) {
privateProperties[p] = allProps[p]
delete this[p]
// If we seclude, we seclude only until the next diamond
if (!diamondSecluded && lastDiamondProperties?.[p])
Object.defineProperty(this, p, lastDiamondProperties[p])
else delete this[p]
}
}
}
const privates = new WeakMap<GateKeeper, TBase>(),
diamondSecluded = !fLegs(base),
diamond = diamondSecluded ? Diamond(PropertyCollector) : PropertyCollector
const diamond = diamondSecluded ? Diamond(PropertyCollector) : PropertyCollector
class GateKeeper extends diamond {
static secluded(obj: TBase): TBase | undefined {
/*static (obj: GateKeeper): TBase | undefined {
return privates.get(obj)
}
}*/
constructor(...args: any[]) {
const init: BasketBall = { privateProperties: {} }
initPropertiesBasket.unshift(init)
Expand Down Expand Up @@ -98,6 +101,13 @@ export function Seclude<TBase extends Ctor, Keys extends (keyof InstanceType<TBa
privates.set(this, secluded)
}
}
// In order to be treated as a function (to retrieve the private part), we have to use yet another proxy
const GateKeeperProxy = new Proxy(GateKeeper, {
apply(target, thisArg, argArray) {
return privates.get(argArray[0])
}
})
GateKeeper.prototype.constructor = GateKeeperProxy
function whoAmI(receiver: TBase) {
const domain = privates.has(receiver)
? 'public'
Expand Down Expand Up @@ -207,5 +217,5 @@ export function Seclude<TBase extends Ctor, Keys extends (keyof InstanceType<TBa
getPrototypeOf: (target) => diamond.prototype
})
Object.setPrototypeOf(GateKeeper.prototype, fakeCtor.prototype)
return GateKeeper as any
return GateKeeperProxy as any
}
4 changes: 3 additions & 1 deletion src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,9 @@ export function hasInstanceManager<Class extends Ctor>(
if (!obj || typeof obj !== 'object') return false
if (inheritsFrom(obj.constructor)) return true
const fLeg = fLegs(obj.constructor)
return Boolean(fLeg && fLeg.some(inheritsFrom))
if (fLeg && fLeg.some(inheritsFrom)) return true
const protoObj = Object.getPrototypeOf(obj)
return obj.constructor.prototype !== protoObj && protoObj instanceof cls
}
}
export const hasInstanceManagers = new WeakSet<Ctor>()
Expand Down
14 changes: 7 additions & 7 deletions test/animal.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import D from '../src'
import Diamond from '../src'
import { log, logs } from './logger'

class Animal extends D() {
class Animal extends Diamond() {
get actions(): string[] {
return ['eat', 'sleep']
}
Expand All @@ -10,7 +10,7 @@ class Animal extends D() {
}
}

class FlyingAnimal extends D(Animal) {
class FlyingAnimal extends Diamond(Animal) {
get actions(): string[] {
return [...super.actions, 'fly']
}
Expand All @@ -20,7 +20,7 @@ class FlyingAnimal extends D(Animal) {
}
}

class SwimmingAnimal extends D(Animal) {
class SwimmingAnimal extends Diamond(Animal) {
get actions(): string[] {
return [...super.actions, 'swim']
}
Expand All @@ -31,7 +31,7 @@ class SwimmingAnimal extends D(Animal) {
}
}

class WalkingAnimal extends D(Animal) {
class WalkingAnimal extends Diamond(Animal) {
get actions(): string[] {
return [...super.actions, 'walk']
}
Expand All @@ -42,7 +42,7 @@ class WalkingAnimal extends D(Animal) {
}
}

class Duck extends D(FlyingAnimal, SwimmingAnimal, WalkingAnimal) {
class Duck extends Diamond(FlyingAnimal, SwimmingAnimal, WalkingAnimal) {
doIt() {
log('KWAK')
super.doIt()
Expand All @@ -61,7 +61,7 @@ test('inheritance', () => {
duck.doIt()
expect(logs()).toEqual(['KWAK', 'woosh', 'fshhh', 'picpoc', 'mniom'])

const beaver = new (D(WalkingAnimal, SwimmingAnimal))()
const beaver = new (Diamond(WalkingAnimal, SwimmingAnimal))()
expect(beaver.actions).toEqual(['eat', 'sleep', 'swim', 'walk'])
beaver.doIt()
expect(logs()).toEqual(['picpoc', 'fshhh', 'mniom'])
Expand Down
Loading

0 comments on commit 77d35bf

Please sign in to comment.