June 27, 2014 EuroClojure 2014 Krakow, Poland. Components. Just Enough

Similar documents
Clojure Lisp for the Real clojure.com

Clojure Lisp for the Real #clojure

Introduction Basics Concurrency Conclusion. Clojure. Marcel Klinzing. December 13, M. Klinzing Clojure 1/18

Symbols. abstractions, implementing, 270 through indirection, 77 with macros, 183 abstract syntax tree (AST), 149

Macro #clojureconj

Lazytest Better Living Through Protocols. Stuart Sierra. Clojure NYC April 15, 2010

ClojureScript. as a compilation target to JS. Michiel Vijay FP AMS October 16th 2014

Page 1

Clojure is. A dynamic, LISP-based. programming language. running on the JVM

Inside core.async Channels. Rich Hickey

Clojure for OOP folks Stefan innoq

clojure & cfml sitting in a tree sean corfield world singles

Applications MW Technologies Fundamentals. Evolution. Applications MW Technologies Fundamentals. Evolution. Building Blocks. Summary.

Graph: composable production systems in Clojure. Jason Wolfe Strange Loop 12

Introduction to yada. Malcolm

This tutorial is designed for all those software professionals who are keen on learning the basics of Clojure and how to put it into practice.

Object-Oriented Design

Persistent Data Structures and Managed References

Identity, State and Values

Stuart

QUIZ. What is wrong with this code that uses default arguments?

Big Modular Java with Guice

Learning from Bad Examples. CSCI 5828: Foundations of Software Engineering Lecture 25 11/18/2014

BUILDING DECLARATIVE GUIS WITH CLOJURE, JAVAFX AND FN-FX DR. NILS BLUM-OESTE

With great power comes great responsibility

15CS45 : OBJECT ORIENTED CONCEPTS

Java EE Architecture, Part Two. Java EE architecture, part two 1

Assigned Date: August 27, 2014 Due Date: September 7, 2015, 11:59 PM

Produced by. Design Patterns. MSc in Computer Science. Eamonn de Leastar

Building Flexible Systems

Thread Safety. Review. Today o Confinement o Threadsafe datatypes Required reading. Concurrency Wrapper Collections

Socket attaches to a Ratchet. 2) Bridge Decouple an abstraction from its implementation so that the two can vary independently.

Life in a Post-Functional World

com.walmartlabs/lacinia-pedestal Documentation

Functional Programming and the Web

Mock Objects and Distributed Testing

Michiel DomCode, May 26th 2015

Exam Questions 1Z0-895

.NET-6Weeks Project Based Training

Programming in Scala Second Edition

Macros, and protocols, and metaprogramming - Oh My!

Kotlin/Native concurrency model. nikolay

A- Core Java Audience Prerequisites Approach Objectives 1. Introduction

CPL 2016, week 10. Clojure functional core. Oleg Batrashev. April 11, Institute of Computer Science, Tartu, Estonia

Reference types in Clojure. April 2, 2014

StackVsHeap SPL/2010 SPL/20

Introduction to Programming Using Java (98-388)

Clojure. A (not-so-pure) functional approach to concurrency. Paolo Baldan Linguaggi per il Global Computing AA 2016/2017

Advanced React JS + Redux Development

Fall Problem Set 1

Programming Safe Agents in Blueprint. Alex Muscar University of Craiova

CS193k, Stanford Handout #12. Threads 4 / RMI

For this week, I recommend studying Chapter 2 of "Beginning Java EE 7".

CS 2340 Objects and Design - Scala

Clojure Concurrency Constructs. CSCI 5828: Foundations of Software Engineering Lecture 12 10/02/2014

A Transactional Model and Platform for Designing and Implementing Reactive Systems

An Explicit-Continuation Metacircular Evaluator

ANGULAR 2.X,4.X + TYPESRCIPT by Sindhu

Favoring Isolated Mutability The Actor Model of Concurrency. CSCI 5828: Foundations of Software Engineering Lecture 24 04/11/2012

Top Down Design vs. Modularization

What if Type Systems were more like Linters?

Optimising large dynamic code bases.

A Separation of Concerns Clean Architecture on Android

INHERITANCE: EXTENDING CLASSES

Modular Java Applications with Spring, dm Server and OSGi

