Visibility and Access Control: Securing Your Code

Introduction

Proper access control is fundamental to writing secure, maintainable code. Scala provides sophisticated visibility modifiers that go beyond simple public/private access, allowing you to create clean APIs, enforce encapsulation, and control how your code is used by others. Understanding these mechanisms is crucial for building robust applications and libraries.

This lesson will teach you how to use Scala's access modifiers effectively, design clean public interfaces, protect internal implementation details, and create secure, well-encapsulated code structures.

Access Modifier Fundamentals

Basic Access Levels

package com.company.banking

class BankAccount(private val accountNumber: String) {
  // Private - only accessible within this instance
  private var _balance: BigDecimal = BigDecimal(0)
  private val transactionHistory: scala.collection.mutable.ListBuffer[String] = 
    scala.collection.mutable.ListBuffer.empty

  // Protected - accessible in subclasses
  protected def logTransaction(description: String): Unit = {
    val timestamp = java.time.LocalDateTime.now()
    transactionHistory += s"[$timestamp] $description"
  }

  // Package-private - accessible within the banking package
  private[banking] def getInternalId(): String = s"INTERNAL_${accountNumber}"

  // Public - accessible from anywhere (default visibility)
  def balance: BigDecimal = _balance

  def deposit(amount: BigDecimal): Either[String, BigDecimal] = {
    if (amount <= 0) {
      Left("Deposit amount must be positive")
    } else {
      _balance += amount
      logTransaction(s"Deposit: +$amount")
      Right(_balance)
    }
  }

  def withdraw(amount: BigDecimal): Either[String, BigDecimal] = {
    if (amount <= 0) {
      Left("Withdrawal amount must be positive")
    } else if (amount > _balance) {
      Left("Insufficient funds")
    } else {
      _balance -= amount
      logTransaction(s"Withdrawal: -$amount")
      Right(_balance)
    }
  }

  // Public method that uses private data
  def getStatement(): String = {
    val header = s"Account: $accountNumber\nBalance: $balance\n${"-" * 30}\n"
    val transactions = transactionHistory.takeRight(10).mkString("\n")
    header + transactions
  }
}

// Subclass demonstrating protected access
class SavingsAccount(accountNumber: String, val interestRate: Double) 
  extends BankAccount(accountNumber) {

  def applyInterest(): BigDecimal = {
    val interest = balance * BigDecimal(interestRate / 100)
    if (interest > 0) {
      val _ = deposit(interest)  // This will work - deposit is public
      logTransaction(s"Interest applied: +$interest at ${interestRate}%")  // Protected access
    }
    balance
  }

  // Cannot access private members
  // def getAccountNumber(): String = accountNumber  // This would not compile
  // def getBalance(): BigDecimal = _balance  // This would not compile
}

// Another class in the same package
class BankingSystem {
  private val accounts: scala.collection.mutable.Map[String, BankAccount] = 
    scala.collection.mutable.Map.empty

  def createAccount(accountNumber: String): BankAccount = {
    val account = new BankAccount(accountNumber)
    accounts(accountNumber) = account
    account
  }

  def getAccountDetails(accountNumber: String): Option[String] = {
    accounts.get(accountNumber).map { account =>
      // Can access package-private method
      s"Account Details - Internal ID: ${account.getInternalId()}, Balance: ${account.balance}"
    }
  }
}

// Usage
val account = new BankAccount("ACC12345")
account.deposit(BigDecimal("1000"))
account.withdraw(BigDecimal("250"))
println(account.getStatement())

val savings = new SavingsAccount("SAV67890", 2.5)
savings.deposit(BigDecimal("5000"))
savings.applyInterest()
println(s"Savings balance after interest: ${savings.balance}")

Qualified Private Access

package com.company.security

// Outer scope for qualified private access
package object security {
  // Package-level utility
  private[security] def generateSecretKey(): String = {
    java.util.UUID.randomUUID().toString.replace("-", "")
  }
}

