Podsumowanie

Przedstawiliśmy podstawy programowania zorientowanego obiektowo i widzieliśmy, jak zaimplementować systemy obiektowe w R z trzema różnymi modelami obiektowymi: S3, S4 i R6. Przyjrzeliśmy się podstawowym elementom budulcowym modeli obiektowych, takim jak hermetyzacja, polimorfizm i hierarchie. Pokazaliśmy, jak zaimplementować polimorfizm parametryczny z S3 i S4, a także zwykły polimorfizm z R6, i pokazaliśmy, jak używać pojęć takich jak interfejsy, nawet jeśli nie ma dla nich jawnego wsparcia w R. Zaimplementowaliśmy pełny obiekt. zorientowany na system śledzenia informacji o kryptowalutach, a robiąc to, przyjrzał się różnym wzorcom i technikom, a także sposobom na trzy różne modele obiektów mogą być używane razem. Rodzaj używanego modelu obiektowego jest przedmiotem pewnych kontrowersji wśród programistów języka R, a decyzja zależy od tego, jak elastyczny, formalny lub intuicyjny ma być kod. Ogólnie rzecz biorąc, jeśli wolisz większą elastyczność, użyj S3, jeśli wolisz większą formalność i solidność użyj S4, a jeśli wolisz, aby Twój kod był łatwy do zrozumienia i intuicyjny dla ludzi, którzy pochodzą z innych języków i nie są zaznajomieni z S3 i S4, to użyj R6. Jednak nadal istnieją kontrowersje. John Chambers, twórca języka S i jeden z głównych twórców języka R, zaleca S4 zamiast S3 w swojej książce Software for Data Analysis, Springer, 2008. Przewodnik po stylu R firmy Google () mówi, że w miarę możliwości należy unikać S4, i zamiast tego powinien używać S3. Miejmy nadzieję, że po przeczytaniu tego rozdziału będziesz wiedział, jaki system preferujesz w swoim następnym projekcie i dlaczego. W rozdziale Wdrażanie wydajnej prostej średniej ruchomej będziemy nadal rozbudowywać system, który stworzyliśmy w tym rozdziale, aby był bardziej wydajny, gdy zaczniemy zajmować się dużymi ilościami danych.

Kilka porad dotyczących pracy z systemami obiektowymi

Programowanie obiektowe zapewnia dużą elastyczność, ale jeśli jest używane nieprawidłowo, może również powodować wiele zamieszania, ponieważ łatwo jest tworzyć bardzo złożone systemy, gdy wystarczą znacznie prostsze rozwiązania. Powinieneś uruchomić mały działający system, zanim rozwiniesz go w bardziej złożony. Pamiętaj też, że większość projektów w świecie rzeczywistym jest nadmiernie ograniczona i nie będziesz w stanie zadowolić wszystkich, więc musisz zdecydować o priorytetach swojego systemu. Każda część twojego systemu powinna skupiać się na jednej rzeczy i robić to dobrze. W razie wątpliwości rób krótsze rzeczy. Twórz krótsze klasy i krótsze metody. Takie postępowanie zmusi obiekty do skupienia się na jednej odpowiedzialności, co z kolei poprawi Twój projekt i pozwoli na łatwiejsze ponowne użycie kodu. Spraw, aby Twoje obiekty były jak najbardziej prywatne. Klasy publiczne nie powinny mieć żadnych pól publicznych, co oznacza, że ​​wszędzie należy używać hermetyzacji. Maksymalizuj ukrywanie informacji i minimalizuj sprzężenia. Pamiętaj też, że imiona mają znaczenie. Unikaj tajemniczych skrótów w swoim kodzie i bądź konsekwentny. To samo słowo powinno oznaczać to samo wszędzie w systemie. Na koniec postaraj się, aby kod był jak najbardziej niezmienny. Tworzy to kod, który jest łatwy do zrozumienia, o wiele bardziej wielokrotnego użytku i bezpieczny dla wątków, co może być bardzo przydatne przy równoległym wykonywaniu, co zobaczymy w następnym rozdziale. Jeśli jednak zaimplementujesz zmienny system, zachowaj jak najmniejszą przestrzeń stanów. Ogólna rada: Twój projekt powinien być łatwy do zrozumienia i trudny do niewłaściwego użycia, nawet bez dokumentacji. Twój kod powinien być czytelny i łatwy w utrzymaniu, a wysiłek włożony w tworzenie kodu, który jest łatwy do zmiany, powinien być pozytywnie skorelowany z prawdopodobieństwem wystąpienia takiej zmiany

