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)