Hello! In this lesson, we will explore an essential aspect of Rust programming and OOP — modules and encapsulation. This lesson welcomes you into the world of Rust's modularity and the principles of encapsulation, key features in writing maintainable and scalable code.
Modules in Rust help organize code into separate namespaces, making it easier to manage and navigate larger projects. Encapsulation allows you to restrict access to parts of your code, promoting safer and more intentional interactions with your data structures.
In this lesson, we will:
pub
struct
methodsLet's dive in!
Modules in Rust are like containers that organize your functions, structs
, traits, and methods. They allow you to create structs and methods while using encapsulation to control access to a structs methods and fields.
Here's a simple example to create a module in Rust:
Rust1mod bank { 2 struct BankAccount { 3 balance: f32, 4 name: String, 5 } 6}
In this example:
bank
using the mod
keywordbank
module, we created a BankAccount
struct with two fieldsNow that we have created a module, let's explore how to use it! By default, any structs or methods defined in a module are private and cannot be accessed by code outside the module. To allow access to code inside the module, we use the pub
keyword, making the code public. In this section we make the BankAccount
public, create a public constructor, and create a new BankAccount
instance.
Rust1mod bank { 2 pub struct BankAccount { 3 balance: f32, 4 name: String, 5 } 6 7 impl BankAccount { 8 pub fn new(balance: f32, name: String) -> BankAccount { 9 BankAccount { balance: balance, name: name } 10 } 11 } 12} 13 14fn main() { 15 let my_account = bank::BankAccount::new(1000.0, String::from("Cosmo")); 16}
pub
keyword to the BankAccount
struct allows code in the main
function to access the struct
BankAccount
struct called new
that creates a new instance of a BankAccount
main
, we created an instance of a BankAccount
new
method is <module name>::<struct name>::<struct method>(<input parameters>)
Let's take a deeper dive into encapsulation with the pub
keyword. Continuing with the BankAccount
example, currently both fields of the struct are private.
We want users to be allowed to check the balance and deposit money, but not allow them to directly access the balance
field. To do this, we will create a public deposit
method to change the balance
field and a public get_balance
method.
Additionally, we want the name associated with the account to be accessed by anyone, so we make it public. Let's take a look:
Rust1mod bank { 2 pub struct BankAccount { 3 balance: f32, 4 pub name: String, 5 } 6 7 impl BankAccount { 8 pub fn new(balance: f32, name: String) -> BankAccount { 9 BankAccount { balance: balance, name: name } 10 } 11 12 pub fn deposit(&mut self, amount: f32) { 13 self.balance += amount; 14 } 15 16 pub fn get_balance(&self) -> f32 { 17 self.balance 18 } 19 } 20} 21 22fn main() { 23 let mut my_account = bank::BankAccount::new(1000.0, String::from("Cosmo")); 24 25 my_account.deposit(500.0); 26 println!("Balance: ${}", my_account.get_balance()); // Prints: Balance: $1500 27 //println!("Account balance is {}", my_account.balance); // Causes error because balance field is private 28 29 println!("Account belongs to {}", my_account.name); // Prints: Account belongs to Cosmo 30}
Let's break this code down step by step.
bank
module, we've changed the name
field to be publicdeposit
that takes in a mutable reference to self
get_balance
that returns the account balance.In the main function
BankAccount
called my_accountdeposit
and get_balance
methods are public, so we can call them and print the results.my_account.balance
, we will get an error because the balance
field is privatename
field because it is publicTo make the importance of encapsulation more clear we will allow users to withdraw money, ensuring the account balance does not become negative.
Adding onto the code from the previous section:
Rust1mod bank { 2 pub struct BankAccount { 3 balance: f32, 4 pub name: String, 5 } 6 7 impl BankAccount { 8 pub fn new(balance: f32, name: String) -> BankAccount { 9 BankAccount { balance: balance, name: name } 10 } 11 12 pub fn deposit(&mut self, amount: f32) { 13 self.balance += amount; 14 } 15 16 pub fn get_balance(&self) -> f32 { 17 self.balance 18 } 19 20 pub fn withdraw(&mut self, amount: f32) -> String { 21 if self.balance >= amount { 22 self.balance -= amount; 23 String::from("Account balance: $") + &self.balance.to_string() 24 } else { 25 String::from("Insufficient Funds") 26 } 27 } 28 } 29} 30 31fn main() { 32 let mut my_account = bank::BankAccount::new(1000.0, String::from("Cosmo")); 33 34 println!("{}", my_account.withdraw(300.5)); // Prints: Account balance: $1199.5 35 println!("{}", my_account.withdraw(2000.0)); // Prints: Insufficient Funds 36 37 // my_account.balance -= 2000.0 // Causes error since `balance` is private 38 39 println!("Final balance: ${}", my_account.get_balance()); // Prints: Final balance: $1199.5 40}
The withdraw
method ensures that amount
can be withdrawn from the balance
without the account becoming negative.
The code shows the successful withdrawal of $300.50 and an unsuccessful withdrawal of $2000.
The attempt to directly modify my_account.balance
causes an error because the balance
field is private. If users could directly modify the balance
field, they could withdraw more money than they actually have.
Great job! You've learned how to create and use modules, control access with pub
, and apply encapsulation in Rust. These skills are crucial for organizing your code and enhancing its readability, safety, and maintainability.
Now it’s time to practice what you’ve learned. Get ready to dive into the exercises and reinforce your understanding of modules and encapsulation in Rust.