R OOP with S4
S4 is R's formal object-oriented system. Unlike S3, S4 requires explicit class definitions with typed slots, formal method signatures, and validation rules. It is stricter, more structured, and better suited for large, collaborative projects where data integrity is critical.
S3 vs S4 Comparison
Feature S3 S4
──────────────────────────────────────────────────────────────────
Class definition Implicit (assign class) Explicit setClass()
Slot access $ operator @ operator
Slot types No enforcement Enforced (typed slots)
Method definition function.class() setMethod()
Validation Manual validity= argument
Inheritance c("Child","Parent") contains= argument
Checking class(x) is(x, "ClassName")
Defining an S4 Class
setClass("BankAccount", representation(
owner = "character",
balance = "numeric",
id = "character"
), validity = function(object) {
if (object@balance < 0) "Balance cannot be negative"
else TRUE
})
Creating S4 Objects with new()
acc1 <- new("BankAccount",
owner = "Asha Sharma",
balance = 10000,
id = "ACC-001")
# Access slots with @
acc1@owner # "Asha Sharma"
acc1@balance # 10000
acc1@id # "ACC-001"
# Check object
isVirtualClass("BankAccount") # FALSE
is(acc1, "BankAccount") # TRUE
slotNames(acc1) # "owner" "balance" "id"
Defining S4 Methods
# Generic function (define once)
setGeneric("deposit", function(account, amount) {
standardGeneric("deposit")
})
# Method for BankAccount
setMethod("deposit", "BankAccount", function(account, amount) {
if (amount <= 0) stop("Deposit amount must be positive")
account@balance <- account@balance + amount
cat("Deposited ₹", amount, "| New balance: ₹", account@balance, "\n")
account
})
acc1 <- deposit(acc1, 5000)
# Deposited ₹ 5000 | New balance: ₹ 15000
show() Method (like print for S4)
setMethod("show", "BankAccount", function(object) {
cat("Account ID: ", object@id, "\n")
cat("Owner: ", object@owner, "\n")
cat("Balance: ₹", object@balance, "\n")
})
acc1 # automatically calls show()
# Account ID: ACC-001
# Owner: Asha Sharma
# Balance: ₹ 15000
Inheritance with S4
setClass("SavingsAccount",
contains = "BankAccount", # inherits BankAccount
representation(
interest_rate = "numeric"
)
)
setMethod("show", "SavingsAccount", function(object) {
callNextMethod() # calls BankAccount show()
cat("Interest Rate:", object@interest_rate * 100, "%\n")
})
savings <- new("SavingsAccount",
owner="Balu", balance=20000,
id="SAV-001", interest_rate=0.06)
savings
# Account ID: SAV-001
# Owner: Balu
# Balance: ₹ 20000
# Interest Rate: 6 %
Multiple Dispatch
# S4 allows methods that dispatch on the type of MULTIPLE arguments
setGeneric("transfer", function(from, to, amount) standardGeneric("transfer"))
setMethod("transfer",
signature("BankAccount","BankAccount","numeric"),
function(from, to, amount) {
from@balance <- from@balance - amount
to@balance <- to@balance + amount
cat("Transferred ₹", amount, "from", from@id, "to", to@id, "\n")
list(from=from, to=to)
}
)
Validity Checking
# validObject() explicitly checks the validity function
tryCatch({
bad_acc <- new("BankAccount", owner="X", balance=-500, id="BAD")
validObject(bad_acc)
}, error = function(e) cat("Validation Error:", conditionMessage(e), "\n"))
# Validation Error: Balance cannot be negative
S4 shines in large, multi-developer projects where strict type enforcement prevents data corruption. The Bioconductor project — which powers most computational biology in R — uses S4 extensively. For smaller projects, S3 or R6 is usually simpler. Choose S4 when you need guaranteed slot types, multiple dispatch, and formal inheritance hierarchies.
