Getters, Setters, and Organizing Responsibility in JavaScript

Dawno, dawno temu,istniał język C, 1 i ten język miał coś o nazwie struct, I można go użyć do tworzenia heterogenicznie zagregowanych struktur danych, które miały członków. Kluczową rzeczą, jaką należy wiedzieć o C jest to, że gdy masz strukturę o nazwie currentUser I członka takiego jak id, I piszesz coś takiego jak currentUser.id = 42, kompilator C przekształcił to w niezwykle szybkie instrukcje asemblera. To samo dla int id = currentUser.id.

ważne było również to, że możesz mieć wskaźniki do funkcji w strukturach, więc możesz pisać rzeczy takie jakcurrentUser->setId(42), jeśli wolisz, aby ustawienieid było funkcją, co również zostało przetłumaczone na szybki asembler.

i wreszcie, programowanie w języku C ma bardzo silną kulturę preferowania „ekstremalnie szybkiego” zamiast po prostu „szybkiego”, a więc jeśli chcesz zwrócić uwagę programisty C, musisz upewnić się, że nigdy nie robisz czegoś, co jest po prostu szybkie, kiedy możesz zrobić coś, co jest ekstremalnie szybkie. Jest to oczywiście uogólnienie. Jestem pewien, że jeśli popytamy, w końcu spotkamy obu programistów C, którzy wolą eleganckie abstrakcje od niezwykle szybkiego kodu.

Flathead Dragster

java i javascript

wtedy był język zwany Java, który został zaprojektowany do działania w przeglądarkach i był przenośny na wszelkiego rodzaju sprzęcie i systemach operacyjnych, a jednym z jego celów było nakłonienie programistów C do pisania kodu Java w przeglądarce zamiast pisania C, które żyło we wtyczce. A raczej, to była jedna z jego strategii, celem było, aby Sun Microsystems pozostał istotny w świecie, który Microsoft był commoditizing, ale to jest kolejny rozdział książki historycznej.

więc mili ludzie stojący za Javą dali jej składnię podobną do C z nawiasami klamrowymi i dychotomią wypowiedzi/wyrażenia i notacją kropek. Mają one „obiekty” zamiast struktur, a obiekty mają o wiele więcej wspólnego niż struktury, ale projektanci Javy dokonali rozróżnienia pomiędzy currentUser.id = 42 I currentUser.setId(42) i upewnili się, że jeden jest niezwykle szybki, a drugi po prostu szybki. A raczej, ten jeden był szybki, a drugi był po prostu ok w porównaniu do C, ale programiści C mogli poczuć się jak robią ważne myślenie przy podejmowaniu decyzji, czy id powinien być bezpośrednio Dostępny dla wydajności lub pośrednio Dostępny dla elegancji i elastyczności.

historia pokazała, że był to właściwy sposób na sprzedaż nowego języka. Historia pokazała również, że rzeczywiste rozróżnienie performance było nieistotne dla prawie wszystkich. Wydajność jest tylko na teraz, elastyczność kodu jest na zawsze.

Cóż, okazało się, że Sun miał rację co do tego, że programiści C używali Javy (u mnie zadziałało, porzuciłem CodeWarrior i Lightspeed C), ale myliłem się co do używania Javy w przeglądarkach. Zamiast tego ludzie zaczęli używać innego języka o nazwie JavaScript do pisania kodu w przeglądarkach i używali Javy do pisania kodu na serwerach.

czy zaskoczy Cię informacja, że JavaScript został również zaprojektowany po to, aby Programiści C pisali kod? I że to poszło ze składnią podobną do C z nawiasami klamrowymi, dychotomią wypowiedzi/wyrażenia i notacją kropkową? I chociaż JavaScript ma coś,co jest trochę jak obiekt Java i trochę jak słownik Smalltalk, czy zaskoczy Cię, że JavaScript ma również rozróżnienie między currentUser.id = 42 I currentUser.setId(42)? I to początkowo, jeden był powolny, a drugi pies-powolny, ale programiści mogli zrobić ważne myślenie o tym, kiedy zoptymalizować pod kątem wydajności, a kiedy dać hoot o rozsądku programisty?

