Upgrade to Pro — share decks privately, control downloads, hide ads and more …

RxJava + Java 8, Sortir de l'Enfer des Callbacks

RxJava + Java 8, Sortir de l'Enfer des Callbacks

[FR] Comment faire du code asynchrone, lisible et composable? RxJava couplé à Java8 pourrait être une bonne solution pour sortir de cet Enfer des Callbacks.

Fda20bf9d9c85c4390ca7237beba45a2?s=128

Simon Baslé

May 23, 2014
Tweet

Transcript

  1. Sortir de l’Enfer des Callbacks RxJava + Java 8

  2. @SimonBasle @Couchbase @InfoQFR @bbl_fr

  3. L‘Enfer des Callbacks?

  4. L‘Enfer des Callbacks?

  5. synchrone == inefficace*

  6. nous avons besoin de code asynchrone

  7. nous avons besoin de code asynchrone réactif

  8. nous avons besoin de code asynchrone parallélisable

  9. nous avons besoin de code asynchrone composable

  10. nous avons besoin de code asynchrone lisible

  11. mais comment bien faire Asynchrone ?

  12. les Futures<T>

  13. les Futures<T> une modélisation lisible

  14. les Futures<T> peuvent bloquer (get)

  15. les Futures<T> deviennent compliquées au delà d’un niveau

  16. les Futures<T> se composent mal

  17. les Callbacks

  18. les Callbacks tout simple

  19. les Callbacks pas de blocage possible

  20. les Callbacks ne se composent pas

  21. les Callbacks deviennent vite illisibles

  22. EXEMPLE : je veux mes 10 premiers docs marqués comme

    favoris sous forme de json complet
  23. Pour ça je dois agréger en asynchrone : méta commentaires

    & auteurs description d’images
  24. DocumentService.find("userId", new Callback<List<Document>>() { public void onSuccess(List<Document> result) { final

    List<String> jsonList = new ArrayList<String>(10); int taken = 0; for (Document doc : result) { if (taken >= 10) break; if (!doc.isStarred()) continue; taken++; final CountDownLatch rendezVous = new CountDownLatch(3); final JsonObject jsonBuffer = new JsonObject(); jsonBuffer.appendInt("id", doc.getId()); jsonBuffer.append("text", doc.getText()); CommentService.findForDoc(doc, new Callback<List<Comment>>() { public void onSuccess(List<Comment> comments) { final JsonObject commentArray = JsonObject.createArray(); CountDownLatch userLatch = new CountDownLatch(comments.size()); for (Comment c : comments) { JsonObject cj = new JsonObject(); cj.append("content", c.getText()); cj.append("date", c.getDate()); UserService.find(c.getUserId(), new Callback<User>() { public void onSuccess(User user) { cj.append("author", user.getName()); cj.append("nickname", user.getLogin()); cj.append("email", user.getEmail()); // …
  25. // … continued commentArray.add(cj); userLatch.countDown(); } }); } userLatch.await(); jsonBuffer.addArray("comments",

    commentArray); rendezVous.countDown(); } }); MetaService.findForDoc(doc, new Callback<List<Meta>>() { public void onSuccess(List<Meta> metas) { jsonBuffer.addArray("meta", jsonify(metas)); rendezVous.countDown(); } }); PictureService.findAllMetas(doc.getPictures(), new Callback<List<PicMeta>>() { public void onSuccess(List<PicMeta> picMetas) { jsonBuffer.addArray("pictures", jsonify(picMetas)); rendezVous.countDown(); } }); rendezVous.await(); jsonList.add(jsonBuffer.toString()); } somethingToDo.onSuccess(jsonList); } });
  26. 2 slides en taille 10 58 lignes de code max

    7 tabulations
  27. jusqu’à 3 Callbacks imbriqués 2 usages de CountDownLatch

  28. DocumentService.find("userId", new Callback<List<Document>>() { public void onSuccess(List<Document> result) { final

    List<String> jsonList = new ArrayList<String>(10); int taken = 0; for (Document doc : result) { if (taken >= 10) break; if (!doc.isStarred()) continue; taken++; final CountDownLatch rendezVous = new CountDownLatch(3); final JsonObject jsonBuffer = new JsonObject(); jsonBuffer.appendInt("id", doc.getId()); jsonBuffer.append("text", doc.getText()); CommentService.findForDoc(doc, new Callback<List<Comment>>() { public void onSuccess(List<Comment> comments) { final JsonObject commentArray = JsonObject.createArray(); CountDownLatch userLatch = new CountDownLatch(comments.size()); for (Comment c : comments) { JsonObject cj = new JsonObject(); cj.append("content", c.getText()); cj.append("date", c.getDate()); UserService.find(c.getUserId(), new Callback<User>() { public void onSuccess(User user) { cj.append("author", user.getName()); cj.append("nickname", user.getLogin()); cj.append("email", user.getEmail()); // … THIS
  29. IS DocumentService.find("userId", new Callback<List<Document>>() { public void onSuccess(List<Document> result) {

    final List<String> jsonList = new ArrayList<String>(10); int taken = 0; for (Document doc : result) { if (taken >= 10) break; if (!doc.isStarred()) continue; taken++; final CountDownLatch rendezVous = new CountDownLatch(3); final JsonObject jsonBuffer = new JsonObject(); jsonBuffer.appendInt("id", doc.getId()); jsonBuffer.append("text", doc.getText()); CommentService.findForDoc(doc, new Callback<List<Comment>>() { public void onSuccess(List<Comment> comments) { final JsonObject commentArray = JsonObject.createArray(); CountDownLatch userLatch = new CountDownLatch(comments.size()); for (Comment c : comments) { JsonObject cj = new JsonObject(); cj.append("content", c.getText()); cj.append("date", c.getDate()); UserService.find(c.getUserId(), new Callback<User>() { public void onSuccess(User user) { cj.append("author", user.getName()); cj.append("nickname", user.getLogin()); cj.append("email", user.getEmail()); // …
  30. callback DocumentService.find("userId", new Callback<List<Document>>() { public void onSuccess(List<Document> result) {

    final List<String> jsonList = new ArrayList<String>(10); int taken = 0; for (Document doc : result) { if (taken >= 10) break; if (!doc.isStarred()) continue; taken++; final CountDownLatch rendezVous = new CountDownLatch(3); final JsonObject jsonBuffer = new JsonObject(); jsonBuffer.appendInt("id", doc.getId()); jsonBuffer.append("text", doc.getText()); CommentService.findForDoc(doc, new Callback<List<Comment>>() { public void onSuccess(List<Comment> comments) { final JsonObject commentArray = JsonObject.createArray(); CountDownLatch userLatch = new CountDownLatch(comments.size()); for (Comment c : comments) { JsonObject cj = new JsonObject(); cj.append("content", c.getText()); cj.append("date", c.getDate()); UserService.find(c.getUserId(), new Callback<User>() { public void onSuccess(User user) { cj.append("author", user.getName()); cj.append("nickname", user.getLogin()); cj.append("email", user.getEmail()); // …
  31. HEEEELL! DocumentService.find("userId", new Callback<List<Document>>() { public void onSuccess(List<Document> result) {

    final List<String> jsonList = new ArrayList<String>(10); int taken = 0; for (Document doc : result) { if (taken >= 10) break; if (!doc.isStarred()) continue; taken++; final CountDownLatch rendezVous = new CountDownLatch(3); final JsonObject jsonBuffer = new JsonObject(); jsonBuffer.appendInt("id", doc.getId()); jsonBuffer.append("text", doc.getText()); CommentService.findForDoc(doc, new Callback<List<Comment>>() { public void onSuccess(List<Comment> comments) { final JsonObject commentArray = JsonObject.createArray(); CountDownLatch userLatch = new CountDownLatch(comments.size()); for (Comment c : comments) { JsonObject cj = new JsonObject(); cj.append("content", c.getText()); cj.append("date", c.getDate()); UserService.find(c.getUserId(), new Callback<User>() { public void onSuccess(User user) { cj.append("author", user.getName()); cj.append("nickname", user.getLogin()); cj.append("email", user.getEmail()); // …
  32. Wow Such Space! DocumentService.find("userId", new Callback<List<Document>>() { public void onSuccess(List<Document>

    result) { final List<String> jsonList = new ArrayList<String>(10); int taken = 0; for (Document doc : result) { if (taken >= 10) break; if (!doc.isStarred()) continue; taken++; final CountDownLatch rendezVous = new CountDownLatch(3); final JsonObject jsonBuffer = new JsonObject(); jsonBuffer.appendInt("id", doc.getId()); jsonBuffer.append("text", doc.getText()); CommentService.findForDoc(doc, new Callback<List<Comment>>() { public void onSuccess(List<Comment> comments) { final JsonObject commentArray = JsonObject.createArray(); CountDownLatch userLatch = new CountDownLatch(comments.size()); for (Comment c : comments) { JsonObject cj = new JsonObject(); cj.append("content", c.getText()); cj.append("date", c.getDate()); UserService.find(c.getUserId(), new Callback<User>() { public void onSuccess(User user) { cj.append("author", user.getName()); cj.append("nickname", user.getLogin()); cj.append("email", user.getEmail()); // …
  33. RxJava

  34. RxJava

  35. Netflix OpenSource

  36. le pendant du Iterable - Iterator

  37. Iterable - Iterator devient Observable - Observer

  38. Iterable - Iterator devient Observable - Observer “Pull” “Push”

  39. composer

  40. composer des programmes asynchrones basés sur les événements

  41. composer des programmes asynchrones basés sur les événements en utilisant

    des séquences observables
  42. abstraire l’aspect concurrent

  43. tout est Observable sous le capot : pool de threads,

    acteurs, peu importe
  44. Flexibilité ...on gère...

  45. Flexibilité ...on gère... les valeurs uniques

  46. Flexibilité ...on gère... les séquences

  47. Flexibilité ...on gère... les flux infinis

  48. Flexibilité 3-en-1

  49. the Reactive Manifesto reactivemanifesto.org

  50. reactive-streams.org standardiser sur la jvm

  51. reactive-streams.org standardiser sur la jvm akka - reactor - rxjava

  52. Montre-moi Comment ça Marche !

  53. interface Observer<T>

  54. interface Observer<T> onNext(T data)

  55. interface Observer<T> onNext(T data) onCompleted()

  56. interface Observer<T> onNext(T data) onCompleted() onError(Throwable t)

  57. interface Observer<T> onNext(T data) onCompleted() onError(Throwable t)

  58. Observable<T>

  59. Observable<T> implémenté ou créé

  60. Observable<T> Observable.from(T… values);

  61. Observable<T> Observable.from(Iterable<T> itrbl);

  62. Observable<T> Observable.just(T oneValue);

  63. Observable<Integer> Observable.range(int start, int count);

  64. Observable<T> permet de s’abonner

  65. Observable<T> monObservable.subscribe(someObserver);

  66. Observable<T> permet de composer

  67. diagrammes “à billes”

  68. beaucoup de diagrammes “à billes”

  69. Transformation

  70. Observable<R> map(Func1<T, R> func)

  71. Observable<R> map(Func1<T, R> func)

  72. Observable<R> flatMap(Func1<T, Observable<R>> func)

  73. Observable<R> flatMap(Func1<T, Observable<R>> func)

  74. Observable<List<T>> buffer(int count)

  75. Observable<List<T>> buffer(int count)

  76. Observable<T> reduce(Func2<T,T,T> accumulator)

  77. Observable<T> reduce(Func2<T,T,T> accumulator)

  78. Observable<T> scan(Func2<T,T,T> accumulator)

  79. Observable<T> scan(Func2<T,T,T> accumulator)

  80. Filtrage

  81. Observable<T> take(int n)

  82. Observable<T> take(int n)

  83. Observable<T> skip(int n)

  84. Observable<T> skip(int n)

  85. Observable<T> filter(Func1<T, Boolean> predicate)

  86. Observable<T> filter(Func1<T, Boolean> predicate)

  87. Observable<T> sample(long period, TimeUnit unit)

  88. Observable<T> sample(long period, TimeUnit unit)

  89. Observable<T> distinct()

  90. Observable<T> distinct()

  91. Combinaison

  92. static Observable<T> concat(Observable<T> t1, Observable<T> t2)

  93. static Observable<T> concat(Observable<T> t1, Observable<T> t2)

  94. static Observable<T> merge(Observable<T> t1, Observable<T> t2)

  95. static Observable<T> merge(Observable<T> t1, Observable<T> t2)

  96. Observable<R> zip(Observable<T2> other, Func2<T, T2, R> zipFunction)

  97. Observable<R> zip(Observable<T2> other, Func2<T, T2, R> zipFunction)

  98. Parallélisme

  99. Observable<T> subscribeOn(Scheduler workThere)

  100. Observable<T> subscribeOn(Scheduler workThere)

  101. Observable<T> observeOn(Scheduler workThere)

  102. Observable<T> observeOn(Scheduler workThere)

  103. Observable<T> cache()

  104. Observable<T> cache()

  105. ...

  106. Tellement de Choix !

  107. Observable<T> permet de gérer les erreurs

  108. On a déjà la méthode onError(Throwable t) dans Observer ...

  109. ...mais on peut aussi les gérer différemment

  110. static Observable<T> mergeDelayError( Observable<T> t1, Observable<T> t2)

  111. static Observable<T> mergeDelayError( Observable<T> t1, Observable<T> t2)

  112. Observable<T> onErrorReturn(Func1<Throwable, T> fallbackFunction)

  113. Observable<T> onErrorReturn(Func1<Throwable, T> fallbackFunction)

  114. Observable<T> onErrorResumeNext(Observable<T> fallback)

  115. Observable<T> onErrorResumeNext(Observable<T> fallback)

  116. Observable<T> retry(int retryCount)

  117. Observable<T> retry(int retryCount)

  118. Je suis perdu ça donne quoi concrètement ?

  119. Observable<JsonObject> fullDocumentJson = DocumentService.find("user") .filter(new Func1<Document, Boolean>() { public Boolean

    call(Document doc) { return doc.isStarred(); }}) .take(10) .map(new Func1<Document, JsonObject>() { public JsonObject call(Document doc) { Observable<JsonObject> oc = CommentService.findForDoc(doc) .flatMap(new Func1<Comment, Observable<JsonObject>>() { public Observable<JsonObject> call(Comment c) { return UserService.find(c.getUserId()) .first() .map(new Func1<User, JsonObject>() { public JsonObject call(User u) { JsonObject result = jsonify(c) .append("author", u.getName()); return result.append("nickname", u.getLogin()) .append("email", u.getEmail()); }}); }}); Observable<JsonObject> om = MetaService.findForDoc(doc) .map(new Func1<Meta, JsonObject>() { public JsonObject call(Meta m) { return jsonify(m); }});
  120. Observable<JsonObject> op = PictureService.findAllMetas(doc.getPictures()) .map(new Func1<PicMeta, JsonObject>() { public JsonObject

    call(PicMeta p) { return jsonify(p); }}); Func2<JsonArray, JsonObject, JsonArray> arrayAggregator = new Func2<JsonArray, JsonObject, JsonArray>() { public JsonArray call(JsonArray t1, JsonObject t2) { return t1.addElement(t2); }}; JsonObject docJson = new JsonObject().appendInt("id", doc.getId()).append("text", doc.getText()); JsonArray c = new JsonArray(); docJson.addArray("comments", c); oc.reduce(c, arrayAggregator); JsonArray m = new JsonArray(); docJson.addArray("meta", m); om.reduce(m, arrayAggregator); JsonArray p = new JsonArray(); docJson.addArray("pictures", p); op.reduce(p, arrayAggregator); return docJson; } });
  121. On voit toujours rien #CallBackHell @simonbasle

  122. Observable<JsonObject> fullDocumentJson = DocumentService.find("user") .filter(new Func1<Document, Boolean>() { public Boolean

    call(Document doc) { return doc.isStarred(); }}) .take(10) .map(new Func1<Document, JsonObject>() { public JsonObject call(Document doc) { Observable<JsonObject> oc = CommentService.findForDoc(doc) .flatMap(new Func1<Comment, Observable<JsonObject>>() { public Observable<JsonObject> call(Comment c) { return UserService.find(c.getUserId()) .first() .map(new Func1<User, JsonObject>() { public JsonObject call(User u) { JsonObject result = jsonify(c) .append("author", u.getName()); return result.append("nickname", u.getLogin()) .append("email", u.getEmail()); }}); }}); Observable<JsonObject> om = MetaService.findForDoc(doc) .map(new Func1<Meta, JsonObject>() { public JsonObject call(Meta m) { return jsonify(m); }});
  123. Observable<JsonObject> op = PictureService.findAllMetas(doc.getPictures()) .map(new Func1<PicMeta, JsonObject>() { public JsonObject

    call(PicMeta p) { return jsonify(p); }}); Func2<JsonArray, JsonObject, JsonArray> arrayAggregator = new Func2<JsonArray, JsonObject, JsonArray>() { public JsonArray call(JsonArray t1, JsonObject t2) { return t1.addElement(t2); }}; JsonObject docJson = new JsonObject().appendInt("id", doc.getId()).append("text", doc.getText()); JsonArray c = new JsonArray(); docJson.addArray("comments", c); oc.reduce(c, arrayAggregator); JsonArray m = new JsonArray(); docJson.addArray("meta", m); om.reduce(m, arrayAggregator); JsonArray p = new JsonArray(); docJson.addArray("pictures", p); op.reduce(p, arrayAggregator); return docJson; } });
  124. filter on doc.isStarred()

  125. take(10)

  126. map( meta to Json)

  127. map( pictures to Json)

  128. map( comments to Json)

  129. map( comments to Json) flattened with User

  130. reduce streams of Json to single arrays

  131. c’est mieux structuré et on a pas mal de bruit

    en fait Mmh OK
  132. Java 8 la cerise sur le gâteau

  133. Java 8 la cerise sur le gâteau

  134. la version hyper courte

  135. la version hyper courte

  136. la version hyper courte

  137. fonctions de 1ère classe

  138. interfaces fonctionnelles public interface Func1<A, B> { public B call(A

    from); }
  139. interfaces fonctionnelles public interface Func1<A, B> { public B call(A

    from); } // ^ une méthode unique
  140. classes anonymes

  141. classes anonymes LAM BDAS

  142. lambdas

  143. new Func1<String, Integer>() { public Integer call(String from) { return

    from.length(); } } lambdas
  144. (String s) -> { return s.length(); } lambdas

  145. s -> s.length() lambdas

  146. références de méthodes

  147. références de méthodes MaClasse::méthodeStatique

  148. références de méthodes monInstance::méthodeInstance

  149. références de méthodes UnType::méthodeInstanceDuType

  150. références de méthodes String::length

  151. on va pouvoir éliminer le code superflu

  152. Observable<JsonObject> fullDocumentJson = DocumentService.find("user") .filter(new Func1<Document, Boolean>() { public Boolean

    call(Document doc) { return doc.isStarred(); } });
  153. Observable<JsonObject> fullDocumentJson = DocumentService.find("user") .filter(doc -> doc.isStarred());

  154. Observable<JsonObject> fullDocumentJson = DocumentService.find("user") .filter(Document::isStarred);

  155. Observable<JsonObject> fullDocumentJson = DocumentService.find("user") .filter(Document::isStarred) .take(10) .map(doc -> { Observable<JsonObject>

    oc = CommentService.findForDoc(doc) .flatMap(c -> UserService.find(c.getUserId()) .first() .map(u -> { JsonObject result = jsonify(c).append("author", u.getName()); return result.append("nickname", u.getLogin()).append("email", u.getEmail()); })); Observable<JsonObject> om = MetaService.findForDoc(doc).map(m -> jsonify(m)); Observable<JsonObject> op = PictureService.findAllMetas(doc.getPictures()).map(p -> jsonify(p)); JsonObject docJson = new JsonObject().appendInt("id", doc.getId()).append("text", doc.getText()); JsonArray c = new JsonArray(); docJson.addArray("comments", c); oc.reduce(c, (array, elem) -> array.addElement(elem)); JsonArray m = new JsonArray(); docJson.addArray("meta", m); om.reduce(m, (array, elem) -> array.addElement(elem)); JsonArray p = new JsonArray(); docJson.addArray("pictures", p); op.reduce(p, (array, elem) -> array.addElement(elem)); return docJson; });
  156. Observable<JsonObject> fullDocumentJson = DocumentService.find("user") .filter(Document::isStarred) .take(10) .map(doc -> { Observable<JsonObject>

    oc = CommentService.findForDoc(doc) .flatMap(c -> UserService.find(c.getUserId()) .first() .map(u -> { JsonObject result = jsonify(c).append("author", u.getName()); return result.append("nickname", u.getLogin()).append("email", u.getEmail()); })); Observable<JsonObject> om = MetaService.findForDoc(doc).map(m -> jsonify(m)); Observable<JsonObject> op = PictureService.findAllMetas(doc.getPictures()).map(p -> jsonify(p)); JsonObject docJson = new JsonObject().appendInt("id", doc.getId()).append("text", doc.getText()); JsonArray c = new JsonArray(); docJson.addArray("comments", c); oc.reduce(c, (array, elem) -> array.addElement(elem)); JsonArray m = new JsonArray(); docJson.addArray("meta", m); om.reduce(m, (array, elem) -> array.addElement(elem)); JsonArray p = new JsonArray(); docJson.addArray("pictures", p); op.reduce(p, (array, elem) -> array.addElement(elem)); return docJson; }); Observable<JsonObject> fullDocumentJson = DocumentService.find("user") .filter(Document::isStarred) .take(10)
  157. Observable<JsonObject> fullDocumentJson = DocumentService.find("user") .filter(Document::isStarred) .take(10) .map(doc -> { Observable<JsonObject>

    oc = CommentService.findForDoc(doc) .flatMap(c -> UserService.find(c.getUserId()) .first() .map(u -> { JsonObject result = jsonify(c).append("author", u.getName()); return result.append("nickname", u.getLogin()).append("email", u.getEmail()); })); Observable<JsonObject> om = MetaService.findForDoc(doc).map(m -> jsonify(m)); Observable<JsonObject> op = PictureService.findAllMetas(doc.getPictures()).map(p -> jsonify(p)); JsonObject docJson = new JsonObject().appendInt("id", doc.getId()).append("text", doc.getText()); JsonArray c = new JsonArray(); docJson.addArray("comments", c); oc.reduce(c, (array, elem) -> array.addElement(elem)); JsonArray m = new JsonArray(); docJson.addArray("meta", m); om.reduce(m, (array, elem) -> array.addElement(elem)); JsonArray p = new JsonArray(); docJson.addArray("pictures", p); op.reduce(p, (array, elem) -> array.addElement(elem)); return docJson; }); Observable<JsonObject> oc = CommentService.findForDoc(doc) .flatMap(c -> UserService.find(c.getUserId()) .first() .map(u -> { JsonObject result = jsonify(c) .append("author", u.getName()); .append("nickname", u.getLogin()) .append("email", u.getEmail()); return result; }) );
  158. Observable<JsonObject> fullDocumentJson = DocumentService.find("user") .filter(Document::isStarred) .take(10) .map(doc -> { Observable<JsonObject>

    oc = CommentService.findForDoc(doc) .flatMap(c -> UserService.find(c.getUserId()) .first() .map(u -> { JsonObject result = jsonify(c).append("author", u.getName()); return result.append("nickname", u.getLogin()).append("email", u.getEmail()); })); Observable<JsonObject> om = MetaService.findForDoc(doc).map(m -> jsonify(m)); Observable<JsonObject> op = PictureService.findAllMetas(doc.getPictures()).map(p -> jsonify(p)); JsonObject docJson = new JsonObject().appendInt("id", doc.getId()).append("text", doc.getText()); JsonArray c = new JsonArray(); docJson.addArray("comments", c); oc.reduce(c, (array, elem) -> array.addElement(elem)); JsonArray m = new JsonArray(); docJson.addArray("meta", m); om.reduce(m, (array, elem) -> array.addElement(elem)); JsonArray p = new JsonArray(); docJson.addArray("pictures", p); op.reduce(p, (array, elem) -> array.addElement(elem)); return docJson; }); oc.reduce(c, (array, elem) -> array.addElement(elem));
  159. 2 1 slides en taille 10 58 23 lignes de

    code max 7 3 tabulations 3 0 imbrications
  160. on a bien dégrossi notre code

  161. ah oui mais moi j’ai que Java 5/6/7 5

  162. Projet RetroLambda https://github.com/orfjackal/retrolambda

  163. now I’m Batman

  164. TakeAway

  165. du code Asynchrone

  166. sans Callbacks

  167. avec un minimum de pollution visuelle

  168. lisible et compréhensible

  169. Hope you’ll love it too !

  170. None
  171. Merci!

  172. Crédits ➔ The Door to Hell - By-Sa Flydime http://www.flickr.com/photos/flydime/4671890969

    ➔ Cat Attack - By-Nc Static416 http://www.flickr.com/photos/ehacke/4584255926/ ➔ Série Stormtroopers - By J.D. Hancock http://photos.jdhancock.com/series/stormtroopers.html ➔ Marbles Reflected - By ~Pawsitive~Candie_N http://www.flickr.com/photos/scjn/4274713988/ ➔ Marble Diagrams - Apache 2.0 Netflix OSS http://netflix.github.io/ (doc RxJava) ➔ Chien de près - By Rpavich http://www.flickr.com/photos/rpavich/11409595543/ ➔ Hyperspace - By Procsilas Moscas http://www.flickr.com/photos/procsilas/ 12821454664/ ➔ Takeaway - By Edimbhurg Blog http://www.flickr.com/photos/theedinburghblog/ 6493647769/ ➔ The End Sable - Cc0 Elektro-Plan http://pixabay.com/p-283407/