High Quality with Scala from Day One

Mateusz Kubuszok > Scalac

Agenda

  • project's setup
  • clean code
  • teamwork and collaboration

Setting up project


// build.sbt

name         := "my-project"
scalaVersion := "2.11.8"
            

Power of modules

Examples of usage:

  • layered architecture
  • hexagonal architecture
  • separating boundary contexts

// build.sbt

lazy val root   = project.in(file("."))
                         .aggregate(app, model, common)

lazy val app    = project.in(file("modules/app"))
                         .dependsOn(model)

lazy val model  = project.in(file("modules/model"))
                         .dependsOn(common)

lazy val common = project.in(file("modules/common"))
            
// project/Dependencies.scala
import sbt._
import sbt.Keys._

trait Dependencies {

  val commonResolvers = Seq(
    Resolver sonatypeRepo "public",
    Resolver typesafeRepo "releases"
  )

  val commonLibraries = Seq(
    "org.scalaz" %% "scalaz-core" % "7.1.3")

  val testLibraries = Seq(
    "org.mockito" % "mockito-core" % "1.10.8")
}
// project/Settings.scala
import sbt._
import sbt.Keys._

trait Settings { self: Dependencies =>

  val settings = Seq(
    organization := "com.example",
    version      := "0.1.0-SNAPSHOT",

    scalaVersion := "2.11.7",
    scalaOptions ++= Seq(...),

    resolvers           ++= commonResolvers,
    libraryDependencies ++= commonLibraries,
    libraryDependencies ++= testLibraries map (_ % "test"))
}
// project/Common.scala
object Common with Settings with Dependencies
            
// build.sbt

lazy val root = project.in(file("."))
                         .aggregate(app, model, common)

lazy val app  = project.in(file("modules/app"))
                       .settings(name := "my-project")
                       .settings(Common.settings: _*)
                       .dependsOn(model)

...

Automatic quality checking

For starters: unit testing.

class UtilsSpec extends Specification {

  "Utils.convertToX" should {

    "be a positive number" in {
      // given
      val y = ...

      // when
      val x = UtilsSpec.convertToX(y)

      // then
      x must beGreaterThan(0)
    }
  }
}
> sbt test        # test all modules
> sbt common:test # test only common

Now let's check coverage.

// project/plugins.sbt

addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.3.3")
// build.sbt
import scoverage.ScoverageSbtPlugin

...

lazy val app = project.in(file("modules/app"))
                      .settings(name := "my-project")
                      .settings(Common.settings: _*)
                      .dependsOn(model)
                      .enablePlugins(ScoverageSbtPlugin)
> sbt clean coverage test                   # single module project
> sbt clean coverage test coverageAggregate # multi module project

Finally, let's make sure that code follows some style guidelines.

// project/plugins.sbt

addSbtPlugin("org.scalariform" % "sbt-scalariform" % "1.5.1")

addSbtPlugin("org.scalastyle" %% "scalastyle-sbt-plugin" % "0.7.0")
// project/Settings.scala
import com.typesafe.sbt.SbtScalariform._
import scalariform.formatter.preferences._
import org.scalastyle.sbt.ScalastylePlugin._

trait Settings { self: Dependencies =>

  val settings = Seq(
    ...,

    ScalariformKeys.preferences := ScalariformKeys.preferences.value
      .setPreference(DoubleIndentClassDeclaration, true)
      .setPreference(IndentLocalDefs, false)

    scalastyleFailOnError := true
  )
}
> sbt compile                  # scalariform run on each compilation
> sbt scalastyleGenerateConfig # generate scalastyle-config.xml file
> sbt scalastyle               # can be configured to fail on error

Clean Scala code

Avoid Java in Scala

// usually you don't want returns like this one...
def isThereADigit1(source: String): Boolean = {
  for (c <- source) {
    if (c.isDigit)
      return true
  }
  return false
}

// ...you probably wanted some predicate instead
def isThereADigit2(source: String) = source.exists(_.isDigit)
// you probably didn't want a return here as well...
def findANumber1(source: String): Long = {
  source split "," foreach { s =>
    if (s.matches("-?[0-9]+"))
      return s.toLong
  }
  return 0
}

// ...you most likely needed to "find" first occurrence
def findANumber2(source: String): Option[Long] =
  source split "," find (_.matches("-?[0-9]+")) map (_.toLong)
// what about aggregation?
var sum = 0
for (i <- 0 until 50) {
  sum += i
}
sum

// well, numbers are quite easy to add up
(0 until 50).fold(0)(_ + _)
(0 until 50).reduce(_ + _)
(0 until 50).sum
// numbers are also easy to multiply...
var product = 1
for (i <- 1 until 7) {
  product *= i
}
product

// ...without iterating manually
(1 until 7).fold(1)(_ * _)
(1 until 7).reduce(_ * _)
(1 until 7).product
// let's think about exceptional cases

def extractNumbers1a(source: String) = try {
  source split "," map (_.toLong) toSeq
} catch {
  case _: NumberFormatException => Seq()
}