Aktywacja naszego systemu za pomocą dwóch prostych funkcji

Po załadowaniu niektórych danych do systemu będziesz mógł uruchomić pliki update-markets.R i update-assets.R, których zawartość jest pokazana poniżej. Pierwszy ładuje wymagane definicje, tak jak poprzednio podczas tworzenia danych użytkownika, i udostępnia funkcję update_markets_loop(), która otrzymuje parametr określający liczbę minut między każdym pobraniem aktualnych danych rynkowych. Co 60 minut to dobra opcja i używamy jej poniżej. Funkcja po prostu tworzy instancję Storage przy użyciu specyfikacji  SETTINGS przedstawionej wcześniej, pobiera istniejące wymiany (które są tylko w tym momencie CoinMarketCap) i wywołuje metodę publiczną update_markets() na każdej z nich, z odpowiednimi parametrami:

library(R6)

library(methods)

source(„../storage/storage.R”, chdir = TRUE)

source(„../utilities/time-stamp.R”)

source(„../settings.R”)

update_markets_loop <- function(minutes_interval) {

storage = Storage$new(SETTINGS)

exchanges <- storage$read_exhanges()

repeat {

timestamp = now.TimeStamp()

for (exchange in exchnages) {

exchange$update_markets(timestamp, storage)

}

Sys.sleep(minutes_interval * 60)

}

}

update_markets_loop(60)

Po uruchomieniu tego pliku zobaczysz dane pokazujące postęp w konsoli, jak pokazano poniżej

Funkcja update_assets_loop() działa podobnie, ale pobiera użytkowników w każdej iteracji, która dynamicznie dostosowuje się, aby uwzględnić wszelkie dodatki lub usunięcia użytkowników, które mogły mieć miejsce, gdy funkcja oczekiwała na następny cykl, i wywołuje metodę publiczną update_assets() dla każdej instancji User:

library(R6)

library(methods)

source(„../storage/storage.R”, chdir = TRUE)

source(„../utlities/time-stamp.R”)

source(„../settings.R”)

update_ssets_loop <- function(minutes_interval) {

storage = Storage$new(SETTINGS)

repeat {

users <- storage$read_users()

timestamp = now.TimeStamp()

lapply(users, update_assets, timestamp)

Sys.sleep(minutes_interval * 60)

}

}

update_assets_loop(60)

Oto przykład wyniku dla pliku:

Po uruchomieniu tych dwóch plików cały opracowany przez nas system obiektowy zacznie działać, aby okresowo pobierać bieżące dane i zapisywać je w odpowiednich plikach CSV. Możesz zajrzeć bezpośrednio do tych plików, aby zobaczyć, jakie dane są zapisywane. Pamiętaj, że jeśli portfel nie zawiera dodatniej liczby aktywów, nie zostanie wyświetlony. Kiedy wdrażasz swój pierwszy system obiektowy, wydaje się to niemal magiczne. Jeśli jest to pierwszy system obiektowy, który zbudowałeś, mam nadzieję, że masz takie odczucie, i mam również nadzieję, że ten przykład był dla Ciebie interesujący i przydatny

Zapisywanie naszych początkowych danych użytkownika w systemie