Nie, Nie zdziwi Cię, że działa trochę jak C w taki sam sposób, jak Java kind-sort działa jak C i z dokładnie tego samego powodu. A powód nie ma już znaczenia.

profesor Frink o Javie

problem z bezpośrednim dostępem

wkrótce po tym, jak ludzie zaczęli pracować z Javą na dużą skalę, dowiedzieli się, że bezpośredni dostęp do zmiennych instancji był okropnym pomysłem. Kompilatory JIT zawęziły różnicę w wydajności pomiędzy currentUser.id = 42I currentUser.setId(42)do prawie nic istotnego dla nikogo, a kod używający currentUser.id = 42lub int id = currentUser.id był wyjątkowo nieelastyczny.

nie było sposobu, aby udekorować takie operacje przekrojowymi problemami, takimi jak logowanie lub Walidacja. Nie można nadpisać zachowania ustawiania lub uzyskiwania id w podklasie. (Programiści Javy uwielbiają podklasy!)

w międzyczasie Programiści JavaScript również pisali currentUser.id = 42, I w końcu również odkryli, że był to okropny pomysł. Jednym z katalizatorów zmian było pojawienie się frameworków dla aplikacji JavaScript po stronie klienta. Załóżmy, że mamy śmiesznie prostą klasę person:

class Person { constructor (first, last) { this.first = first; this.last = last; } fullName () { return `${this.first} ${this.last}`; }};

i równie śmieszny widok:

