On Github ogrigas / living-without-objects
Osvaldas Grigas | @ogrigas
public class ZipDownloadService { public List<File> downloadAndExtract(String location) { } }
public class FileDownloader { public List<File> downloadFiles(String location) { ... } }
public class ZipExtractor { public File extractZip(File archive) { ... } }
(defn download-files [location] (...)) (defn extract-zip [archive] (...))
in a nutshell
( some-function ( arg1 , arg2 , arg3 )
public class ProductCatalog { public ProductId Save(Product product) { ... } public Product FindById(ProductId id) { ... } }
public class ProductSaver { public ProductId Save(Product product) { ... } }
public class ProductFinder { public Product FindById(ProductId id) { ... } }
Somethin' ain't right
public class ProductRepository { public ProductId Save(Product product) { ... } }
public class ProductQuery { public Product FindById(ProductId id) { ... } }
Feelin' good now
(defn save-product [product] (...)) (defn find-product-by-id [id] (...))
Applying OO design principles often leads to...
functional design
(ns my.product.repository) (defn save [product] (...)) (defn find-by-id [id] (...))
(require '[my.product.repository :as product-repo]) (product-repo/find-by-id 42)
Who cares!
We'll get there
Avoiding hard-coded dependencies
public class ProfilePage { public String render(Repository repository, int customerId) { return toHtml(repository.loadProfile(customerId)); } }
Repository repository = new Repository(); ProfilePage page = new ProfilePage();
String html = page.render(repository, customerId);
(defn render-page [repository-fn customer-id] (to-html (repository-fn customer-id)))
(defn load-profile [customer-id] (...))
(render-page load-profile customer-id)
ProfilePage pageInjected = new ProfilePage(new Repository());
pageInjected.render(customerId);
(def render-injected (fn [customer-id] (render-page load-profile customer-id)))
(render-injected customer-id)
(def render-injected (partial render-page load-profile))
(render-injected customer-id)
(defn to-view-model [profile] (...)) (render-page (comp to-view-model load-profile) customer-id)
(defn with-logging [f] (fn [& args] (log/debug "Called with params" args) (def [result (apply f args)] (log/debug "Returned" result) result))) (render-page (with-logging load-profile) customer-id)
I don't know.I don't want to know.
public interface JsonObj { String toJson(); }
public class JsonString implements JsonObj { private final String value; public JsonString(String value) { this.value = value; } public String toJson() { return "\"" + value + "\""; } }
public class JsonList implements JsonObj { private final List<? extends JsonObj> list; public JsonString(List<? extends JsonObj> list) { this.list = list; } public String toJson() { return "[" + list.stream() .map(JsonObj::toJson) .collect(joining(",")) + "]"; } }
JsonObj obj = new JsonList(asList( new JsonString("a"), new JsonList(asList( new JsonString("b"), new JsonString("c") )), new JsonString("d") )); System.out.println(obj.toJson()); // ["a",["b","c"],"d"]
Too constraining!
dispatch on the type of first argument
(defprotocol Json (to-json [this]))
(extend-type String Json (to-json [this] (str "\"" this "\"")))
(extend-type List Json (to-json [this] (str "[" (->> this (map to-json) (string/join ",")) "]")))
(extend-type nil Json (to-json [this] "null"))
(to-json ["a" ["b" "c"] nil "d"]) ;;=> ["a",["b","c"],null,"d"]
Why stop there?
dispatch on anything!
(defmulti greet :country) (defmethod greet "PL" [person] (println "Dzień dobry," (:name person))) (defmethod greet "FR" [person] (println "Bonjour," (:name person) "!")) (defmethod greet :default [person] (println "Hello," (:name person)))
(greet {:name "Jacques" :country "FR"}) ;;=> Bonjour, Jacques !
OOP ⇄ FP
(questions? "Osvaldas Grigas" @ogrigas)