Zanim zaczniemy korzystać z naszego systemu, musimy wprowadzić do niego dane, które posłużą nam do pobierania danych. W szczególności musimy stworzyć kilku użytkowników, dodać do nich kilka portfeli i je zapisać. Aby to zrobić, tworzymy plik creat-user-data.R zawierający skrypt, który wykona to za nas. Skrypt ładuje modele obiektów S4 i R6 (S3 nie musi być ładowany jawnie), pozyskuje pliki z definicjami, których bezpośrednio potrzebujemy, którymi są Storage, User i SETTINGS tworzy dla nas dwóch użytkowników i zapisuje je:

library(R6)

library(methods)

source(„../storage/storage.R”, chdir – TRUE)

source(„../users/user.R”)

source(„../settings.R”)

storage = Storage$new(SETTINGS)

user_1 <- user_constructor(1@somewhere.com , storage)

user_1 <- new_wallet(user_1,

„BTC”,

„3D2oetdNuZUqQHPJmcMDDHYoqkyNVsFk9r”, „”)

user_1 <- new_wallet(user_1,

„LTC”,

„LdP8Qox1VAhCzLJNqrr74YovaWYyNBUWvL”, „”)

save(user_1)

user_2 <- user_constructor(2@somewhere.com,storage)

user_2 <- new_wallet(user_2,

„BTC”,

„16rCmCmbuWDhPjWTrpQGaU3EPdZF7MTdUk”, „”)

user_2 <- new_wallet(user_2,

„LTC”,

„LbGi4Ujj2dhhcMdiS9vaCpWxtayBujBQYZw”, „”)

save(user_2)

Po wykonaniu skryptu możesz zajrzeć do katalogu csv_files/ i znaleźć w nim odpowiednie dane. W tym przypadku użyliśmy portfeli z największą liczbą bitcoinów i litecoinów, które można znaleźć online. Możesz eksperymentować, używając własnych portfeli lub dowolnego portfela, którego zawartość chcesz śledzić. Oczywiście parametry email i note i nie muszą być prawdziwe; jedynymi parametrami, które muszą być rzeczywiste, są symbole aktywów, którymi mogą być BTC lub LTC dla systemu, który wdrożyliśmy, oraz adresy portfeli dla takich symboli. Możesz pozostawić to pole  note puste, tak jak w przykładzie.

Pomagamy sobie ze scentralizowanym plikiem ustawień

Na koniec pokazujemy słynny scentralizowany plik ustawień, o którym wspominaliśmy w całym przykładzie. Jak widać, jest to po prostu lista list zawierająca parametry określające, jak powinien zachowywać się nasz system. Scentralizowanie tych opcji w jednym pliku, tak jak tutaj robimy, może być często bardzo wygodne. Zamiast zmieniać kod, gdy chcemy innych zachowań z naszego systemu, możemy po prostu zmienić ten plik, a wszystko zostanie za nas załatwione:

SETTING <- list (

„debug” = TRUE,

„storage” = list (

„read” = list (

„name” = „CSVFiles”,

„envronment” = „production”

),

„write” = list (

list (

„name” = „CSVFiles”,

„environment” = „production”

)

),

„table_names” = list (

„production” = list (

„assets” = „production_assets”,

„markets” = „production_markets”,

„users” = „production_users”,

„wallets” = „production_wallets”,

),

„development” = list (

„assets” = „development_assets”,

„markets” = „development_markets”,

„users” = „development_users”,

„wallets” = „development_wallets”,

)

)

),

„batch_data_colletion” = list (

„assets” = list (

„minutes” = 60

),

„markets” = list (

„minutes” = 60

)

)

)

W szczególności zwróć uwagę, że istnieje wartość logiczna debug, której ostatecznie nie używaliśmy, ale która może być przydatna podczas debugowania naszego systemu w pewnym momencie. Zwróć również uwagę, że istnieją dwie główne części naszego pliku ustawień, część storage i część batch_data_ollection. Część  storage jest tą, z której korzystaliśmy do tej pory i zawiera specyfikację dla jakich baz danych powinny być używane do odczytu i zapisu danych poprzez podanie nazwy implementacji, która ma być zastosowana w elementach names , a environment na której aktualnie pracujemy , którym może być albo product lub development. Oba te elementy są wykorzystywane przez czynniki  do odpowiedniego skonfigurowania systemu, zanim zacznie on działać. Zwróć również uwagę, że pliki CSV, które zostaną utworzone, odpowiadają ciągom znaków znalezionych w elemencie i będą się różnić w zależności od enviromnment bazy danych, w której ma działać.

