Best Practices
## Learning Objectives
- Learn idiomatic Scala patterns
- Master functional programming patterns
- Understand Java interoperability
- Write maintainable Scala code
## Style Guidelines
### Naming Conventions
```scala
// Classes, traits: PascalCase
class UserAccount
trait Comparable
// Objects, packages: camelCase
object UserService
package com.example.myapp
// Methods, functions: camelCase
def calculateTotal()
def processPayment()
// Constants: PascalCase
val MaxRetryCount = 3
val DefaultTimeout = 5000
// Type parameters: single letter (usually)
class List[T]
class Map[K, V]
```
### Indentation and Formatting
```scala
// Use 2 spaces (Scala style)
// Good
def calculateTotal(items: List[Item]): BigDecimal = {
items.fold(BigDecimal(0)) {
case (sum, item) => sum + item.price
}
}
// Avoid
def calculateTotal(items: List[Item]): BigDecimal = {
items.fold(BigDecimal(0)) {
case (sum, item) => sum + item.price
}
}
```
## Immutability
### Prefer val over var
```scala
// Good
val result = List(1, 2, 3)
.map(_ * 2)
.filter(_ > 3)
// Avoid
var result = List(1, 2, 3)
result = result.map(_ * 2)
result = result.filter(_ > 3)
```
### Use Immutable Collections
```scala
// Good
val list = List(1, 2, 3)
val newList = list :+ 4
// Mutable when needed
import scala.collection.mutable
val buffer = mutable.ListBuffer(1, 2, 3)
buffer += 4
```
### Copy for Modification
```scala
case class Person(name: String, age: Int)
// Good - returns new instance
val aliceOlder = alice.copy(age = alice.age + 1)
// Avoid - mutates
val person = new Person("Alice", 30)
person.age = 31
```
## Null Handling
### Avoid null
```scala
// Good - Option instead of null
def findUser(id: String): Option[User] = ???
findUser("123") match {
case Some(user) => println(user)
case None => println("Not found")
}
// Avoid
def findUser(id: String): User = null
```
### Converting null to Option
```scala
val javaValue: String = null // From Java interop
val option: Option[String] = Option(javaValue) // None if null
```
## Error Handling
### Use Appropriate Types
```scala
// For optional values
def findById(id: String): Option[User] = ???
// For operations that can fail
def parseNumber(s: String): Either[String, Int] = ???
// For operations that throw
def readFile(path: String): Try[String] = Try(...)
```
### Fail Fast
```scala
// Good - validate early
def createUser(name: String, age: Int): User = {
require(name.nonEmpty, "Name cannot be empty")
require(age >= 0, "Age cannot be negative")
new User(name, age)
}
// Avoid - fail late
def createUser(name: String, age: Int): User = {
val user = new User(name, age)
if (name.isEmpty) throw new IllegalArgumentException
if (age < 0) throw new IllegalArgumentException
user
}
```
## Functional Patterns
### Chain Operations
```scala
// Good - chain transformations
val result = users
.filter(_.age >= 18)
.map(_.name)
.sorted
.take(10)
// Nested (avoid)
val result = {
val filtered = users.filter(_.age >= 18)
val mapped = filtered.map(_.name)
val sorted = mapped.sorted
sorted.take(10)
}
```
### Pattern Matching
```scala
// Good - comprehensive
sealed trait Result
case class Success(data: String) extends Result
case class Failure(message: String) extends Result
def describe(result: Result): String = result match {
case Success(data) => s"Success: $data"
case Failure(msg) => s"Failed: $msg"
}
// Avoid - unchecked
def describe(result: Result): String = result match {
case Success(data) => s"Success: $data"
}
```
### Partial Functions
```scala
// Good - handle specific cases
val pf: PartialFunction[Status, String] = {
case Ready => "Ready"
case Running => "Running"
case Done => "Completed"
}
status.collect(pf) // Only applies where defined
// Or with for
for {
s @ (Ready | Running | Done) <- statuses
} yield describe(s)
```
## Classes and OOP
### Use Case Classes for Data
```scala
// Good
case class Person(name: String, email: Option[String] = None)
// Avoid - too much boilerplate
class Person(val name: String, val email: Option[String] = None) {
override def equals(o: Any) = ???
override def hashCode = ???
override def toString = ???
}
```
### Dependency Injection
```scala
// Constructor injection (preferred)
class UserService(userRepository: UserRepository,
emailService: EmailService) {
def createUser(name: String): User = {
val user = userRepository.save(User(name))
emailService.sendWelcome(user)
user
}
}
// Avoid - no dependencies visible
class UserService {
val repository = new UserRepository()
val emailService = new EmailService()
}
```
## Performance
### Avoid Memory Leaks
```scala
// Bad - captures reference
def process(): Unit = {
val data = loadData()
future.onComplete { result =>
println(data) // Captures 'data' reference
}
}
```
### Use Views for Large Collections
```scala
// Good - lazy
val result = (1 to 1000000)
.view
.map(_ * 2)
.filter(_ > 100)
.take(10)
.toList
// Avoid - creates intermediate collections
val result = (1 to 1000000)
.map(_ * 2) // Creates 1M element collection
.filter(_ > 100)
.take(10)
.toList
```
## Java Interoperability
### Converting Collections
```scala
import scala.collection.JavaConverters._
// Scala to Java
val scalaList = List(1, 2, 3)
val javaList: java.util.List[Int] = scalaList.asJava
// Java to Scala
val javaSet = new java.util.HashSet[String]()
val scalaSet: Set[String] = javaSet.asScala.toSet
```
### Using Java Classes
```scala
import java.util.{ArrayList, HashMap}
import scala.collection.mutable._
// Java ArrayList to Scala mutable Buffer
val javaArrayList = new ArrayList[String]()
val buffer: mutable.Buffer[String] = javaArrayList.asScala
```
### Scala to Java Conversions
```scala
import scala.jdk.CollectionConverters._
// Scala 2.13+ uses java.nio files
import scala.jdk.javaapi.CollectionConverters.asJava
// Using Java streams (Scala 2.12+)
import scala.collection.JavaConverters._
val javaStream = scalaIterable.asJava.stream()
```
### Annotations
```scala
// Java annotations work in Scala
class MyClass {
@Deprecated
def oldMethod(): Unit = ???
@throws(classOf[IOException])
def readFile(): String = ???
@varargs
def process(args: String*): Unit = ???
}
```
## Testing
### Use ScalaTest
```scala
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatest.matchers.should.Matchers
class CalculatorSpec extends AnyFlatSpec with Matchers {
"Calculator" should "add numbers" in {
val calc = new Calculator
calc.add(2, 3) should be(5)
}
it should "throw on division by zero" in {
val calc = new Calculator
an [ArithmeticException] should be thrownBy calc.divide(1, 0)
}
}
```
### Property-Based Testing
```scala
import org.scalatest.propspec.AnyPropSpec
import org.scalatest.prop.TableDrivenPropertyChecks._
class ListSpec extends AnyPropSpec {
property("list concatenation is associative") {
forAll { (a: List[Int], b: List[Int], c: List[Int]) =>
(a ::: b) ::: c should be(a ::: (b ::: c))
}
}
}
```
## Summary
- Follow Scala naming conventions and formatting
- Prefer val over var, immutable over mutable
- Use Option, Try, Either for error handling
- Chain operations for clean functional code
- Use case classes for data, sealed traits for hierarchies
- Use views for large or infinite collections
- Use JavaConverters for Java/Scala interop
- Write tests using ScalaTest
- Prefer composition over inheritance
- Keep functions small and focused
Comments
Comments powered by Giscus
To enable comments, add your Giscus embed code here.
Learn more about Giscus →