class CryptoManager {
  // Private to the security package
  private[security] val masterKey: String = generateSecretKey()

  // Private to the company scope
  private[company] val systemSalt: String = "COMPANY_SALT_2023"

  // Private to this class only
  private val internalConfig: Map[String, String] = Map(
    "algorithm" -> "AES-256",
    "mode" -> "GCM",
    "padding" -> "NoPadding"
  )

  // Public API
  def encrypt(data: String, userKey: String): String = {
    val key = combineKeys(userKey)
    performEncryption(data, key)
  }

  def decrypt(encryptedData: String, userKey: String): String = {
    val key = combineKeys(userKey)
    performDecryption(encryptedData, key)
  }

  // Package-private helper methods
  private[security] def combineKeys(userKey: String): String = {
    s"${masterKey}_${userKey}_${systemSalt}".hashCode.toHexString
  }

  // Private implementation details
  private def performEncryption(data: String, key: String): String = {
    // Simplified encryption for example
    val algorithm = internalConfig("algorithm")
    s"ENCRYPTED[$algorithm]:${data.reverse}:$key"
  }

  private def performDecryption(encryptedData: String, key: String): String = {
    // Simplified decryption for example
    val pattern = """ENCRYPTED\[([^\]]+)\]:([^:]+):(.+)""".r
    encryptedData match {
      case pattern(algorithm, reversedData, encKey) if encKey == key =>
        reversedData.reverse
      case _ => throw new IllegalArgumentException("Invalid encrypted data or key")
    }
  }
}

class SecurityAuditor {
  // Can access package-private members
  def auditCryptoManager(manager: CryptoManager): String = {
    s"Master Key Present: ${manager.masterKey.nonEmpty}, " +
    s"System Salt: ${manager.systemSalt}"
  }

  def generateAuditKey(): String = {
    generateSecretKey()  // Package-private function
  }
}

// Different package scope
package com.company.admin {
  import com.company.security.CryptoManager

  class SystemAdmin {
    private val crypto = new CryptoManager()

    def getSystemInfo(): String = {
      // Can access company-level private members
      s"System Salt: ${crypto.systemSalt}"

      // Cannot access security package private members
      // val key = crypto.masterKey  // This would not compile
    }
  }
}

// Usage demonstrating access levels
val crypto = new CryptoManager()
val auditor = new SecurityAuditor()

val plaintext = "Sensitive Data"
val userKey = "user123"

val encrypted = crypto.encrypt(plaintext, userKey)
println(s"Encrypted: $encrypted")

val decrypted = crypto.decrypt(encrypted, userKey)
println(s"Decrypted: $decrypted")

println(auditor.auditCryptoManager(crypto))

Protected Access and Inheritance

Protected Members in Class Hierarchies

abstract class Vehicle(protected val vin: String) {
  // Protected fields accessible in subclasses
  protected var _mileage: Double = 0.0
  protected val _manufactureDate: java.time.LocalDate = java.time.LocalDate.now()

  // Protected methods
  protected def updateMileage(miles: Double): Unit = {
    require(miles >= _mileage, "Mileage cannot decrease")
    _mileage = miles
  }

  protected def validateVin(vin: String): Boolean = {
    vin.length == 17 && vin.matches("[A-HJ-NPR-Z0-9]+")
  }

  // Public interface
  def mileage: Double = _mileage
  def age: java.time.Period = java.time.Period.between(_manufactureDate, java.time.LocalDate.now())

  // Abstract methods for subclasses
  def fuelType: String
  def maxSpeed: Int
}

class Car(vin: String, val make: String, val model: String) extends Vehicle(vin) {
  require(validateVin(vin), "Invalid VIN")  // Using protected method

  // Private to Car class
  private val _features: scala.collection.mutable.Set[String] = 
    scala.collection.mutable.Set.empty

  // Protected method accessible to Car subclasses
  protected def addFeature(feature: String): Unit = {
    _features += feature
  }

