Skip to content

Latest commit

 

History

History
1123 lines (968 loc) · 25.2 KB

File metadata and controls

1123 lines (968 loc) · 25.2 KB

@colorGoldenRod Architecture

with

@colorIndianRed FP

sauce


Who am I?

- declared @color[GoldenRod](developer) ;-) - enthusiastic technical @color[GoldenRod](trainer/coach) - wannabe @color[GoldenRod](entrepreneur)

doubleloop


Thanks to


Thanks to


@colorGoldenRod

@colorIndianRed


What is it?


### it's an @color[GoldenRod](architectural stereotype) ### useful to decompose a system ### in a @color[IndianRed](modular and composable) way

Alias


### @color[IndianRed](hexagonal) or ### @color[GoldenRod](port/adapter)

Onion rules

  • The application is built around an @color[GoldenRod](independent domain)
  • Direction of @color[GoldenRod](coupling is toward the center)
  • Domain code can be @color[GoldenRod](run separate from infrastructure)

Zoom in @size0.2em

Onion


Zoom in @size0.2em

Onion


Onion as a radar

Radar


Architecture Benefits

  • domain's code @color[GoldenRod](speaks loudly)
  • infrastructure's code is @colorGoldenRod
  • @colorGoldenRod different levels of abstraction
  • @colorGoldenRod cross-cutting concerns
  • @colorGoldenRod the need of system tests

@colorGoldenRod

Functional

@colorIndianRed


Functional Programming

@color[GoldenRod](compose functions) as a central
building block to write software

val toS: Int => String = 
  n => n.toString

val fromS: String => Int = 
  s => s.length

val toAndFrom: Int => Int = 
  fromS compose toS

@[1-2](from Int to String) @[4-5](from String to Int) @[7-8](compose them)


Pure Functional Programming

in this context “function” refer to the @colorIndianRed one

  • @colorGoldenRod: it must yield a value for every possible input
  • @colorGoldenRod: it must yield the same value for the same input
  • @colorGoldenRod: it’s only effect must be the computation of its return value

This is not valid

val toS : Int => String = n => {
  appendAll("log.txt", "some content")
  n.toString
}

Nor even this

val list = collection.mutable.ListBuffer[Int]()

val toS : Int => String = n => {
  list += n
  if (list.size < 42) n.toString
  else "Yo!"
}

In other words

pure FP is about @colorIndianRed or @colorGoldenRod side-effects

// pure (function w/out side-effect)
val f: A => B = ...

// effectful (function w/ controlled side-effect)
val g: A => F[B] = ...

Side-effects are a @color[IndianRed](complexity source)

  • hide inputs and outputs
  • destroy testability
  • destroy composability

Referential Transparency

An @color[GoldenRod](expression can be replaced) with
its corresponding value @color[IndianRed](without changing)
the program's behavior


Referential Transparency

it means these two programs are @colorGoldenRod

val y = foo(x)
val z = y + y
val z = foo(x) + foo(x)

Referential Transparency Benefits

functions get an @color[IndianRed](extraordinary quality) boost:


The @colorGoldenRod rat


@color[GoldenRod](birthday greetings) kata

by Matteo Vaccari
https://github.com/xpmatteo/birthday-greetings-kata

@colorIndianRed Porting

by Me ;-)
https://github.com/matteobaglini/birthday-greetings-kata-scala

Problem

@color[GoldenRod](write a program that)

  1. Loads a set of employee records from a flat file
last_name, first_name, date_of_birth, email
Doe, John, 1982/10/08, john.doe@foobar.com
Ann, Mary, 1975/09/11, mary.ann@foobar.com
  1. Sends a greetings email to all employees whose birthday is today
