Nauczyliśmy się sporo, ale ten projekt ma pewne wady konstrukcyjne. Teraz sobie wyjaśnimy co jest nie tak i jak to naprawimy.
Generalnie chodzi o to, że informacje z internetu, i to nie te najgorsze tutoriale, ale płatne kursy lub artykuły na ładnych portalach, gdzie się ktoś z nazwiska podpisuje, są pełne bełkotu, który ktoś „przetestował” na jednym przykładzie i mu się wydaje, że to jest to, co należy pokazywać.
Zobaczmy ten autobind:
function Autobind(_: any, _2: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
const adjDescriptor: PropertyDescriptor = {
configurable: true,
enumerable: false,
get() {
const boundFn = originalMethod.bind(this);
return boundFn;
}
};
return adjDescriptor;
}
To nie będzie działać. Raz, że event listenery (remove) nie zadziała, bo to będzie wiecznie inna funkcja, dwa, że nawet jakiś memory leak może się przytrafić.
Ok, tu lepsza wersja:
const IDENTIFIER = `@typed-decorators/autobind`;
export function autobind<F extends (...args: Array<any>) => any>(
_target: any,
name: string,
descriptor: TypedPropertyDescriptor<F>,
): TypedPropertyDescriptor<F> {
const { enumerable, configurable, value } = descriptor;
const boundMethod = Symbol(`${IDENTIFIER}/${name}`);
return {
enumerable,
configurable,
get(this: { [boundMethod]: any }) {
return this[boundMethod] || (this[boundMethod] = value!.bind(this));
},
set(value: F) {
Object.defineProperty(this, name, {
writable: true,
enumerable: true,
configurable: true,
value,
});
},
};
}
Aczkolwiek ten setter to nie wiem po co. Tak, działa, ale traci binding to this po ustawieniu. I szczerze nie wiem, jak by to można było naprawić. W sumie to bez sensu, nadpisywać, ale autor tego kodu (biblioteczka służąca tylko do tego) się z tym setterem pofatygował.
No dobra. To z czym problem? Z tym (pamietamy ten kod):
@WithRender('ul-app')
class PersonItem {
@required
public name: string;
// @aboveZero
@min(1)
@max(100)
public age: number;
constructor(name: string, age: number){
this.name = name;
this.age = age;
}
@enumerable(true)
@deprecated
sayHello() {
console.log(`Hello, my name is ${this.name}!`);
}
}
Zdejmijmy withRender, bo on nam przysłania coś. A mianowicie to, że age nie jest w obiekcie, tylko w prototypie i jest współdzielone przez wszystkie obiekty. WithRender działa jeszcze zanim to się stanie, więc on tego nie widzi, do DOM wrzuca to, co ma tam być.
Ale już potem jak są definiowane te dekoratory…. Tak wygląda np. min:
function min(minNum: number) {
return function(target: any, key: string) {
let possibleSetter = Object.getOwnPropertyDescriptor(target, key)?.set
if(possibleSetter === undefined){
let currentValue = target[key];
Object.defineProperty(target, key, {
set: (newValue: number) => {
if (newValue < minNum) {
throw new Error(`${key} is too small!`);
}
currentValue = newValue;
},
get: () => currentValue,
configurable: true
});
} else if (possibleSetter){
Object.defineProperty(target, key, {
set: (newValue: number) => {
if (newValue < minNum) {
throw new Error(`${key} is too small!`);
}
possibleSetter!(newValue);
},
configurable: true
});
}
}
}
I nie licząc stackowania tych dekoratorów, co sam ogarnąłem metodą prób i błędów, ale coś mi śmierdziało, to robienie Object.defineProperty i cały ten szablon na „walidatory przy użyciu dekoratorów” to jest kod z internetu, z ładnego portalu, zdaje się podpisany przez kogoś z imienia i nazwiska, zdaje się programisty z długim doświadczeniem (to są ci „super obyci” seniorzy, blokujący „nic nie umiejącym” would-be juniorom miejsce w pracy? xD).
No właśnie. To jest głupie i efekt tego jest taki, że wszystkie obiekty współdzielą to property. To teraz sobie zróbmy kilka prób:
function Atempt1(min:Number){
return function(target: any, propName: string) {
if(target.validatorRules === undefined){
target.validatorRules = {
[propName]: {}
};
}
let rules = target.validatorRules;
target.validatorRules[propName] = {...rules[propName], min: min};
console.log(target.constructor);
}
}
Mi się nie chce dużo pisać, rozpisywać się, bo ja więcej koduję, a mniej publikuję ładnych tekstów od linijki (z kodem, który nie został przetestowany, albo został przetestowany raz i na jednym przykładzie).
Cóż, tu chodzi o to, że właśnie wykorzystamy tę współdzieloność bo target to jest właśnie to. Do obiektu nie mamy dostępu i naprawdę nie wiem, co ktoś w głowie miał z tym Object.defineProperty, nieważne…
Tu jest drugi dekorator:
function Atempt2(max: number){
return function(target: any, propName: string) {
if(target.validatorRules === undefined){
target.validatorRules = {
[propName]: {}
};
}
let rules = target.validatorRules;
target.validatorRules[propName] = {...rules[propName], max: max};
console.log(target.constructor);
}
}
Ok, czyli to jest tam zapisywane. Teraz co zrobimy dalej:
Zrobimy dekorator klasowy. ValidatableClass albo coś takiego. I on wstrzyknie:
- Metodę validate, która przeleci po zasadach i sprawdzi, czy wszystko gra
- Użycie tej metody w konstruktorze
- Być może return proxy w konstruktorze, które będzie walidowało zawsze zanim puści dalej reflect set
Muszę to głęboko przemyśleć, morał jest taki, nie ufajcie tekstom z internetu, większość programistów nawet po 10 latach nie wie co robi, zwłaszcza jak piszą o tym to nie pilnują jakie rady dają, więcej wiary we własne umiejętności i kombinowania, mniej wiary w jakieś tutaj skrypty, które ktoś napisał.