  // Public methods
  def fuelType: String = "Gasoline"
  def maxSpeed: Int = 120  // mph

  def addMiles(miles: Double): Unit = {
    updateMileage(_mileage + miles)  // Using protected method
  }

  def features: Set[String] = _features.toSet

  def getVehicleInfo(): String = {
    s"$make $model (VIN: $vin), Mileage: ${_mileage}, Age: ${age.getYears} years"
  }
}

class ElectricCar(vin: String, make: String, model: String, val batteryCapacity: Double) 
  extends Car(vin, make, model) {

  // Electric car specific features
  private var _batteryLevel: Double = 100.0

  // Can access protected members from parent classes
  def initializeElectricFeatures(): Unit = {
    addFeature("Electric Motor")      // Protected from Car
    addFeature("Regenerative Braking")
    addFeature("Silent Operation")

    // Can access protected field from Vehicle
    println(s"Initializing electric features for VIN: $vin")
  }

  override def fuelType: String = "Electric"
  override def maxSpeed: Int = 150  // Electric cars can be faster

  def batteryLevel: Double = _batteryLevel

  def charge(percentage: Double): Unit = {
    require(percentage >= 0 && percentage <= 100, "Invalid battery percentage")
    _batteryLevel = percentage

    // Record charging as "mileage event"
    if (percentage > 90) {
      // Can access protected method to log significant events
      updateMileage(_mileage)  // Just to demonstrate access
    }
  }
}

// Motorcycle in the same hierarchy
class Motorcycle(vin: String, val brand: String, val engineSize: Int) extends Vehicle(vin) {
  require(validateVin(vin), "Invalid VIN")  // Using protected validation

  def fuelType: String = "Gasoline"
  def maxSpeed: Int = 180  // Motorcycles can be fast

  def performMaintenance(): Unit = {
    // Access protected mileage for maintenance scheduling
    if (_mileage > 10000) {
      println(s"$brand motorcycle needs major service at ${_mileage} miles")
    }
  }

  def ride(miles: Double): Unit = {
    updateMileage(_mileage + miles)  // Using protected method
  }
}

// Cannot access protected members from outside the hierarchy
class VehicleInspector {
  def inspect(vehicle: Vehicle): String = {
    // Can only access public members
    s"Vehicle mileage: ${vehicle.mileage}, Age: ${vehicle.age.getYears} years"

    // Cannot access protected members
    // vehicle._mileage  // This would not compile
    // vehicle.updateMileage(1000)  // This would not compile
  }
}

// Usage
val car = new Car("1HGCM82633A123456", "Honda", "Accord")
car.addMiles(15000)
println(car.getVehicleInfo())

val electricCar = new ElectricCar("5YJ3E1EA4JF123456", "Tesla", "Model 3", 75.0)
electricCar.initializeElectricFeatures()
electricCar.addMiles(5000)
electricCar.charge(85.0)
println(s"Electric car features: ${electricCar.features}")
println(s"Battery level: ${electricCar.batteryLevel}%")

val motorcycle = new Motorcycle("JH2RC5009GM123456", "Honda", 600)
motorcycle.ride(12000)
motorcycle.performMaintenance()

val inspector = new VehicleInspector()
println(inspector.inspect(car))
println(inspector.inspect(electricCar))
println(inspector.inspect(motorcycle))

API Design with Access Control

Creating Clean Public Interfaces

package com.company.collections

// Public trait defining the contract
trait SafeCollection[T] {
  def add(item: T): Boolean
  def remove(item: T): Boolean
  def contains(item: T): Boolean
  def size: Int
  def isEmpty: Boolean
  def toList: List[T]
}