Training topic: OCPJP (Oracle certified professional Java programmer) or SCJP (Sun certified Java programmer) Content and Objectives

Modellistica Medica. Maria Grazia Pia, INFN Genova. Scuola di Specializzazione in Fisica Sanitaria Genova Anno Accademico

Lecture 8: Summary of Haskell course + Type Level Programming

Frustrated by all the hype?

CS558 Programming Languages

Compositional Design Principles

COURSE DETAILS: CORE AND ADVANCE JAVA Core Java

JAVA MOCK TEST JAVA MOCK TEST II

Chapter 6. Object- Oriented Programming Part II

Behind the scenes - Continuous log analytics

DOT NET COURSE BROCHURE

Clojure. The Revenge of Data. by Vjeran Marcinko Kapsch CarrierCom

Dagger 2 Android. Defeat the Dahaka" Garima

Ruby: Objects and Dynamic Types

CSE 70 Final Exam Fall 2009

COPYRIGHTED MATERIAL. Table of Contents. Foreword... xv. About This Book... xvii. About The Authors... xxiii. Guide To The Reader...

Principles of Programming Languages. Objective-C. Joris Kluivers

C++ Threading. Tim Bailey

Magento Technical Guidelines

Program to an Interface (a.k.a. - P2I) Not an Implementation

Java for Programmers Course (equivalent to SL 275) 36 Contact Hours

Absolute C++ Walter Savitch

Refactoring Without Ropes

Applied Unified Ownership. Capabilities for Sharing Across Threads

Program to an Interface, Not an Implementation

Object Oriented Paradigm

National Aeronautics and Space and Administration Space Administration. cfe Release 6.6

COMPUTER SCIENCE TRIPOS

Design Patterns in Python (Part 2)

Comp-304 : Object-Oriented Design What does it mean to be Object Oriented?

INF 212 ANALYSIS OF PROG. LANGS FUNCTION COMPOSITION. Instructors: Crista Lopes Copyright Instructors.