class PersonView { constructor (person) { this.model = person; } // ... redraw () { document .querySelector(`person-${person.id}`) .text(person.fullName()) }}

za każdym razem, gdy aktualizujemy klasę person, musimy pamiętać o przerysuj widok:

const currentUser = new Person('Reginald', 'Braithwaite');const currentUserView = new PersonView(currentUser);currentUserView.redraw();currentUser.first = 'Ragnvald';currentUserView.redraw();

Dlaczego to ma znaczenie?

cóż, jeśli nie możesz kontrolować, gdzie określone obowiązki są obsługiwane, nie możesz tak naprawdę zorganizować swojego programu. Podklasy, metody, mixiny i dekoratory to techniki: umożliwiają one wybór, który Kod odpowiada za jaką funkcjonalność.

i na tym polega programowanie: porządkowanie funkcjonalności. Bezpośredni dostęp nie pozwala na organizowanie funkcji związanych z uzyskiwaniem i ustawianiem właściwości, zmusza kod wykonujący pobieranie i ustawianie do odpowiedzialności za wszystko inne związane z uzyskiwaniem i ustawianiem.

pamięć Magnetyczna rdzenia

Pobierz i ustaw

autorzy bibliotek JavaScript nie musieli długo zastanawiać się, jak to usunąć, używając metodyget Iset. Rozebrany do nagich podstaw dla celów ilustracyjnych, możemy napisać to:nasza nowa klasa superclass pozwala obiektom na ręczne nasłuchiwanie plikówget

Modelsuperclass pozwala na ręczne nasłuchiwanie plikówgetIset/div > metody na modelu. Jeśli są wywołane, „słuchacze”są powiadamiani za pomocą metody.notifyAll. Używamy tego, aby miećPersonViewnasłuchać jejPersonI wywołać własną metodę.redraw, gdy właściwość jest ustawiona za pomocą.set.

więc możemy pisać:

const currentUser = new Person('Reginald', 'Braithwaite');const currentUserView = new PersonView(currentUser);currentUser.set('first', 'Ragnvald');

i nie musimy wywoływaćcurrentUserView.redraw(), ponieważ powiadomienie wbudowane w.set robi to za nas.

możemy robić inne rzeczy z.get I.set, oczywiście. Teraz, gdy są to metody, możemy ozdobić je logowaniem lub walidacją, jeśli zdecydujemy. Metody sprawiają, że nasz kod jest elastyczny i otwarty na rozszerzenie. Na przykład, możemy użyć ES.później dekorator dodaje poradę logowania do .set:

const after = (behaviour, ...methodNames) => (clazz) => { for (let methodName of methodNames) { const method = clazz.prototype; Object.defineProperty(clazz.prototype, methodName, { value: function (...args) { const returnValue = method.apply(this, args); behaviour.apply(this, args); return returnValue; }, writable: true }); } return clazz; }function LogSetter (model, property, value) { console.log(`Setting ${property} of ${model.fullName()} to ${value}`);}@after(LogSetter, 'set')class Person extends Model { constructor (first, last) { super(); this.set('first', first); this.set('last', last); } fullName () { return `${this.get('first')} ${this.get('last')}`; }};

natomiast nie możemy zrobić czegoś takiego z bezpośrednim dostępem do właściwości. Mediacja dostępu do nieruchomości za pomocą metod jest bardziej elastyczna niż bezpośredni dostęp do nieruchomości, a to pozwala nam porządkować nasz program i prawidłowo rozdzielać odpowiedzialność.

późniejsze dekoratory klas mogą być używane w kodzie es 6 jako zwykłe funkcje. Zamiast @after(LogSetter, 'set') class Person extends Model {...}, po prostu napisz const Person = after(LogSetter, 'set')(class Person extends Model {...})

techniki

gettery i settery w javascript

problem z geterami i seterami był dobrze zrozumiany, a stewardzi stojący za ewolucją JavaScript odpowiedzieli, wprowadzając specjalny sposób na przekształcenie bezpośredniego dostępu do właściwości w rodzaj metody. Oto jak napiszemy nasząPerson klasę używając „getters” i ” setters:”

class Model { constructor () { this.listeners = new Set(); } addListener (listener) { this.listeners.add(listener); } deleteListener (listener) { this.listeners.delete(listener); } notifyAll (message, ...args) { for (let listener of this.listeners) { listener.notify(this, message, ...args); } }}const _first = Symbol('first'), _last = Symbol('last');class Person extends Model { constructor (first, last) { super(); this.first = first; this.last = last; } get first () { this.notifyAll('get', 'first', this); return this; } set first (value) { this.notifyAll('set', 'first', value); return this = value; } get last () { this.notifyAll('get', 'last', this); return this; } set last (value) { this.notifyAll('set', 'last', value); return this = value; } fullName () { return `${this.first} ${this.last}`; }};

Kiedy poprzedzamy metodę słowem kluczowymget, definiujemy getter, metodę, która zostanie wywołana, gdy kod będzie próbował odczytać z właściwości. A kiedy poprzedzamy metodę z set, definiujemy setter, metodę, która zostanie wywołana, gdy kod spróbuje zapisać do właściwości.

Gettery i settery nie muszą właściwie odczytywać ani zapisywać żadnych właściwości, mogą zrobić wszystko. Ale w tym eseju porozmawiamy o ich użyciu do pośredniczenia w dostępie do nieruchomości. Za pomocą getterów i seterów możemy napisać:

const currentUser = new Person('Reginald', 'Braithwaite');const currentUserView = new PersonView(currentUser);currentUser.first = 'Ragnvald';

i wszystko nadal działa tak, jakbyśmy napisali currentUser.set('first', 'Ragnvald') z kodem .set-style.

Gettery i settery pozwalają nam mieć semantykę używania metod, ale składnię bezpośredniego dostępu.

Keypunch

kombinator after, który może obsługiwać gettery i settery

Gettery i settery wydają się na pierwszy rzut oka magiczną kombinacją znanej składni i potężnej zdolności do metaprogramowania. Jednak getter lub setter nie jest metodą w zwykłym znaczeniu. Więc nie możemy dekorować setera dokładnie tym samym kodem, którego użylibyśmy do dekorowania zwykłej metody.

za pomocą metody.set możemy bezpośrednio uzyskać dostęp doModel.prototype.set I zawinąć ją w inną funkcję. Tak pracują nasi dekoratorzy. Ale nie ma metody Person.prototype.first. Zamiast tego istnieje deskryptor właściwości, który możemy przeprowadzić tylko za pomocą Object.getOwnPropertyDescriptor()I zaktualizować za pomocą Object.defineProperty().

z tego powodu podany powyżej dekorator after nie będzie działał dla getterów i seterów.2 musielibyśmy użyć jednego rodzaju dekoratora dla metod, innego dla getterów, a trzeciego dla seterów. To nie brzmi jak zabawa, więc zmodyfikuj nasz kombinatorafter, abyś mógł używać jednej funkcji z metodami, geterami i seterami:

function getPropertyDescriptor (obj, property) { if (obj == null) return null; const descriptor = Object.getOwnPropertyDescriptor(obj, property); if (obj.hasOwnProperty(property)) return Object.getOwnPropertyDescriptor(obj, property); else return getPropertyDescriptor(Object.getPrototypeOf(obj), property);};const after = (behaviour, ...methodNames) => (clazz) => { for (let methodNameExpr of methodNames) { const = methodNameExpr.match(/^(?:(get|set) )(.+)$/); const descriptor = getPropertyDescriptor(clazz.prototype, methodName); if (accessor == null) { const method = clazz.prototype; descriptor.value = function (...args) { const returnValue = method.apply(this, args); behaviour.apply(this, args); return returnValue; }; descriptor.writable = true; } else if (accessor === "get") { const method = descriptor.get; descriptor.get = function (...args) { const returnValue = method.apply(this, args); behaviour.apply(this, args); return returnValue; }; descriptor.configurable = true; } else if (accessor === "set") { const method = descriptor.set; descriptor.set = function (...args) { const returnValue = method.apply(this, args); behaviour.apply(this, args); return returnValue; }; descriptor.configurable = true; } Object.defineProperty(clazz.prototype, methodName, descriptor); } return clazz; }

teraz możemy napisać:

const notify = (name) => function (...args) { this.notifyAll(name, ...args); };@after(notify('set'), 'set first', 'set last')@after(notify('get'), 'get first', 'get last')class Person extends Model { constructor (first, last) { super(); this.first = first; this.last = last; } get first () { return this; } set first (value) { return this = value; } get last () { return this; } set last (value) { return this = value; } fullName () { return `${this.first} ${this.last}`; }};

oddzieliliśmy kod powiadamiania słuchaczy od kodu pobierania i ustawiania wartości. Co prowokuje proste pytanie: jeśli kod, który śledzi słuchaczy, jest już oddzielony od Model, dlaczego kod do wyzwalania powiadomień nie powinien być w tej samej jednostce?

jest na to kilka sposobów. Użyjemy uniwersalnego mixinu zamiast wpychania tej logiki do superklasy:

const Notifier = mixin({ init () { this.listeners = new Set(); }, addListener (listener) { this.listeners.add(listener); }, deleteListener (listener) { this.listeners.delete(listener); }, notifyAll (message, ...args) { for (let listener of this.listeners) { listener.notify(this, message, ...args); } }}, { notify (name) { return function (...args) { this.notifyAll(name, ...args); } }});

To pozwala nam pisać:

@Notifier@after(Notifier.notify('set'), 'set first', 'set last')@after(Notifier.notify('get'), 'get first', 'get last')class Person { constructor (first, last) { this.init(); this.first = first; this.last = last; } get first () { return this; } set first (value) { return this = value; } get last () { return this; } set last (value) { return this = value; } fullName () { return `${this.first} ${this.last}`; }};

co zrobiliśmy? Wprowadziliśmy gettery i setery do naszego kodu, zachowując możliwość ozdabiania ich dodatkową funkcjonalnością, jakby były zwykłymi metodami.

to wygrana za rozkładanie kodu. I to wskazuje na coś do przemyślenia: kiedy masz tylko metody, jesteś zachęcany do tworzenia superklas wagi ciężkiej. Dlatego tak wiele frameworków wymusza rozszerzenie ich specjalnych klas bazowych, takich jakModel lubView.

ale kiedy znajdziesz sposób na użycie mixinów i metod dekorowania, możesz rozłożyć rzeczy na mniejsze kawałki i zastosować je tam, gdzie są potrzebne. Prowadzi to w kierunku wykorzystania zbiorów bibliotek zamiast ciężkiego frameworka.

podsumowanie

Gettery i settery pozwalają nam zachować starszy styl pisania kodu, który wydaje się mieć bezpośredni dostęp do właściwości, podczas gdy faktycznie pośredniczy w tym dostępie metodami. Ostrożnie możemy zaktualizować nasze oprzyrządowanie, aby umożliwić nam dekorowanie naszych getterów i seterów, rozdzielając odpowiedzialność według własnego uznania i uwalniając nas od zależności od klas podstawowych wagi ciężkiej.

(discuss on Hacker News)

Post Scriptum

ten post wykorzystuje nasłuchiwanie seterów właściwości jako pretekst do omówienia mechanizmów gettera i settera oraz sposobów ich dekorowania, abyśmy mogli uporządkować kod wokół problemów.

oczywiście propagowanie zmian poprzez jawne powiadamianie nie jest jedynym sposobem organizowania kodu, który musi zarządzać zależnościami od zmiany danych. To poza zakresem tego postu, aby omówić wiele alternatyw, ale czytelnicy zasugerowali zbadanie obiektu.obserwuj i pracuj z niezmiennymi danymi.

latające spodki dla każdego

jeszcze jedna rzecz

Rubyiści wyśmiewają się z:

get first () { return this;}set first (value) { return this = value;}get last () { return this;}set last (value) { return this = value;}

Rubyiści użyliby wbudowanej metody klasyattr_accessor aby je dla nas napisać. Tak dla Zabawy, napiszemy dekoratora, który pisze getters i setters. Surowe wartości będą przechowywane wattributes mapie:

function attrAccessor (...propertyNames) { return function (clazzOrObject) { const target = clazzOrObject.prototype || clazzOrObject; for (let propertyName of propertyNames) { Object.defineProperty(target, propertyName, { get: function () { if (this.attributes) return this.attributes; }, set: function (value) { if (this.attributes == undefined) this.attributes = new Map(); return this.attributes = value; }, configurable: true, enumerable: true }) } return clazzOrObject; }}

teraz możemy napisać:

@Notifier@attrAccessor('first', 'last')@after(Notifier.notify('set'), 'set first', 'set last')@after(Notifier.notify('get'), 'get first', 'get last')class Person { constructor (first, last) { this.init(); this.first = first; this.last = last; } fullName () { return `${this.first} ${this.last}`; }};

attrAccessor bierze lista nazw nieruchomości i zwraca dekorator dla klasy. Zapisuje on prostą funkcję getter lub setter dla każdej właściwości, a wszystkie zdefiniowane właściwości są przechowywane w.attributes hash. Jest to bardzo wygodne w przypadku serializacji lub innych mechanizmów trwałości.

(trywialne jest również tworzenie funkcjiattrReader IattrWriter za pomocą tego szablonu. Po prostu musimy pominąć set podczas pisania attrReader I pominąć get podczas pisania attrWriter.)

  1. był też język o nazwie BCPL, i inne wcześniej, ale nasza historia musi się gdzieś zacząć i zaczyna się od C. ↩

  2. nie będzie również przepismixin rozwinęliśmy się w poprzednich postach, takich jak używanie ES.później dekoratorzy jako Mixiny. Można go ulepszyć, aby dodać specjalny przypadek dla getterów, seterów i innych problemów, takich jak praca z POJOs. Na przykład Uniwersalny Mixin Andrei Giammarchi. ↩

Dodaj komentarz

Twój adres e-mail nie zostanie opublikowany.