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!
Comments
Be the first to comment on this lesson!