CPSC/ECE 3220 Fall 2017 Exam Give the definition (note: not the roles) for an operating system as stated in the textbook. (2 pts.

CST242 Concurrency Page 1

Chapter 4 Remote Procedure Calls and Distributed Transactions

Spring Interview Questions

Transcription:

June 27, 2014 EuroClojure 2014 Krakow, Poland Components Just Enough Structure @stuartsierra

Presentation Business Logic DB

SMS Email Presentation Thread Pool Business Logic Queues Public API Private API Cache DB DB Monitoring Scheduler DB

SMS Email Presentation Thread Pool Public API Private API Queues Cache DB DB Monitoring Scheduler DB

SMS Email Presentation Thread Pool Queues Static Config Public API Private API Cache DB DB Monitoring Scheduler DB

SMS Email Presentation Thread Pool Queues External Resources Public API Private API Cache DB DB Monitoring Scheduler DB

SMS Email Presentation Thread Pool Queues Process State Public API Private API Cache DB DB Monitoring Scheduler DB

Structure Built-In

Structure Built-In public class Foo { private URL url; private Socket connection; public Foo(URL url) {...} public void connect() {...} } public void shutdown() {...}

Structure Built-In public class Foo { private URL url; Static config private Socket connection; public Foo(URL url) {...} Runtime state Constructor public void connect() {...} Lifecycle } public void shutdown() {...}

Not a Lot of Structure

Not a Lot of Structure (ns my-app.database) (def url " ") (def connection (atom nil)) (defn connect [] (swap connection )) (defn shutdown [] (swap connection ))

Not a Lot of Structure (ns my-app.database) Not instantiable (def url " ") Singletons (def connection (atom nil)) Global mutable (defn connect [] (swap connection )) (defn shutdown [] (swap connection )) variables Global effects

Bootstrapping (defn start-all [] (database/connect) (create-queues) (start-thread-pool) (start-web-server))

Bootstrapping (defn start-all [] (database/connect) (create-queues) (start-thread-pool) (start-web-server)) All modifying global state Manual ordering

Bootstrapping (defn start-all [] (database/connect) (create-queues) (start-thread-pool) (start-web-server)) All modifying global state Manual ordering Hidden dependencies Hard to test

Just Enough Structure

Component

Component Immutable data structure (map or record) Public API Managed lifecycle Relationships to other components

Component It's an object Immutable data structure (map or record) Public API Managed lifecycle Relationships to other components

Component It's an object Immutable data structure (map or record) Public API Focus on behavior Managed lifecycle Relationships to other components

State Wrapper Component (defrecord DB [host conn] )

State Wrapper Component Encapsulates state (defrecord DB [host conn] )

State Wrapper Component Encapsulates state (defrecord DB [host conn] ) Opaque to most consumers

Public API (defrecord DB [host conn] ) (defn query [db & ] (.doquery (:conn db) )) (defn insert [db & ] (.dostatement (:conn db) ))

Public API (defrecord DB [host conn] ) (defn query [db & ] (.doquery (:conn db) )) Take component as argument (defn insert [db & ] (.dostatement (:conn db) ))

Public API (defrecord DB [host conn] ) (defn query [db & ] (.doquery (:conn db) )) May have side effects (defn insert [db & ] (.dostatement (:conn db) ))

Public API (defrecord DB [host conn] ) (defn query [db & ] (.doquery (:conn db) )) (defn insert [db & ] (.dostatement (:conn db) )) May use internal state

Lifecycle: Constructor (defrecord DB [host conn]...) (defn db [host] (map->db {:host host}))

Lifecycle: Constructor (defrecord DB [host conn]...) (defn db [host] (map->db {:host host})) Creates initial state

Lifecycle: Constructor (defrecord DB [host conn]...) (defn db [host] (map->db {:host host})) Creates initial state No side effects

Lifecycle: Transitions (ns com.stuartsierra.component) (defprotocol Lifecycle (start [component]) (stop [component])) Default implementation returns component

Lifecycle: Transitions (defrecord DB [host conn] component/lifecycle (start [this] (assoc this :conn (Driver/connect host))) (stop [this] (.stop conn) this))

Lifecycle: Transitions (defrecord DB [host conn] component/lifecycle (start [this] (assoc this :conn (Driver/connect host))) (stop [this] (.stop conn) this)) Lifecycle protocol

Lifecycle: Transitions (defrecord DB [host conn] component/lifecycle (start [this] (assoc this :conn (Driver/connect host))) (stop [this] (.stop conn) this)) May have side effects

Lifecycle: Transitions (defrecord DB [host conn] component/lifecycle (start [this] (assoc this :conn (Driver/connect host))) (stop [this] (.stop conn) this)) Always return updated component

Service Provider Component (defrecord Email [endpoint api-key] ) (defn send [email address body] )

Service Provider Component (defrecord Email [endpoint api-key] ) Encapsulates state (defn send [email address body] )

Service Provider Component (defrecord Email [endpoint api-key] ) Encapsulates state Public API provides services (defn send [email address body] )

Domain Model? public class Customer { } private String name; private Address address; public void notify() { }

Domain Model? public class Customer { } private String name; private Address address; public void notify() { } Just structured data

Domain Model? public class Customer { } private String name; private Address address; public void notify() { } Just structured data Mingled with behavior

Let Data Be Data

Let Data Be Data Clojure maps, records, vectors, Immutable values No behavior

Domain Model Component (defrecord Customers [db email]) (defn notify [customers name message] (let [{:keys [db email]} customers address (query db name)] (send email address message)))

Domain Model Component (defrecord Customers [db email]) Represents aggregate operations (defn notify [customers name message] (let [{:keys [db email]} customers address (query db name)] (send email address message)))

Domain Model Component (defrecord Customers [db email]) Encapsulates related dependencies (defn notify [customers name message] (let [{:keys [db email]} customers address (query db name)] (send email address message)))

Domain Model Component (defrecord Customers [db email]) Public API takes component as argument (defn notify [customers name message] (let [{:keys [db email]} customers address (query db name)] (send email address message)))

Domain Model Component (defrecord Customers [db email]) Gets dependencies out of component (defn notify [customers name message] (let [{:keys [db email]} customers address (query db name)] (send email address message)))

Domain Model Component (defrecord Customers [db email]) (defn notify [customers name message] (let [{:keys [db email]} customers address (query db name)] (send email address message))) Uses public API of other components

Domain Model Component (defrecord Customers [db email]) (defn notify [customers name message] (let [{:keys [db email]} customers address (query db name)] (send email address message))) Uses public API of other components Treats other components as opaque

Constructor with Dependencies (defn customers [] (component/using (map->customers {}) [:db :email]))

Constructor with Dependencies (defn customers [] (component/using (map->customers {}) [:db :email])) Declare dependencies by name (Adds metadata)

Combining Components

System Map (defn system [ ] (component/system-map :customers (customers) :db (db ) :email (->Email) ))

System Map System constructor (defn system [ ] (component/system-map :customers (customers) :db (db ) :email (->Email) ))

System Map (defn system [ ] (component/system-map :customers (customers) :db (db ) :email (->Email) )) Associates names with components

System Map (defn system [ ] (component/system-map :customers (customers) :db (db ) :email (->Email) )) Associates names with components Manages lifecycle Provides dependencies

System SystemMap :customers :db :email Customers DB Email

Starting a System (component/start the-system) SystemMap :customers :db :email Customers DB Email

Starting a System Reads dependency metadata SystemMap :customers :db :email Customers DB Email [:db :email]

Starting a System Sorts in dependency order SystemMap :customers :db :email Email DB Customers [:db :email]

Starting a System Starts each component SystemMap :customers :db :email Email DB Customers

Starting a System Starts each component SystemMap :customers :db :email Email DB Customers component/start

Starting a System Associates dependencies SystemMap :customers :db :email Email DB Customers assoc :db assoc :email

Starting a System Starts after dependencies SystemMap :customers :db :email Email DB Customers :db :email component/start

Starting a System Associates updated components SystemMap :customers :db :email back into system Email DB Customers :db :email

Stopping a System Same but in reverse order SystemMap :customers :db :email Email DB Customers :db :email

Stopping a System Same but in reverse order SystemMap :customers :db :email Email DB Customers :db :email

Before Start #SystemMap{ :customers #Customers{:db nil :email nil} :db #DB{ } :email #Email{ }}

Before Start #SystemMap{ :customers Dependencies unset #Customers{:db nil :email nil} :db #DB{ } :email #Email{ }}

After Start #SystemMap{ :customers #Customers{:db #DB{ } :email #Email{ }} :db #DB{ } :email #Email{ }}

After Start Assoc'd dependencies #SystemMap{ :customers #Customers{:db #DB{ } :email #Email{ }} :db #DB{ } :email #Email{ }}

After Start Assoc'd dependencies #SystemMap{ :customers #Customers{:db #DB{ } :email #Email{ }} :db #DB{ } Same values :email #Email{ }}

After Start Assoc'd dependencies #SystemMap{ :customers #Customers{:db #DB{ } :email #Email{ }} :db #DB{ } Same values :email #Email{ }} As returned by 'start'

Injecting Alternatives (defn test-system [ ] (assoc (system ) :email (stub-email) :db (test-db)))

Injecting Alternatives (defn test-system [ ] (assoc (system ) :email (stub-email) :db (test-db))) Assoc alternate components into system map

Injecting Alternatives (defn test-system [ ] (assoc (system ) :email (stub-email) :db (test-db))) Assoc alternate components into system map Before start

Stub Service for Testing (defprotocol Send (send [email-service address body])) (defrecord StubEmail [] Send (send [_ address body] ;; record or save input )) (defn stub-email [] (->StubEmail))

Stub Service for Testing (defprotocol Send (send [email-service address body])) (defrecord StubEmail [] Send (send [_ address body] ;; record or save input )) (defn stub-email [] (->StubEmail)) Function into protocol

Stub Service for Testing (defprotocol Send (send [email-service address body])) (defrecord StubEmail [] Send (send [_ address body] ;; record or save input )) (defn stub-email [] (->StubEmail)) Function into protocol Stub implementation Constructor

DB for Testing (defrecord TestDB [host conn] component/lifecycle (start [this] (let [conn (Driver/connect host)] (load-seed-data conn) (assoc this :conn conn))) (stop [this] (drop-database conn) (.stop conn) this))

DB for Testing (defrecord TestDB [host conn] component/lifecycle (start [this] (let [conn (Driver/connect host)] (load-seed-data conn) (assoc this :conn conn))) (stop [this] (drop-database conn) (.stop conn) this)) Doesn't mock DB

DB for Testing (defrecord TestDB [host conn] component/lifecycle Doesn't mock DB (start [this] (let [conn (Driver/connect host)] (load-seed-data conn) (assoc this :conn conn))) (stop [this] (drop-database conn) (.stop conn) this)) Creates/destroys for each use

Testing Business Logic (deftest t-notify-customer (let [system (component/start (test-system)) {:keys [customers]} system] (try (notify customers "bob" "Hi, Bob") ;; make some assertions (finally (component/stop system)))))

Testing Business Logic Isolated system (deftest t-notify-customer (let [system (component/start (test-system)) {:keys [customers]} system] (try (notify customers "bob" "Hi, Bob") ;; make some assertions (finally (component/stop system)))))

Var Substitution & Asynchrony (deftest t-notify-customer (with-redefs [send ] (binding [*db* ] (notify ) (is ))))

Var Substitution & Asynchrony (deftest t-notify-customer (with-redefs [send ] (binding [*db* ] (notify ) (is )))) Delimited in time

Var Substitution & Asynchrony (deftest t-notify-customer (with-redefs [send ] (binding [*db* ] (notify ) (is )))) Delimited in time Potential race conditions

Var Substitution & Asynchrony (deftest t-notify-customer (with-redefs [send ] (binding [*db* ] (notify ) (is )))) Delimited in time Potential race conditions Tightly coupled to implementation

Var Substitution & Asynchrony (deftest t-notify-customer (with-redefs [send ] (binding [*db* ] (notify ) (is )))) Delimited in time Potential race conditions Tightly coupled to implementation Wrong level of granularity

Entry Points

Entry Point: Main (ns com.example.app (:require [com.stuartsierra.component :as component])) (defn -main [ ] (component/start (system )))

Entry Point: Global (def sys (atom nil)) (defn init [] (reset sys (system))) (defn start [] (reset sys (component/start @sys))) (defn stop [] (reset sys (component/stop @sys)))

Entry Point: Global (def sys (atom nil)) (defn init [] (reset sys (system))) (defn start [] (reset sys (component/start @sys))) (defn stop [] (reset sys (component/stop @sys))) Exactly one mutable global for system

Entry Point: Global (def sys (atom nil)) (defn init [] (reset sys (system))) (defn start [] (reset sys (component/start @sys))) (defn stop [] (reset sys (component/stop @sys))) Exactly one mutable global for system Functions on values

Entry Point: Global (def sys (atom nil)) (defn init [] (reset sys (system))) (defn start [] (reset sys (component/start @sys))) (defn stop [] (reset sys (component/stop @sys))) Exactly one mutable global for system Functions on values Transition from one state to the next

Entry Point: Global (def sys (atom nil)) (defn init [] (reset sys (system))) (defn start [] (reset sys (component/start @sys))) Not swap because (defn stop [] of side-effects (reset sys (component/stop @sys)))

Web App: Static Routes (defroutes routes (GET "/foo" [req] get-foo) (POST "/bar" [req] do-bar)) (def server (start-server routes))

Web App: Static Routes (defroutes routes (GET "/foo" [req] get-foo) (POST "/bar" [req] do-bar)) Static (def server (start-server routes))

Web App: Static Routes (defroutes routes (GET "/foo" [req] get-foo) (POST "/bar" [req] do-bar)) Static (def server (start-server routes)) How to get components in here?

Web App: Static Routes (defroutes routes (GET "/foo" [req] get-foo) (POST "/bar" [req] do-bar)) Static (def server (start-server routes)) How to get components in here? Don't just deref system everywhere

Entry Point: Web Request (defn system-middleware [ring-handler] (fn [request] (ring-handler (assoc request ::web-app (::web-app @sys)))))

Entry Point: Web Request (defn system-middleware [ring-handler] (fn [request] (ring-handler (assoc request ::web-app (::web-app @sys))))) Get component from system at beginning of each request

Entry Point: Compiling Routes (defn make-routes [web-app] (routes (GET "/foo" [request] (get-foo web-app request)) (POST "/bar" [request] (do-bar web-app request))))

Entry Point: Compiling Routes Construct routes in a function (defn make-routes [web-app] (routes (GET "/foo" [request] (get-foo web-app request)) (POST "/bar" [request] (do-bar web-app request))))

Entry Point: Compiling Routes Construct routes in a function Close over (defn make-routes [web-app] (routes (GET "/foo" [request] (get-foo web-app request)) (POST "/bar" [request] (do-bar web-app request)))) component

Entry Point: Compiling Routes Construct routes in a function Close over (defn make-routes [web-app] (routes (GET "/foo" [request] (get-foo web-app request)) (POST "/bar" [request] (do-bar web-app request)))) Pass to handlers component

Entry Point: Compiling Routes (defrecord WebApp [customers server] component/lifecycle (start [this] (assoc this :server (start-server (make-routes this)))) (stop [this] (stop-server server) this))

Entry Point: Compiling Routes (defrecord WebApp [customers server] component/lifecycle Create routes (start [this] in 'start' (assoc this :server (start-server (make-routes this)))) (stop [this] (stop-server server) this))

Entry Point: Compiling Routes (defrecord WebApp [customers server] component/lifecycle Create routes (start [this] in 'start' (assoc this :server (start-server (make-routes this)))) (stop [this] Capture (stop-server server) this)) component

Extensions

Renaming Dependencies (defn test-system [] (system-map :test/database (test-db) :test/email (stub-email) :customers (component/using (customers) {:db :test/database :email :test/email})))

Renaming Dependencies (defn test-system [] (system-map :test/database (test-db) :test/email (stub-email) :customers (component/using (customers) {:db :test/database :email :test/email}))) Component s dependency keys

Renaming Dependencies (defn test-system [] (system-map :test/database (test-db) :test/email (stub-email) :customers (component/using (customers) {:db :test/database :email :test/email}))) Component s dependency keys System keys

Merging Systems ;; System A {:a/web-app :a/server :db :email } ;; System B {:b/web-app :b/server :db :email }

Merging Systems ;; System A {:a/web-app :a/server :db :email } ;; System B {:b/web-app :b/server :db :email } (merge system-a system-b)

Merging Systems ;; System A {:a/web-app :a/server :db :email } ;; System B {:b/web-app :b/server :db :email } Namespace-qualified keys prevent clashes (merge system-a system-b)

Merging Systems ;; System A {:a/web-app :a/server :db :email } ;; System B {:b/web-app :b/server :db :email } (merge system-a system-b) Common keys reused Great for testing

core.async (defrecord Processor [input-ch output-ch] component/lifecycle (start [this] (assoc this :go (go-loop [] (when-some [val (< input-ch)] )))) (stop [this] (close input-ch) this)))

core.async (defrecord Processor [input-ch output-ch] component/lifecycle (start [this] (assoc this dependencies :go (go-loop [] (when-some [val (< input-ch)] )))) (stop [this] (close input-ch) this))) Channels as