Wreszcie wprowadzenie użytkowników z klasami S3

Nasz system obiektowy jest prawie gotowy. Brakuje nam tylko definicji User. W tym przypadku użyjemy S3 do zdefiniowania klasy User. Funkcja user_cnstructor() przyjmuje instancję email i Storage w storage, aby utworzyć instancję User. Jednak zanim to zrobi, sprawdza, czy wiadomość e-mail jest poprawna z funkcją valid_email() zdefiniowaną poniżej. Po utworzeniu użytkownika wywoływana jest metoda get_wallets() w celu pobrania portfeli skojarzonych z użytkownikiem przed odesłaniem. Funkcja vaid_email() po prostu otrzymuje ciąg znaków, który ma być adresem e-mail, i sprawdza, czy zawiera co najmniej jeden symbol @ i jeden symbol . . Oczywiście nie jest to solidny mechanizm sprawdzania, czy jest to adres e-mail, i został umieszczony tutaj tylko po to, aby zilustrować, jak można zaimplementować mechanizm sprawdzający:

source(„../assets/wallet.R”, chdir = TRUE)

user_constructor <-0 function(email,storage) {

if (!valid_email(email)) { stop(„Invalid email”) }

user <- list(storage = storage, email = email , wallets = list() )

class(user) <- „User:

user <- get_wallets(user)

return(user)

}

valid_email <- function(string) {

if (grepl(„@”, string) && grepl(„.”, string)) { return (TRUE) }

return(FALSE)

}

Funkcja get_wallets.User() po prostu prosi atrybut storage w obiekcie o pobranie portfeli powiązanych z jego własnym adresem e-mail, przypisuje je do atrybutu listy wallets, i odsyła obiekt User:

get_wallets.User <- function(user) {

user$wallets <- user$storage$read_wallets(usr$email)

return(user)

}

get_wallets <- function(object) {

UseMethod(„get_wallets”)

}

Funkcja new_wallet.User() otrzymuje instancję User, ciąg symbol, łańcuch address i ciąg znaków note  aby utworzyć nową instancję i dołączyć ją do atrybutu listy  wallets instancji User przekazanej do niej. Jednak zanim to zrobi, sprawdza wszystkie wcześniej zarejestrowane portfele dla użytkownika. Jeśli stwierdzi, że portfel jest już zarejestrowany, po prostu zignoruje dodanie i odeśle tę samą instancję z powrotem. To kolejny rodzaj sprawdzania, który możesz wdrożyć we własnych systemach:

new_wallet.User <- function(user, symbol, address, note) {

if (length(user$wallets) >= 1) {

for (wallet in user$wallets) {

if (wallet$get_symbol () == symbol &

wallet$get_address() == address) {

return(user)

}

}

}

wallet <- Wallet$new(user$email, symbol, address, note)

user$wallets <- c(user$wallets, list(wallet))

return(user)

}

new_wallet <- function(object, symbol, address, note) {

UseMethod(„new_wallet”)\

}

Funkcja update_assets.User()  po prostu przechodzi przez każdą instancję  Wallet w atrybucie list wallets i wywołuje swoją metodę publiczną update_assets() z bieżącym, przekazanym timestamp i instancją Storage zawartą wewnątrz instancji User. Jak widzieliśmy wcześniej, powoduje to aktualizację zasobów i zapisanie ich w bazie danych, a obiekt Wallet zajmuje się tym w imieniu instancji User:

update_assets.User <- function(user, timestamp) (

for (walllet in user$wallets) {

walllet$update_assets(timestamp, user$storage)

}

}