Subject: Happy birthday!
Happy birthday, dear {employee's first name}!

Let's see the beast

def sendGreetings(fileName: String,
                  today: XDate,
                  smtpHost: String,
                  smtpPort: Int): Unit = {
  val in = new BufferedReader(new FileReader(fileName))
  var str = ""
  str = in.readLine // skip header
  while ({ str = in.readLine; str != null }) {
    val employeeData = str.split(", ")
    val employee = Employee(employeeData(1),
                            employeeData(0),
                            employeeData(2),
                            employeeData(3))

    if (employee.isBirthday(today)) {
      val recipient = employee.email
      val body = s"Happy Birthday, dear ${employee.firstName}!"
      val subject = "Happy Birthday!"

      sendMessage(smtpHost, smtpPort,
                  "sender@here.com",
                  subject, body,
                  recipient)
    }
  }
}

@[1-4](use case entry point) @[5-8](read file content) @[8-13](parse each line) @[15](birthday check) @[16-23](send message)


System Tests

def setup(): SimpleSmtpServer = {
  SimpleSmtpServer.start(NONSTANDARD_PORT)
}

def tearDown(mailServer: SimpleSmtpServer): Unit = {
  mailServer.stop()
}

test("will send greetings when its somebody's birthday") { mailServer =>
  sendGreetings("employee_data.txt", XDate("2008/10/08"),
                "localhost", NONSTANDARD_PORT)

  assert(mailServer.getReceivedEmailSize == 1, "message not sent?")
  val message = mailServer.getReceivedEmail()
                    .next().asInstanceOf[SmtpMessage]
  assertEquals("Happy Birthday, dear John!", message.getBody)
  assertEquals("Happy Birthday!", message.getHeaderValue("Subject"))

@[1-7](setup the SMTP server) @[8-17](interact with file system and network)


Task

refactor the code one tiny step at time
until the code is @colorGoldenRod and @colorIndianRed


Let the

@colorIndianRed

begins


@colorGoldenRod List

  • split and extract responsibilities
  • push I/O at the boundaries
  • remove mutable variables
  • control side-effects (I/O ops)
  • introduce ports and adapters
  • express acceptance tests w/out infrastructure

Split @colorGoldenRod


Coupled

def sendGreetings(fileName: String,
                  today: XDate,
                  smtpHost: String,
                  smtpPort: Int): Unit = {
  // open file ...                      
  while (/* condition */) {
    // ... build employee
    // ... birthday check
    // ... send message
  }
}

Decoupled

def sendGreetings(fileName: String,
                  today: XDate,
                  smtpHost: String,
                  smtpPort: Int): Unit = {
  // open file ...                      
  while (/* condition */) {
    // ... build employee
  }
  while (/* condition */) {
    // ... birthday check
  }
  while (/* condition */) {
    // ... send message
  }
}

split loops

def sendGreetings(fileName: String // ...
  val loadedBuffer = new ListBuffer[Employee]
  val in = new BufferedReader(new FileReader(fileName))
  var str = in.readLine // skip header
  while ({ str = in.readLine; str != null }) {
    val employeeData = str.split(", ")
    val employee = Employee(employeeData(1),
                            employeeData(0),
                            employeeData(2),
                            employeeData(3))
    loadedBuffer += employee
  }
  val loaded = loadedBuffer.toList

  val birthdaysBuffer = new ListBuffer[Employee]
  for (employee <- loaded) {
    if (employee.isBirthday(today)) {
      birthdaysBuffer += employee
    }
  }
  val birthdays = birthdaysBuffer.toList

  for (employee <- birthdays) {
    val recipient = employee.email
    val body = s"Happy Birthday, dear ${employee.firstName}!"
    val subject = "Happy Birthday!"

    sendMessage(smtpHost, smtpPort,
                "sender@here.com",
                subject, body,
                recipient)
  }
}  

@[2-13](one loop to parse the file) @[2,11,13](one loop to parse the file) @[15-21](one loop to filter employees) @[15,18,21](one loop to filter employees) @[23-32](one loop to send messages) @[23](one loop to send messages)


Extract functions

def sendGreetings(fileName: String // ...
  val loaded = loadEmployees(fileName)
  val birthdays = haveBirthday(loaded, today)
  sendMessages(smtpHost, smtpPort, birthdays)
}

Current status

Status


Remove

@colorIndianRed

state & Co


Imperative style

def loadEmployees(fileName: String): List[Employee] = {
  val buffer = new ListBuffer[Employee]
  val in = new BufferedReader(new FileReader(fileName))
  var str = in.readLine // skip header
  while ({ str = in.readLine; str != null }) {
    val employeeData = str.split(", ")
    val employee = Employee(employeeData(1),
                            employeeData(0),
                            employeeData(2),
                            employeeData(3))
    buffer += employee
  }
  buffer.toList
}

@[4-5](mutable var for each line) @[2,11](mutable collection as accumulator)


Declarative style

def loadEmployees(fileName: String): List[Employee] = {
  val source = io.Source.fromFile(fileName)
  val lines = source.getLines.toList
  lines
    .drop(1) // skip header
    .map(line => {
        val employeeData = line.split(", ")
        val employee = Employee(employeeData(1),
                                employeeData(0),
                                employeeData(2),
                                employeeData(3))
        employee
    })
}

@[3](load all lines) @[4-6](parse lines) @[2-3,7-12](different level of abstractions)


Same level of abstraction

def loadEmployees(fileName: String): List[Employee] =
  loadLines(fileName)
    .drop(1) // skip header
    .map(line => parse(line))

Even more

def loadEmployees(fileName: String): List[Employee] =
  loadLines(fileName) andThen parseContent

Abstraction/Type escalation

def loadLines(fileName: String): List[String]

def loadEmployees(fileName: String): List[Employee]

Imperative style

def haveBirthday(employees: List[Employee],
                 today: XDate): List[Employee] = {
  val employees = new ListBuffer[Employee]
  for (employee <- employees) {
    if (employee.isBirthday(today)) {
      employees += employee
    }
  }
  employees.toList
}

@[4-5](filter logic)


Declarative style

def haveBirthday(employees: List[Employee],
                 today: XDate): List[Employee] =
  employees
    .filter(employee => employee.isBirthday(today))

No mutable state but...

def sendMessages(smtpHost: String,
                 smtpPort: Int,
                 employees: List[Employee]): Unit = {
  for (employee <- employees) {
    val recipient = employee.email
    val body = s"Happy Birthday, dear ${employee.firstName}!"
    val subject = "Happy Birthday!"

    sendMessage(smtpHost, smtpPort,
                "sender@here.com",
                subject, body,
                recipient)
  }
}

Inline sendMessage

def sendMessages(smtpHost: String,
                 smtpPort: Int,
                 employees: List[Employee]): Unit = {
  for (employee <- employees) {
    val recipient = employee.email
    val sender = "sender@here.com"
    val body = s"Happy Birthday, dear ${employee.firstName}!"
    val subject = "Happy Birthday!"

    val props = new Properties
    props.put("mail.smtp.host", smtpHost)
    props.put("mail.smtp.port", "" + smtpPort)
    val session = Session.getInstance(props, null)

    val msg = new MimeMessage(session)
    msg.setFrom(new InternetAddress(sender))
    msg.setRecipient(Message.RecipientType.TO, 
                     new InternetAddress(recipient))
    msg.setSubject(subject)
    msg.setText(body);

    Transport.send(msg)
  }
}

@[10-13](smtp connection) @[5-8,15-20](compose message) @[22](final send)


Extract functions

def sendMessages(smtpHost: String,
                 smtpPort: Int,
                 employees: List[Employee]): Unit = {
  for (employee <- employees) {
    val session = buildSession(smtpHost, smtpPort)
    val msg = buildMessage(session, employee)
    Transport.send(msg)
  }
}

Extract new sendMessage

def sendMessages(smtpHost: String,
                 smtpPort: Int,
                 employees: List[Employee]): Unit = {
  for (employee <- employees)
    sendMessage(smtpHost, smtpPort, employee)
}

Control @colorIndianRed


IO Monad

@colorGoldenRod the intent of I/O on invocation and
@colorGoldenRod their execution until @color[IndianRed](explicitly requested)


Like an Embedded DSL

  • @colorGoldenRod: a data structure that caputure I/O
  • @colorIndianRed: an engine that actually execute I/O

Key Points

@color[IndianRed](evidence code) that interact
with the outside world
and @color[GoldenRod](preserves referential transparency)


Which one have side-effects?

def x(/*...*/): List[Employee]

def y(/*...*/): List[Employee]

def z(/*...*/): Unit

Again, which one have side-effects?

def x(/*...*/): IO[List[Employee]]

def y(/*...*/): List[Employee]

def z(/*...*/): IO[Unit]

Real names

def loadEmployees(/*...*/): IO[List[Employee]]

def haveBirthday(/*...*/): IO[List[Employee]]

def sendMessages(/*...*/): IO[Unit]

@colorGoldenRod

@colorIndianRed REFERENTIALLY TRANSPARENT


Ask two numbers

def askInt(): Future[Int] = 
  Future(println("Please, give me a number:"))
    .flatMap(_ => Future(io.StdIn.readLine().toInt))

def askTwoInt(): Future[(Int, Int)] =
  for {
    x <- askInt()
    y <- askInt()
  } yield (x , y)

def program(): Future[Unit] =
  askTwoInt()
    .flatMap(pair => Future(println(s"Result: ${pair}")))

@[1-3](ask a number) @[5-9](ask two numbers) @[11-13](the program)


It works!


Look closely

def askTwoInt(): Future[(Int, Int)] =
  for {
    x <- askInt()
    y <- askInt()
  } yield (x , y)

First Refactoring

def askTwoInt(): Future[(Int, Int)] = {
  val sameAsk = askInt()
  for {
    x <- sameAsk
    y <- sameAsk
  } yield (x , y)
}

Oh no! @colorIndianRed


Second Refactoring

def askTwoInt(): Future[(Int, Int)] = {
  val ask1 = askInt()
  val ask2 = askInt()
  for {
    x <- ask1
    y <- ask2
  } yield (x , y)
}

WTF? Another @colorIndianRed


@color[GoldenRod](IO Monad)

REFERENTIALLY TRANSPARENT


Same program

def askInt(): IO[Int] = 
  IO(println("Please, give me a number:"))
    .flatMap(_ => IO(io.StdIn.readLine().toInt))

def askTwoInt(): IO[(Int, Int)] =
  for {
    x <- askInt()
    y <- askInt()
  } yield (x , y)

def program(): IO[Unit] =
  askTwoInt()
    .flatMap(pair => IO(println(s"Result: ${pair}")))

@[5-9](ask two numbers)


Same Two Refactorings

def askTwoInt(): IO[(Int, Int)] = {
  val sameAsk = askInt()
  for {
    x <- sameAsk
    y <- sameAsk
  } yield (x , y)
}
def askTwoInt(): IO[(Int, Int)] = {
  val ask1 = askInt()
  val ask2 = askInt()
  for {
    x <- ask1
    y <- ask2
  } yield (x , y)
}

It works!


Input operation

def loadLines(fileName: String): List[String] = {
  val source = io.Source.fromFile(fileName)
  try source.getLines.toList
  finally source.close
}

Done!

def loadLines(fileName: String): IO[List[String]] = IO {
  val source = io.Source.fromFile(fileName)
  try source.getLines.toList
  finally source.close
}

@[1](return IO[List[String]])


Compose with upper layer

def loadEmployees(fileName: String): IO[List[Employee]] =
  loadLines(fileName)
    .map(lines => parseContent(lines)) 

@[1](propagate IO) @[3](same parsing logic but wrapped in a map)


Output operation

def sendMessage(smtpHost: String,
                smtpPort: Int,
                employee: Employee): Unit = {
  val session = buildSession(smtpHost, smtpPort)
  val msg = buildMessage(session, employee)
  Transport.send(msg)
}

Done!

def sendMessage(smtpHost: String,
                smtpPort: Int,
                employee: Employee): IO[Unit] = IO {
  val session = buildSession(smtpHost, smtpPort)
  val msg = buildMessage(session, employee)
  Transport.send(msg)
}

@[3](return IO[Unit])


Execution of many I/O

def sendMessages(smtpHost: String,
                 smtpPort: Int,
                 employees: List[Employee]): Unit = {
  for (employee <- employees)
    sendMessage(smtpHost, smtpPort, employee)
}

Construction of many IO

def sendMessages(smtpHost: String,
                 smtpPort: Int,
                 employees: List[Employee]): List[IO[Unit]] = {
  employees.map { employee =>
    sendMessage(smtpHost, smtpPort, employee)
  }
}

@[4](from Employee to IO[Unit]) @[3](List[IO[Unit]] it's not what we want)


Traverse power!

def sendMessages(smtpHost: String,
                 smtpPort: Int,
                 employees: List[Employee]): IO[List[Unit]] = {
  employees.traverse { employee =>
    sendMessage(smtpHost, smtpPort, employee)
  }
}

@[4](traversal over a structure with an effect) @[3](better but List of Unit is like Unit)


Map vs Traverse


Discard results

def sendMessages(smtpHost: String,
                 smtpPort: Int,
                 employees: List[Employee]): IO[Unit] = {
  employees.traverse_ { employee =>
    sendMessage(smtpHost, smtpPort, employee)
  }
}

@[4](discard results) @3


Use case

def sendGreetings(fileName: String,
                  today: XDate,
                  smtpHost: String,
                  smtpPort: Int): Unit = {
  val loaded = loadEmployees(fileName)
  val birthdays = haveBirthday(loaded, today)
  sendMessages(smtpHost, smtpPort, birthdays)
}

Push up I/O execution

def sendGreetings(fileName: String,
                  today: XDate,
                  smtpHost: String,
                  smtpPort: Int): IO[Unit] =
  loadEmployees(fileName)
    .map(loaded => haveBirthday(loaded, today))
    .flatMap(birthdays => sendMessages(smtpHost, smtpPort, birthdays))

@[4](return IO[Unit]) @[5]("register" a loadEmployees operations) @[6](then change the content with map) @[7](then replace the content with flatMap)


With Syntactic Sugar

def sendGreetings(today: XDate)
                 (employeeRepository: EmployeeRepository, 
                  messageGateway: MessageGateway): IO[Unit] =
  for {
    loaded <- employeeRepository.loadEmployees()
    birthdays = haveBirthday(loaded, today)
    _ <- messageGateway.sendMessages(birthdays)
  } yield ()

The original Main

def main(args: Array[String]): Unit = {
  sendGreetings("employee_data.txt", XDate(), "localhost", 25)
}

The IO Monad Main

def main(args: Array[String]): Unit = {
  sendGreetings("employee_data.txt", XDate(), "localhost", 25)
    .unsafeRunSync()
}

@[3](execute I/O at "the end of the world")


Split @colorGoldenRod

From @colorIndianRed


Now

Now


Final Vision

Vision


The Onion architecture pillar


### @color[GoldenRod](Dependency Inversion Principle)

Dependency Inversion Principle

  • @color[GoldenRod](High-level modules) should not depend on low-level modules. Both should depend on abstractions.
  • @colorIndianRed should not depend on details. Details should depend on abstractions.

DIP into Onion

  • High-level modules define interfaces (@colorGoldenRod)
  • Low-level modules implement interfaces (@colorIndianRed)

entry points

  def loadEmployees(fileName: String): IO[List[Employee]]

  def sendMessages(smtpHost: String, 
                   smtpPort: Int, 
                   employees: List[Employee]): IO[Unit]

  def sendGreetings(fileName: String,
                    today: XDate, 
                    smtpHost: String, 
                    smtpPort: Int): IO[Unit]

Split parameters

def loadEmployees()
                 (fileName: String): IO[List[Employee]]

def sendMessages(employees: List[Employee])
                (smtpHost: String, 
                 smtpPort: Int): IO[Unit]                       

def sendGreetings(today: XDate)
                 (fileName: String, 
                  smtpHost: String, 
                  smtpPort: Int): IO[Unit]

@1,4,8 @2,5-6,9-11


Define the first Port

  def loadEmployees(): IO[List[Employee]]

Define the first Port (2)

trait EmployeeRepository {
  def loadEmployees(): IO[List[Employee]]
}

Implement the first Adapter

object FlatFileEmployeeRepository {

  def fromFile(fileName: String): EmployeeRepository = 
    new EmployeeRepository {

    def loadEmployees(): IO[List[Employee]] = 
        // ...same load and parse code
  }
}

@[1](the adapter module specify the technology) @[3-4](the real adapter is provided as anonymous class) @[6-12](do the file related stuff)


Define the second Port

trait MessageGateway {
  def sendMessages(employees: List[Employee]): IO[Unit]
}

Define the second Port

trait MessageGateway {

  def sendMessages(employees: List[Employee]): IO[Unit] =
    employees.traverse_(employee => sendMessage(employee))

  def sendMessage(employee: Employee): IO[Unit]
}

@[3-4](thanks to traverse we can put the implementation here) @[6](and open sendMessage)


Implement the second Adapter

object SmtpMessageGateway {

  def fromEndpoint(smtpHost: String, smtpPort: Int): MessageGateway =
    new MessageGateway {

      def sendMessage(employee: Employee): IO[Unit] =
        // ...same buildSession and buildMessage code
    }
}

@[1](the adapter module specify the technology) @[3-4](the real adapter is provided as anonymous class) @[6-12](do the smtp related stuff)


Request Ports

def sendGreetings(today: XDate)
                 (employeeRepository: EmployeeRepository,
                  messageGateway: MessageGateway): IO[Unit]

@[2-3](no more file and smtp parameters)


Provide Adapters

def main(args: Array[String]): Unit = {
  val employeeRepository = 
        FlatFileEmployeeRepository.fromFile("employee_data.txt")
  val messageGateway = 
        SmtpMessageGateway.fromEndpoint("localhost", 25)

  sendGreetings(XDate())
    (employeeRepository, messageGateway)
    .unsafeRunSync()
}

@[2-5](build the concrete adapters) @[8](inject into sendGreetings)


@color[GoldenRod](Acceptance tests) w/out @colorIndianRed


Test adapters

class FakeEmployeeRepository(employees: List[Employee]) 
    extends EmployeeRepository {

  def loadEmployees(): IO[List[Employee]] = IO {
    employees
  }
}

class FakeMessageGateway 
    extends MessageGateway {

  val receivers = new collection.mutable.ListBuffer[Employee]

  def sendMessage(e: Employee): IO[Unit] = IO {
    receivers += e
  }
}

@[1-6](fake repository) @[9-16](fake gateway)


Infrastructure free acceptance tests

 test("will send greetings when its somebody's birthday") {
   val john = Employee("John", "Doe", 
                       "1982/10/08", "john.doe@foobar.com")
   val mary = Employee("Mary", "Ann", 
                       "1975/03/11", "mary.ann@foobar.com")
   val fakeEmployeeRepository = 
            new FakeEmployeeRepository(List(john, mary))
   val fakeMessageGateway = new FakeMessageGateway()

   sendGreetings(XDate("2008/10/08"))
     (fakeEmployeeRepository, fakeMessageGateway)
     .unsafeRunSync()

   assert(fakeMessageGateway.receivers.size == 1)
   assert(fakeMessageGateway.receivers.contains(john))
 }

@[2-7](setup fake repository) @[8](setup fake gateway) @[10-12](execute usecase) @[14-15](simple asserts)


Closing notes


Next steps

  • abstract over effect type
  • make invalid state unrepresentable
  • remove mutable state from fakes
  • use streaming I/O (from IO[List[A]] to Stream[IO, A])

Play with requirements

  • handle error and print graceful messages
  • remove dependency from configuration parameters
  • different message contents (Mr/Mrs)
  • different infrastructures (SMTP and/or SMS)

Conclusion

  • Onion rules plus radar technique to @color[GoldenRod](distribute and encapsulate responsibilities)
  • Pure Functional Programming technique to @color[IndianRed](compose behaviours and layers)

@colorGoldenRod