// Internal implementation details hidden through access control
class ThreadSafeSet[T] private(
  private val maxSize: Int,
  private val allowDuplicates: Boolean
) extends SafeCollection[T] {

  // Private internal state
  private val items: scala.collection.mutable.Set[T] = 
    scala.collection.concurrent.TrieMap.empty[T, Unit].keySet
  private val lock = new java.util.concurrent.ReentrantReadWriteLock()

  // Private helper methods
  private def withReadLock[R](operation: => R): R = {
    lock.readLock().lock()
    try operation finally lock.readLock().unlock()
  }

  private def withWriteLock[R](operation: => R): R = {
    lock.writeLock().lock()
    try operation finally lock.writeLock().unlock()
  }

  // Protected methods for potential subclassing
  protected def validateItem(item: T): Boolean = item != null
  protected def onItemAdded(item: T): Unit = {}
  protected def onItemRemoved(item: T): Unit = {}

  // Public API implementation
  def add(item: T): Boolean = withWriteLock {
    if (!validateItem(item)) {
      false
    } else if (items.size >= maxSize && !items.contains(item)) {
      false  // Max size reached
    } else {
      val added = items.add(item)
      if (added) onItemAdded(item)
      added
    }
  }

  def remove(item: T): Boolean = withWriteLock {
    val removed = items.remove(item)
    if (removed) onItemRemoved(item)
    removed
  }

  def contains(item: T): Boolean = withReadLock {
    items.contains(item)
  }

  def size: Int = withReadLock {
    items.size
  }

  def isEmpty: Boolean = withReadLock {
    items.isEmpty
  }

  def toList: List[T] = withReadLock {
    items.toList
  }

  // Package-private methods for internal use
  private[collections] def getCapacity: Int = maxSize
  private[collections] def getCurrentLoad: Double = size.toDouble / maxSize
}

// Companion object with factory methods (public API)
object ThreadSafeSet {
  // Primary factory method
  def apply[T](maxSize: Int = 1000): ThreadSafeSet[T] = {
    require(maxSize > 0, "Max size must be positive")
    new ThreadSafeSet[T](maxSize, false)
  }

  // Factory methods for different use cases
  def withCapacity[T](capacity: Int): ThreadSafeSet[T] = {
    require(capacity > 0, "Capacity must be positive")
    new ThreadSafeSet[T](capacity, false)
  }

  def unbounded[T](): ThreadSafeSet[T] = {
    new ThreadSafeSet[T](Int.MaxValue, false)
  }

  // Factory from existing collection
  def from[T](items: Iterable[T], maxSize: Int = 1000): ThreadSafeSet[T] = {
    val set = new ThreadSafeSet[T](maxSize, false)
    items.foreach(set.add)
    set
  }
}

// Specialized subclass with additional behavior
class AuditedSet[T](maxSize: Int) extends ThreadSafeSet[T](maxSize, false) {
  // Private audit log
  private val auditLog: scala.collection.mutable.ListBuffer[String] = 
    scala.collection.mutable.ListBuffer.empty

  // Override protected methods to add auditing
  override protected def onItemAdded(item: T): Unit = {
    super.onItemAdded(item)
    auditLog += s"ADDED: $item at ${java.time.LocalDateTime.now()}"
  }

  override protected def onItemRemoved(item: T): Unit = {
    super.onItemRemoved(item)
    auditLog += s"REMOVED: $item at ${java.time.LocalDateTime.now()}"
  }

  // Public method to access audit log
  def getAuditLog: List[String] = auditLog.toList

  def clearAuditLog(): Unit = auditLog.clear()
}

// Package-private utility class
private[collections] class CollectionMonitor {
  def monitorCollection[T](collection: ThreadSafeSet[T]): String = {
    // Can access package-private methods
    val capacity = collection.getCapacity
    val load = collection.getCurrentLoad
    val usage = f"${load * 100}%.1f%%"

    s"Collection Monitor: $usage full (${collection.size}/$capacity)"
  }
}

// Usage examples
val regularSet = ThreadSafeSet.withCapacity[String](100)
regularSet.add("item1")
regularSet.add("item2")
regularSet.add("item3")

println(s"Regular set size: ${regularSet.size}")
println(s"Contains item2: ${regularSet.contains("item2")}")