update_assets <- function(object, timestamp) {

UseMethod(„update_assets”)

}

Funkcja save.User() po prostu używa tego atrybutu storage do zapisania instancji User, a także danych jej portfeli. Jak widzieliśmy, jeśli portfele już istnieją w zapisanych danych, to nie zostaną one zduplikowane, a implementacja CSVFiles dba o to w imieniu instancji User:

save.User <- function(user) {

user$storage$write_user(user)

user$storage$write_wallets(user$wallets)

}

save <- function(object) {

UseMethod(„save”)

}

Na koniec użytkownik udostępnia metodę dataS3.User() zwracania listy z adresem e-mail użytkownika w celu ponownego zapisania w bazie danych:

dataS3.User <- function(user) {

return(list(email = user$email))

}

dataS3 <- function(object) {

UseMethod(„dataS3”)

}

Jak widzieliśmy w tej sekcji, po wykonaniu pewnych prac możemy opracować ładne i intuicyjne abstrakcje, które wykorzystują funkcjonalność zaimplementowaną w innych obiektach, aby zapewnić potężne mechanizmy, takie jak zapisywanie danych w bazie danych, za pomocą bardzo prostych wywołań.

Wdrażanie naszych requesterów portfela

WalletRequester Będzie koncepcyjnie podobny do ExchangeRequester. Będzie to interfejs i zostanie zaimplementowany w naszych interfejsach BTCRequester i LTCRequester . Jak widać, wymaga ona wywołania metody publicznej assets() w celu zaimplementowania i zwrócenia listy instancji Asset. Wymaga również zaimplementowania metody prywatnej create_asset(), która powinna zwracać poszczególne instancje Asset, oraz metody prywatnej url, która utworzy adres URL wymagany do wywołania interfejsu API. Oferuje prywatną metodę request(), która będzie używana przez implementacje do pobierania danych z zewnętrznych interfejsów API:

source(„../../../utilities/requester.R”)

WalletRequester <- R6Class (

„WalletRequester”,

public = list (

assets = function() list()

),

private = list (

requester = Requester$new(),

create_asset = function() NULL,

url = function(address) „ „,

request = function(URL) {

return(private$requester$request(URL))

}

)

)

Implementacje BTCRequester i LTCRequester   są pokazane poniżej dla kompletności, ale nie zostaną wyjaśnione. Jeśli śledziłeś wszystko do tej pory, powinny być łatwe do zrozumienia:

source(„.wallet-requester.R”)

source(„../../asstet.R”)

BTCRequester < R6Class (

„BTCRequester „ ,

inherti = WalletRequester,

public = list (

initialize = function(address) {

private$address <- adress

},

assets = function ()  {

total <- as.numeric(private$request(private$url() ))

if (total) > 0) {return(list(private$create_asset(total))) }

return(list() )

}

),

private = list (

address = „”,

url = function(address) {

return(paste (

https://chainz.cryptoid.info/btc/api.dws,

„?q=getbalance”,

„&a=”,

private$address,

sep = „”

))

},

create_asset = function(total) {

return(new(

„Asset”,

email = „”,

timestamp = „”,

name = „Bitcoin”,

symbol = „BTC”,

total = total,

address = private$address

))

}

)

)

source(„.wallet-requester.R”)

source(„../../asstet.R”)

