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.
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.
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 = 42
I currentUser.setId(42)
do prawie nic istotnego dla nikogo, a kod używający currentUser.id = 42
lub 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.
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
Model
superclass pozwala na ręczne nasłuchiwanie plikówget
Iset
/div > metody na modelu. Jeśli są wywołane, „słuchacze”są powiadamiani za pomocą metody.notifyAll
. Używamy tego, aby miećPersonView
nasłuchać jejPerson
I 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 napiszconst Person = after(LogSetter, 'set')(class Person extends Model {...})
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.
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.
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
.)
-
był też język o nazwie BCPL, i inne wcześniej, ale nasza historia musi się gdzieś zacząć i zaczyna się od C. ↩
-
nie będzie również przepis
mixin
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. ↩