Without further ado, here is the first one.
In OO polymorphism is achieved by inheritance. Inheritance also enables sharing data and logic sparing the developer some code duplication. It is a powerful tool, but nowadays generally regarded overused. The Template Method Design Pattern is a nice example and I've implemented it dozens (if not hundreds) of times at work. Let's see how to solve the problems it solves in languages that don't have inheritance.
The idea is to devise a toy problem, solve it in Scala (it would be the same in Java, I just want keep the boilerplate down) to demonstrate the OO-way, then come up with Clojure and Go solutions.
The Problem
Our domain has employees who can fall into 2 broad categories, Office Workers and Field Workers. Their base salary and the calculation of how their years at the company contribute to their salary are different. I let the code speak for itself.
Scala/OO solution
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
sealed abstract class Employee(val name: String, years: Int, baseSalary: BigDecimal) { | |
//abstract method | |
def yearsContribution(): BigDecimal | |
//do the same logic with different data and sub-logic in each subclass | |
def salary(): BigDecimal = baseSalary + yearsContribution() | |
override def toString: String = s"My name is $name and I've been working here for $years years." | |
} | |
class OfficeWorker(name: String, years: Int) | |
extends Employee(name, years, baseSalary = BigDecimal(1000)) { | |
override def yearsContribution(): BigDecimal = years * 30 | |
} | |
//extra fields | |
class FieldWorker(name: String, years: Int, physicalWork: Boolean) | |
extends Employee(name, years, baseSalary = BigDecimal(800)) { | |
override def yearsContribution(): BigDecimal = | |
years * (if (physicalWork) 40 else 30) | |
} | |
object EmloyeeTestRunner extends App { | |
val o = new OfficeWorker("joe", 12) | |
val f = new FieldWorker("jack", 8, true) | |
Seq[Employee](o, f).foreach { e ⇒ | |
println(s"$e. I earn ${e.salary()}") | |
} | |
} |
Clojure solution
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns example | |
(:require [clojure.spec :as s])) | |
;; Spec section. It's not needed for the solution, but provides | |
;; documentation and can generate tests. | |
(def employee-types #{:office-worker :field-agent}) | |
(s/def ::type employee-types) | |
(s/def ::years (s/int-in 0 100)) | |
(s/def ::name string?) | |
(s/def ::base-salary (s/int-in 0 1000)) | |
(s/def ::physical-work? boolean?) | |
(s/def ::employee (s/keys :req [::years ::name ::base-salary ::type] | |
:opt [::physical-work?])) | |
(s/fdef salary | |
:args (s/cat :in ::employee) | |
:ret number?) | |
;; ============================================================ | |
(defmulti years-contribution ::type) | |
(defmethod years-contribution :office-worker [x] | |
(* 30 (::years x))) | |
(defmethod years-contribution :field-agent [x] | |
(let [multiplier (if (::physical-work? x) 40 30)] | |
(* multiplier (::years x)))) | |
(defn to-string [employee] | |
(str "My name is " (::name employee) " and I've been working here for " (::years employee) " years.")) | |
(defn office-worker [name years] | |
{::name name ::years years ::base-salary 800 ::type :office-worker}) | |
(defn field-agent [name years physical-work?] | |
{::name name ::years years ::base-salary 1000 | |
::physical-work? physical-work? ::type :field-agent}) | |
(defn salary [employee] | |
(+ (::base-salary employee) (years-contribution employee))) | |
;;=============== demonstrate ======================= | |
(let [joe (field-agent "joe" 17 false) | |
jil (field-agent "jil" 17 true) | |
jack (office-worker "jack" 20)] | |
(doseq [x [joe jil jack]] | |
(println (to-string x)) | |
(println (salary x)))) |
No inheritance, obviously and no types either. Therefore the toString and salary functions are not defined on types, but the entity maps are passed as arguments. Another solution could have been using protocols and defrecords, that would have yielded a more OO-like solution. However for a single function it seemed to be an overkill.
Go solution
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import "fmt" | |
type ( | |
employee interface { | |
yearContribution() float32 | |
baseSalary() int | |
toString() string | |
} | |
baseEmployee struct { | |
name string | |
years int | |
} | |
officeWorker struct { | |
baseEmployee | |
} | |
fieldWorker struct { | |
baseEmployee | |
physicalWork bool | |
} | |
) | |
func (_ officeWorker) baseSalary() int { | |
return 500 | |
} | |
func (_ fieldWorker) baseSalary() int { | |
return 1000 | |
} | |
func (o officeWorker) yearContribution() float32 { | |
return 30 * float32(o.years) | |
} | |
func (f fieldWorker) yearContribution() float32 { | |
if f.physicalWork { | |
return 40 * float32(f.years) | |
} else { | |
return 30 * float32(f.years) | |
} | |
} | |
func salary(e employee) float32 { | |
return float32(e.baseSalary()) + e.yearContribution() | |
} | |
func (e baseEmployee) toString() string { | |
return fmt.Sprintf("My name is %s and I've been working here for %d years.", e.name, e.years) | |
} | |
func main() { | |
ow := officeWorker{baseEmployee{name:"joe", years:20}} | |
fw := fieldWorker{baseEmployee: baseEmployee{name:"jack", years:20}, physicalWork: true} | |
for _, e := range []employee{ow, fw} { | |
fmt.Println(e.toString()) | |
fmt.Println(salary(e)) | |
} | |
} |
It took me some time to come up with a solution. I Go there are no abstract classes or virtual methods. This required me to define to a separate employee interface and a baseEmployee struct. And I couldn't attach the salary method to the interface either, even though it has both the yearContribution() and the baseSalary() methods on it. It is not necessarily a problem, this could be idiomatic in Go.
No comments :
Post a Comment