LTCRequester <- R6Class(

„LTCRequester „,

inherit = WalletRequester,

public  = list(

initialize = function(address) {

private$address <- address

},

assets = function() {

total <- as.numeric(private$request(private$url() )

if (total > 0 {return(list(private$create_asset(total))) }

return(list () )

}

),

private = list (

address = „”,

url = funtion(address) {

return(paste(

https://chainz.cryptoid.info/btc/api.dws,

„?q=getbalance”,

„&a=”,

private$address,

sep = „”

))

},

create_asset = function(total) {

return(new (

„Asset”,

email = „ „,

timestamp = „ „,

name = „Litecoin”,

symbol = „LTC”,

total = total,

adres = private$address

))

}

)

)

wallet_requester_factory () działa tak jak inne czynniki; jedyną różnicą jest to, że w tym przypadku mamy dwie możliwe implementacje, które mogą zostać zwrócone, co widać w instrukcji if. Gdybyśmy zdecydowali się dodać WalletRequester dla innej kryptowaluty, takiej jak Ether, moglibyśmy po prostu dodać tutaj odpowiednią gałąź i powinno działać dobrze:

source(„./btc-requester.R”)

source(„./ltc-requester.R”)

wallet_requester_factory <- function(symbol,address) {

if (symbol = = „BTC”) {

return(BTCRequester$new(address))

} else if (symbol == „LTC”) {

return(LTCRequester$new(address))

} else {

stop(„Unknown symbol”)

}

}

Rozwijanie infrastruktury naszych portfeli

Teraz, gdy jesteśmy w stanie pobrać aktualne dane o cenach z giełd, przejdźmy do naszej definicji Wallet. Jak widać, określa typ atrybutów prywatnych, których oczekujemy od danych, które ma obsługiwać, a także metodę publiczną data(), aby utworzyć listę danych, które muszą zostać zapisane w bazie danych w pewnym momencie. Udostępnia również metody dla email, symbol, i address oraz metodę publiczną pudate_assets(), która będzie używana do pobierania i zapisywania zasobów w bazach danych, tak jak to zrobiliśmy w przypadku Ezchange. W rzeczywistości zastosowane techniki są dokładnie takie same, więc nie będziemy ich więcej wyjaśniać:

source(„./requesters/wallet-requester-factory.R”. chdir = TRUE)

Wallet <- R6Class (

public  = list (

initialize = function(email, symbol, address,note) {

private$requester <- wallet_requester_fatory(symbol, address)

private$email <- email

private$symbol<- symbol

private$addressl <- address

private$note <- note

},

data = function() {

return(list(

email = private$email,

symbol = private$symbol,

address = private$address,

note = private$note,

))

},

get_mail = function() {

return(a.character(private$email))

},

get_symbol = function() {

return(a.character(private$symbol))

},

get_address = function() {

return(a.character(private$address))

},

update_assets = function(timestamp, storage) {

private$timestamp <- timestamp

strage$write_assets(private$assets())

}

),

private = list (

timestamp = NULL,

requester = NULL,

email = NULL,

symbol = NULL,

address = NULL,

note = NULL,

assets = function() {

return (lapply (

private$requester$assets(),

private$insert_metadata))

},

insert_metadata = function(asset) {

timestamp(assets) <- unclass(private$timestamp)

email(asset) <- unclass(private$timestamp)

email(asset) <- private$email

return(asset)

}

)

)

Pobieranie danych na żywo dla rynków i portfeli za pomocą klas R6

W tej sekcji wyjaśniono, jak utworzyć prosty requester, który jest obiektem żądającym informacji zewnętrznych (w tym przypadku z interfejsu API przez Internet). Będziemy również rozwijać naszą infrastrukturę wymiany i portfela.

Utworzenie bardzo prostego requestera do izolowania wywołań API

Teraz skupimy się na tym, jak faktycznie pobieramy dane na żywo. Ta funkcjonalność zostanie również zaimplementowana przy użyciu klas R6, ponieważ interakcje mogą być złożone. Przede wszystkim tworzymy prostą klasę Requester, która zawiera logikę do pobierania danych z API JSON znalezionych w innym miejscu w Internecie i która będzie używana do pobierania naszych danych o kryptowalutach na żywo dla portfeli i rynków. Nie chcemy, aby logika, która współdziała z zewnętrznymi interfejsami API, była rozproszona po wszystkich naszych klasach, więc scentralizujemy ją tutaj, aby nią zarządzać, ponieważ później pojawią się bardziej wyspecjalizowane potrzeby. Jak widać, wszystko, co robi ten obiekt, to publiczna metoda request(), a wszystko, co robi, to użycie funkcji formJSON() z pakietu jsonlite, aby wywołać URL, który jest do niego przekazywany i wysłać otrzymane dane z powrotem do użytkownika. W szczególności wysyła go jako ramkę danych, gdy dane otrzymane z zewnętrznego interfejsu API mogą zostać przekształcone w postać ramki danych.

library(jsonlite)

Requester <- R6Class (

„Requester”,

public – list (

request = function(URL) {

return(fromJSON(URL))

}

)

)

Hermetyzowanie wielu baz danych za pomocą warstwy pamięci

Teraz, gdy opracowaliśmy nasz interfejs Database i naszą implementację CSVFiles takiego interfejsu, jesteśmy gotowi do opracowania kolejnej warstwy abstrakcji, naszej klasy Storage. Zostanie zaimplementowany z R6. Jak widać, konstruktor Stroage zaimplementowany w funkcji initialize otrzymuje obiekt settings, który będzie pełnym scentralizowanym plikiem ustawień, wspominając i będą używać części storage/read, storage/write, i storage/table do tworzenia różnych instancji bazy danych za pomocą funkcji database_factory(), którą wyjaśniliśmy wcześniej. W przypadku atrybutu read_db będzie to pojedyncza implementacja Database, która będzie używana do odczytu danych. W przypadku atrybutu write_db ​​jak sama nazwa wskazuje, będziemy mieli listę implementacji Database, w których wszystkie dane, które mają być zapisane przez inne obiekty, będą przechowywane. Dzięki tej abstrakcji Storage możemy po prostu wysłać ją do obiektów szukających obiektu podobnego do bazy danych do zapisywania i odczytywania danych, a ona zajmie się replikacją danych w razie potrzeby, jak również dostarczeniem danych do wspomnianych obiektów. Aby to osiągnąć, można poprosić, aby w przypadku metod read po prostu delegował zadanie do implementacji Database  zawartej w jego atrybucie read_db, a w przypadku metody write, robi to samo dla każdej implementacji Database w jej atrybucie write_dbs. To takie proste:

soure(„./database-factory.R”)

Storage <- R6Class (

„Storage”,

public = list (

initialize = function(settings) {

private$read_db <- database_factory (

settings[„storage”]] [[„read”]],

settings[[„storage”]][[„tables_names”]]

)

private$write_dbs <- lapply(

settings[[„storage”]] [[„”write”]],

database_factory,

settings[[„storage”]][[„tables_names”]]

)

).

read_exchanges = function() {

return(private$read_db$read_exchanges() }

),

read_users = funtion() {

return(private$read_db$read_users(self))

),

read_wallets = function(email) {

return(private$read_db$read_wallets(email))

),

read_all_wallets = function() {

return(private$read_db$read_all_wallets() )

),

read_analysis_assets = function(email) {

return(private$read_db$read_analysis_assets(email))

),

write_user = function(user) {

for (db in private$write_dbs) { db$write_user(user) }

),

write_wallets = function(wallets) {

for (db in private$write_dbs) { db$write_wallets(wallets) }

),

write__assets = function(assets) {

for (db in private$write)dbs)  { db$write_assets(assets) }

},

write_markets = function(markets) {

for (db in private$write_dbs) { db$write_markets(markets) }

},

),

private = list(read_db = NULL, write_dbs = list () )

)

To wszystko w przypadku naszych abstrakcji pamięci masowej. W tym momencie zaimplementowaliśmy interfejs Database, implementację CSVFIles wspomnianego interfejsu i warstwę Storage, która umożliwia użycie wielu implementacji Database jednocześnie i odsprzęga dla nas obiektów odczytu i zapisu. Moglibyśmy zdecydować się na użycie jednego typu bazy danych do operacji odczytu i innego do operacji zapisu i mieć jakiś rodzaj zewnętrznego mechanizmu synchronizowania ich razem poza R. Może to być przydatne na przykład ze względu na wydajność.