def extractNumbers2a(source: String) = source split "," flatMap { s =>
  try {
    Some(s.toLong)
  } catch {
    case _: NumberFormatException => None
  }
} toSeq
// try/catch -> Try is a small improvement...

def extractNumbers1b(source: String) =
  Try(source split "," map (_.toLong) toSeq) getOrElse Seq.empty

def extractNumbers2b(source: String) = source split "," flatMap { s =>
  Try(s.toLong).toOption
} toSeq
// ...but expressing posibility of a failure directly is even better

def extractNumbers1c(source: String) =
  Try(source split "," map (_.toLong) toSeq)

def extractNumbers2c(source: String) = {
  val (right, left) = source split "," partition { s =>
    Try(s.toLong).isSuccess
  }

  if (left.isEmpty) Right(right map (_.toLong) toSeq)
  else Left(left.toSeq)
}
  • don't iterate unnecessarily with breaks and returns - remember about .folds, .reduce, .exists, .forall, .find, .partition...
  • don't throw exceptions - instead use Either, Validators, \/...
  • don't use mutable state when possible - instead use immutable state and pass it around

Avoid abusing Scala features

trait MyService { ... }

trait MyServiceComponent {

  def myService: MyService
}

trait MyServiceComponentImpl { self: MyOtherServiceComponent =>

  object myService extends MyService { ... }
}
trait MyOtherServiceComponentMock extends Mockito {

  val myService = mock[MyService]
}

val testComponent = new MyServiceComponentImpl
   with MyOtherServiceComponentMock
   {}
class ComponentRegistry
    extends MyServiceComponentImpl
    with MyOtherServiceComponentImpl
trait CommonComponentRegistryImpl extends CommonComponentRegistry
    with MyServiceComponentImpl
    with MyOtherServiceComponentImpl
    ...

trait ModelComponentRegistryImpl extends ModelComponentRegistry
    with UserRepositoryComponentImpl
    with AddressRepositoryComponentImpl
    ...
    { self: CommonComponentRegistry
       with DBDriverComponent
       ...
}
trait CommonComponentRegistryImpl extends CommonComponentRegistry
    with MyServiceComponentImpl
    with MyOtherServiceComponentImpl
    ...

trait ModelComponentRegistryImpl extends ModelComponentRegistry
    with UserRepositoryComponentImpl
    with AddressRepositoryComponentImpl
    ...
    { self: CommonComponentRegistry
       with DBDriverComponent
       ...
}
class UserRecordsRepository(...) {

  def fetch(id: Long): Option[(Long, String, Long, Date)]

  def save(record: (String, Long, Date)): (Long, String, Long, Date)

  def update(record: (String, Long, Date)): Boolean
}

case class User(name: String, addressId: Long, lastUpdate: Date)

case class UserRecord(
    id: Option[Long], name: String, addressId: Long, lastUpdate: Date)

class UserRecordsRepository(...) {

  def fetch(id: Long): Option[UserRecord]

  def save(record: User): UserRecord

  def update(record: User): Boolean
}
// we love debugging those
def prepareMailSubscriptionsForFriends(userCollection: Seq[User]) =
  userCollection
    .flatMap(getFriendsAddresses)
    .filter(onlyContactAddresses)
    .map(enrichAddressesWithDetails)
    .filter(onlySupportedLocations)
    ... //
// we love debugging those even more
def createFriendsActivity: JSonValue = for {
  currentUserId <- context.currentUserId.toSeq
  user          <- userRepository.find(currentUserId)
  address       <- addressRepository.find(user.addressId)
  friends       =  userRepository.findFriendsOf(user.id)
  friend        <- friends if newerThanYesterday(friend.lastVisited)
  ... // 30 more lines of for comprehension
} yield JSon.obj(
  "user" -> JSon.obj(...),
  "address" -> JSon.obj(...),
  ...
)
// if only there was a way to handle it at compile time...
def handleValue(value: Any) = value match {
  case l: Long => ...
  case s: String => ...
  case _ => throw UnsupportedOperationException("type not supported")
}

// ...with type safety and everything
def handleValue(value: Long) = ...
def handleValue(value: String) = ...

Try to avoid:

  • cake pattern
  • using tuples in public API, consider case classes instead
  • extremely long monad chains, both for-comprehension and plain map/flatMap/filter
  • using pattern matching on distinct types - things that could be checked at compile time, should not be done in runtime

Keeping other people in mind

Communicating via code

  • deciding on conventions
  • using descriptive, meaningful names
  • using types to inform how API should be used
  • commenting concepts that cannot be expressed with code

Communicating outside the code

  • thinking about your users and contributors
  • providing README with basic information
  • documenting everything that could be important to user, in a way he could find it fast
  • being responsible

Summary

  • use possibilities that the build tool gives you
  • avoid Java in Scala
  • less is more
  • think about people that will be using the project
  • think about people that will be contributing to it

Questions?

Thank you!