1 Concurrency with Promise Style Rayco Araña Software Engineer at Tuenti [email protected]
2 ¿De qué vamos a hablar? El patrón Promise Promesas vs Listeners Paralelizar sin morir en el intento Algo de reactive programming… o no El patrón Promise Promesas vs Listeners Paralelizar sin morir en el intento Algo de reactive programming… o no
3 Usando listeners… public class FileManager { public void read(File jsonFile, OnFileReadListener listener) {... } }
4 Usando listeners… public void foo() { FileManager fileManager =...; //Initialize fileManager.read(new File("config.json"), new OnFileReadListener() { public void onFileRead(JSONObject jsonFile) {... } public void onError(Exception ex) {... } }); }
5 Usando listeners…
6 Problemas… Sincronizar múltiples llamadas?
7 Problemas… Sincronizar múltiples llamadas? Implementación síncrona? Sincronizar múltiples llamadas? Implementación síncrona?
8 Problemas… La pila de ejecución queda contaminada public class CacheFileManager implements FileManager { private Cache cache; public void read(File jsonFile, OnFileReadListener listener) { JSONObject object = cache.get(jsonFile); if(object != null) { listener.onFileRead(object); } else {... } } }
9 Problemas… Sincronizar múltiples llamadas? Implementación síncrona? En qué hilo se ejecuta el Listener? Sincronizar múltiples llamadas? Implementación síncrona? En qué hilo se ejecuta el Listener?
10 Problemas… Sincronizar múltiples llamadas? Implementación síncrona? En qué hilo se ejecuta el Listener? Es esto “clean code”? Sincronizar múltiples llamadas? Implementación síncrona? En qué hilo se ejecuta el Listener? Es esto “clean code”?
11 Problemas… Output arguments are counterintuitive. Readers expect arguments to be inputs, not outputs. If your function must change the state of something, have it change the state of the object it is called on. Robert C. Martin
12 Promise style… public void foo() { FileManager fileManager =...; //Initialize Promise promise = fileManager.read(new File("config.json")); } public class FileManager { public Promise read(File jsonFile) {... } }
13 Patrón Promise Objeto que hace de proxy con el resultado de una operación El resultado suele ser desconocido debido a que su cálculo aún no ha acabado Objeto que hace de proxy con el resultado de una operación El resultado suele ser desconocido debido a que su cálculo aún no ha acabado
14 Patrón Promise. Características Código más legible Concurrencia más sencilla Tests más sencillos Sin ArgumentCaptors para capturar callbacks que luego estimular. Código más legible Concurrencia más sencilla Tests más sencillos Sin ArgumentCaptors para capturar callbacks que luego estimular.
15 jDeferred Implementación Java del patrón Promise Inspirado en Deferred Object de jQuery Soporte específico para Android Código muy compacto con Lambdas en Java 8 http://jdeferred.org Implementación Java del patrón Promise Inspirado en Deferred Object de jQuery Soporte específico para Android Código muy compacto con Lambdas en Java 8 http://jdeferred.org
16 jDeferred. Claves Promise : interface Promesa que recibes al realizar una operación. DeferredObject : class Objeto con el que controlas el estado de la promesa AndroidDeferredObject DeferredManager : interface Gestor de promesas DefaultDeferredManager implementación por defecto AndroidDefererdManager Promise : interface Promesa que recibes al realizar una operación. DeferredObject : class Objeto con el que controlas el estado de la promesa AndroidDeferredObject DeferredManager : interface Gestor de promesas DefaultDeferredManager implementación por defecto AndroidDefererdManager
17 jDeferred. Flujo Hilo 2 create ClienteAPI DeferredObject init attachCallbacks resolve() executeCallbacks read()
18 Vamos a implementar una API sencilla jDeferred. Caso práctico I public class FileManager { public Promise read(File jsonFile) {... } }
19 Creamos un DeferredObject Realizamos la operación en background Devolvemos la promesa Creamos un DeferredObject Realizamos la operación en background Devolvemos la promesa jDeferred. Caso práctico II public Promise read(File jsonFile) { DeferredObject deferredObject = new DeferredObject(); doReadAsync(jsonFile, deferredObject); return deferredObject.promise(); }
20 El trabajo lo dejamos para que lo ejecute un ThreadPool jDeferred. Caso práctico III private Executor threadPool = Executors.newSingleThreadExecutor(); //Whatever thread pool private void doReadAsync(File jsonFile, DeferredObject deferredObject) { threadPool.execute(new Runnable() { @Override public void run() { doTryRead(jsonFile, deferredObject); } }); }
21 Tratamos el caso de error e implementamos la lógica jDeferred. Caso práctico IV private void doTryRead(File jsonFile, DeferredObject deferredObject) { try { deferredObject.resolve(doRead(jsonFile)); } catch (Exception ex) { deferredObject.reject(ex); } } private JSONObject doRead(File jsonFile) throws IOException, ParseException { JSONObject jsonObject =...; //Read the file return jsonObject; }
22 Usando la API que hemos creado jDeferred. Caso práctico V FileManager fileManager = new FileManager(); fileManager.read(new File("config.json")).done(new DoneCallback () { @Override public void onDone(JSONObject result) { config.init(result); view.showMessage("Config loaded."); } }).fail(new FailCallback () { @Override public void onFail(Exception ex) { config.initWithDefaults(); view.showMessage("Default config loaded."); } });
23 Con Java 8 queda aún más compacto jDeferred. Caso práctico V FileManager fileManager = new FileManager(); fileManager.read(new File("config.json")).done((result) -> { config.init(result); view.showMessage("Config loaded."); }).fail((ex) -> { config.initWithDefaults(); view.showMessage("Default config loaded."); });
24 jDeferred. Caso práctico VI Mocking de forma sencilla private void givenSomeValidConfigFile() { FileManager fileManager = mock(FileManager.class); JSONObject jsonObject =...//Create the mock config DeferredObject deferredObject = new DeferredObject(); when(fileManager.read(any(File.class))).thenReturn(deferredObject.resolve(jsonObject)); }
25 Flexibilidad Implementación síncrona de la API muy sencilla private Cache cache = new Cache(); //Some cache private FileManager networkFileManager = new NetworkFileManager(); public Promise read(File jsonFile) { JSONObject jsonObject = cache.get(jsonFile); if (jsonObject != null) { return new DeferredObject().resolve(jsonObject); } else { return networkFileManager.read(jsonFile); //Delegate } }
26 ¿En qué hilo se ejecuta el Listener? ¿UI o Background? UI Cosas en el hilo de UI que no queremos Background Cuando queremos hacer algo en UI tenemos que recurrir a Handlers Problema subyacente Responsabilidad recae en la API: Bad idea Es el cliente de la API quien debe decidir esto ¿En qué hilo se ejecuta el Listener? ¿UI o Background? UI Cosas en el hilo de UI que no queremos Background Cuando queremos hacer algo en UI tenemos que recurrir a Handlers Problema subyacente Responsabilidad recae en la API: Bad idea Es el cliente de la API quien debe decidir esto Listeners en Android
27 AndroidDeferredObject Wrapper de DeferredObject para añadir soporte para Android Nuevos callbacks AndroidDoneCallback AndroidFailCallback AndroidProgressCallback AndroidAlwaysCallback Método para elegir en que hilo se va a ejecutar A partir de 1.2.5, se puede indicar con una anotación AndroidDeferredObject Wrapper de DeferredObject para añadir soporte para Android Nuevos callbacks AndroidDoneCallback AndroidFailCallback AndroidProgressCallback AndroidAlwaysCallback Método para elegir en que hilo se va a ejecutar A partir de 1.2.5, se puede indicar con una anotación jDeferred en Android
28 Consejo: Crear clases abstractas que oculten el método public abstract class UIDoneCallback implements AndroidDoneCallback { public AndroidExecutionScope getExecutionScope() { return AndroidExecutionScope.UI; } public abstract class BackgroundDoneCallback implements AndroidDoneCallback { public AndroidExecutionScope getExecutionScope() { return AndroidExecutionScope.BACKGROUND; }
29 jDeferred en Android Adios Handlers! FileManager fileManager = new FileManager(); fileManager.read(new File("config.json")).done(new BackgroundDoneCallback () { @Override public void onDone(JSONObject result) { config.init(result); } }).done(new UIDoneCallback () { @Override public void onDone(JSONObject result) { view.showMessage("Config loaded."); } });
30 CPUs con muchos cores Algunos se apagan, pero hay un consumo mínimo Usar todos de forma eficiente permite apagar la CPU antes Un ThreadPool no es suficiente Nos ayuda a lanzar trabajos en paralelo ¿Cómo sincronizar trabajos dependientes entre si? Herramientas de bajo nivel… complicadas de depurar. CPUs con muchos cores Algunos se apagan, pero hay un consumo mínimo Usar todos de forma eficiente permite apagar la CPU antes Un ThreadPool no es suficiente Nos ayuda a lanzar trabajos en paralelo ¿Cómo sincronizar trabajos dependientes entre si? Herramientas de bajo nivel… complicadas de depurar. Paralelizar trabajo
31 DeferredManager. Permite lanzar y/o sincronizar promesas Devuelve otra promesa DeferredManager. Permite lanzar y/o sincronizar promesas Devuelve otra promesa Paralelizar trabajo DeferredManager deferredManager = new DefaultDeferredManager(); Promise configPromise = fileManager.read(new File("config.json")); Promise serverPromise = fileManager.read(new File("server.json")); Promise userPromise = fileManager.read(new File("user.json")); deferredManager.when(configPromise, serverPromise, userPromise).done(new DoneCallback () { @Override public void onDone(MultipleResults results) { for (OneResult result : results) { view.showJSON((JSONObject) result.getResult()); } view.showMessage("All files loaded!"); } });
32 Filters Nos permiten manipular o transformar una promesa deferredManager.when(configPromise, serverPromise, userPromise).then(new DoneFilter () { @Override public Config filterDone(MultipleResults results) { JSONObject config = (JSONObject) results.get(0).getResult(); JSONObject server = (JSONObject) results.get(1).getResult(); JSONObject user = (JSONObject) results.get(2).getResult(); return new Config(config, server, user); } }).done(result -> view.showConfig(result)).fail(ex -> view.showMessage("Config not loaded :-("));
33 Filters Se puede aplicar a cualquier tipo de promesa fileManager.read(new File("config.json")).then(new DoneFilter () { @Override public Config filterDone(JSONObject result) { return new Config(result); } }).done(result -> view.showConfig(result)).fail(ex -> view.showMessage("Config not loaded :-("));
34 Pipes Más potente que los filtros Transformar con posibilidad de errores Más potente que los filtros Transformar con posibilidad de errores fileManager.read(new File("config.json")).then(new DonePipe () { @Override public Promise pipeDone(JSONObject result) { DeferredObject deferred = new DeferredObject(); if (Config.isValid(result)) { return deferred.resolve(new Config(result)); } else { return deferred.reject(new IllegalConfigException()); } } }).done(result -> view.showConfig(result)).fail(ex -> view.showMessage("Config not loaded :-("));
35 Pipes Pero también encadenar operaciones fileManager.read(new File("user/server.json")).then(new DonePipe () { @Override public Promise pipeDone(JSONObject result) { Server server = new Server(result); return fileManager.read(server.getConfigPath()); } }).done(result -> view.showConfig(result)).fail(ex -> view.showMessage("User config not loaded :-("));
36 …
37 jDeferred no es RxJava Promesas tienen estado No se puede reutilizar Solo se puede llamar 1 vez a resolve() o reject() A notify() si podemos llamar muchas veces Para progreso, no para notificar cambios Ni mejor ni peor, depende de nuestros requisitos. Promesas tienen estado No se puede reutilizar Solo se puede llamar 1 vez a resolve() o reject() A notify() si podemos llamar muchas veces Para progreso, no para notificar cambios Ni mejor ni peor, depende de nuestros requisitos.
38 The end!
39 We are hiring! ¿Quieres unirte a nosotros? Buscas un ambiente creativo Grandes desafíos Desarrollar productos que cambien la vida de millones de personas ¿Quieres unirte a nosotros? Buscas un ambiente creativo Grandes desafíos Desarrollar productos que cambien la vida de millones de personas Envíanos tu CV!
40 Tuenti challenge 5
41 ¿Preguntas? jDeferred http://jdeferred.org Mi blog personal http://raycoarana.com Tuenti Developers Blog http://corporate.tuenti.com/es/dev/blog jDeferred http://jdeferred.org Mi blog personal http://raycoarana.com Tuenti Developers Blog http://corporate.tuenti.com/es/dev/blog Rayco Araña Software Engineer at Tuenti @raycoarana [email protected] Rayco Araña Software Engineer at Tuenti @raycoarana [email protected]