core.async (defrecord Processor [input-ch output-ch] component/lifecycle (start [this] (assoc this dependencies :go (go-loop [] (when-some [val (< input-ch)] )))) (stop [this] (close input-ch) this))) Channels as Create event loop in start

core.async (defn system [] :events (chan 100) :log (chan 100) :processor (component/using (->Processor) {:input-ch :events :output-ch :log}) )

core.async Create channels (defn system [] :events (chan 100) in system :log (chan 100) :processor (component/using (->Processor) {:input-ch :events :output-ch :log}) )

core.async Create channels (defn system [] :events (chan 100) in system :log (chan 100) :processor (component/using (->Processor) {:input-ch :events :output-ch :log}) ) Connect to component

core.async Create channels (defn system [] :events (chan 100) in system :log (chan 100) :processor (component/using (->Processor) {:input-ch :events :output-ch :log}) ) Connect to component Insert mult / pipe / etc. for indirection

Summary

Advantages Explicit dependencies and clear boundaries Isolation, decoupling Easier to test, refactor Automatic ordering of start/stop Easy to swap in alternate implementations Everything at most one map lookup away

Disadvantages Requires whole-app buy-in System map is too big to inspect visually Cannot start/stop just part of a system Boilerplate Records, constructors, Lifecycles, dependencies Destructuring component fields in functions

Possible Future Additional lifecycle protocols init acquires resources but does not start release closes resources and drops dependencies

Possible Future Handle mutable containers for systems Allow individual components to start, stop, or change at runtime Deref container and get current component with latest dependencies Catch errors, mark component as failed

@stuartsierra stuartsierra.com github.com/stuartsierra/component