val auditedSet = new AuditedSet[Int](50)
auditedSet.add(1)
auditedSet.add(2)
auditedSet.remove(1)

println(s"Audited set size: ${auditedSet.size}")
println("Audit log:")
auditedSet.getAuditLog.foreach(println)

// Package-private monitor usage
val monitor = new CollectionMonitor()
println(monitor.monitorCollection(regularSet))
println(monitor.monitorCollection(auditedSet))

Builder Pattern with Access Control

package com.company.http

// Public interface
trait HttpRequest {
  def method: String
  def url: String
  def headers: Map[String, String]
  def body: Option[String]
  def timeout: Int
  def execute(): HttpResponse
}

trait HttpResponse {
  def status: Int
  def headers: Map[String, String]
  def body: String
  def isSuccess: Boolean = status >= 200 && status < 300
}

// Internal implementation
private[http] class HttpRequestImpl(
  val method: String,
  val url: String,
  val headers: Map[String, String],
  val body: Option[String],
  val timeout: Int
) extends HttpRequest {

  def execute(): HttpResponse = {
    // Simulate HTTP request execution
    Thread.sleep(100)  // Simulate network delay

    new HttpResponseImpl(
      status = 200,
      headers = Map("Content-Type" -> "application/json"),
      body = s"""{"method": "$method", "url": "$url", "status": "success"}"""
    )
  }
}

private[http] class HttpResponseImpl(
  val status: Int,
  val headers: Map[String, String],
  val body: String
) extends HttpResponse

// Builder with fluent interface and validation
class HttpRequestBuilder private() {
  // Private mutable state
  private var _method: String = "GET"
  private var _url: String = ""
  private var _headers: Map[String, String] = Map.empty
  private var _body: Option[String] = None
  private var _timeout: Int = 30000  // 30 seconds default

  // Private validation methods
  private def validateUrl(url: String): Either[String, String] = {
    if (url.isEmpty) Left("URL cannot be empty")
    else if (!url.startsWith("http://") && !url.startsWith("https://")) Left("URL must start with http:// or https://")
    else Right(url)
  }

  private def validateMethod(method: String): Either[String, String] = {
    val validMethods = Set("GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS")
    if (validMethods.contains(method.toUpperCase)) Right(method.toUpperCase)
    else Left(s"Invalid HTTP method: $method")
  }

  private def validateTimeout(timeout: Int): Either[String, Int] = {
    if (timeout < 1000) Left("Timeout must be at least 1000ms")
    else if (timeout > 300000) Left("Timeout cannot exceed 300000ms (5 minutes)")
    else Right(timeout)
  }

  // Fluent builder methods
  def method(method: String): HttpRequestBuilder = {
    validateMethod(method) match {
      case Right(validMethod) => _method = validMethod
      case Left(error) => throw new IllegalArgumentException(error)
    }
    this
  }

  def url(url: String): HttpRequestBuilder = {
    validateUrl(url) match {
      case Right(validUrl) => _url = validUrl
      case Left(error) => throw new IllegalArgumentException(error)
    }
    this
  }

  def header(key: String, value: String): HttpRequestBuilder = {
    require(key.nonEmpty, "Header key cannot be empty")
    require(value.nonEmpty, "Header value cannot be empty")
    _headers = _headers + (key -> value)
    this
  }

  def headers(headers: Map[String, String]): HttpRequestBuilder = {
    headers.foreach { case (k, v) => header(k, v) }
    this
  }

  def body(body: String): HttpRequestBuilder = {
    _body = Some(body)
    this
  }

  def jsonBody(json: String): HttpRequestBuilder = {
    _body = Some(json)
    header("Content-Type", "application/json")
  }

  def timeout(timeoutMs: Int): HttpRequestBuilder = {
    validateTimeout(timeoutMs) match {
      case Right(validTimeout) => _timeout = validTimeout
      case Left(error) => throw new IllegalArgumentException(error)
    }
    this
  }

  // Build method with final validation
  def build(): Either[String, HttpRequest] = {
    if (_url.isEmpty) {
      Left("URL is required")
    } else if (_method == "POST" || _method == "PUT" || _method == "PATCH") {
      if (_body.isEmpty) {
        Left(s"${_method} requests require a body")
      } else {
        Right(new HttpRequestImpl(_method, _url, _headers, _body, _timeout))
      }
    } else {
      Right(new HttpRequestImpl(_method, _url, _headers, _body, _timeout))
    }
  }

  // Convenience method that throws on validation error
  def buildUnsafe(): HttpRequest = {
    build() match {
      case Right(request) => request
      case Left(error) => throw new IllegalStateException(s"Cannot build request: $error")
    }
  }
}

// Companion object with factory methods
object HttpRequestBuilder {
  def create(): HttpRequestBuilder = new HttpRequestBuilder()

  // Convenience factory methods
  def get(url: String): HttpRequestBuilder = create().method("GET").url(url)
  def post(url: String): HttpRequestBuilder = create().method("POST").url(url)
  def put(url: String): HttpRequestBuilder = create().method("PUT").url(url)
  def delete(url: String): HttpRequestBuilder = create().method("DELETE").url(url)
}

// Public facade object for the HTTP client
object HttpClient {
  def request(): HttpRequestBuilder = HttpRequestBuilder.create()

  def get(url: String): HttpRequestBuilder = HttpRequestBuilder.get(url)
  def post(url: String): HttpRequestBuilder = HttpRequestBuilder.post(url)
  def put(url: String): HttpRequestBuilder = HttpRequestBuilder.put(url)
  def delete(url: String): HttpRequestBuilder = HttpRequestBuilder.delete(url)

  // Convenience methods for simple requests
  def simpleGet(url: String): HttpResponse = {
    get(url).buildUnsafe().execute()
  }

  def simplePost(url: String, jsonBody: String): HttpResponse = {
    post(url).jsonBody(jsonBody).buildUnsafe().execute()
  }
}

// Usage examples
val response1 = HttpClient.simpleGet("https://api.example.com/users")
println(s"Response: ${response1.status} - ${response1.body}")

val request = HttpClient.post("https://api.example.com/users")
  .header("Authorization", "Bearer token123")
  .jsonBody("""{"name": "John", "email": "john@example.com"}""")
  .timeout(60000)
  .build()

request match {
  case Right(req) =>
    val response = req.execute()
    println(s"Created user: ${response.status} - ${response.isSuccess}")
  case Left(error) =>
    println(s"Request validation failed: $error")
}

// Builder pattern allows for readable, validated construction
val complexRequest = HttpClient.request()
  .method("PUT")
  .url("https://api.example.com/users/123")
  .headers(Map(
    "Authorization" -> "Bearer token123",
    "X-Client-Version" -> "1.0.0",
    "Accept" -> "application/json"
  ))
  .jsonBody("""{"name": "John Doe", "active": true}""")
  .timeout(120000)
  .buildUnsafe()

val updateResponse = complexRequest.execute()
println(s"Update response: ${updateResponse.status}")

Summary

In this lesson, you've mastered visibility and access control in Scala:

Access Modifiers: Private, protected, and package-private visibility
Qualified Access: Fine-grained control with package-level privacy
Protected Inheritance: Sharing implementation details with subclasses
API Design: Creating clean public interfaces while hiding implementation
Builder Patterns: Using access control for safe object construction
Encapsulation: Protecting internal state and maintaining invariants

Proper access control is essential for creating maintainable, secure code that provides clear contracts between different parts of your application.

What's Next

In the next lesson, we'll explore "The Apply Method: Making Objects Callable." You'll learn how to use the apply method to create function-like syntax for objects, implement factory patterns elegantly, and make your APIs more intuitive and concise.

This will complete your understanding of Scala's core object-oriented features and introduce you to one of Scala's most distinctive syntactic features.

Ready to master the apply method? Let's continue!