Coupling Pallets
In computer science, coupling is the degree to which two software modules depend on each other. System designers use the terms high and low coupling to describe how computer systems are structured. The term also applies to object oriented programming paradigms, whereby tight coupling is when two groups of classes are dependant on each other, and loose coupling is when a class uses an interface that another class exposes.
In Substrate, tight and loose pallet coupling is used to for calling a function that lives inside another pallet. Both techniques achieve the same thing in different ways, each having certain trade-offs. In a nutshell, tight coupling of pallets should be used in instances where a pallet requires inheriting its coupled counterpart as a whole as opposed to specific types or methods. In general, tight coupling makes working with two pallets less flexible and extensible.
Tightly coupled pallets
Tightly coupling pallets is more explicit than loosely coupling them. When writing a pallet that
tightly couple, you explicitly specify the pallet's Config
trait to be bound by the Config
trait
of the other pallet you want to couple with.
Notice that all FRAME pallets are tightly coupled to the frame_system
pallet. Here's an example
of tightly coupling a pallet with the Config
trait of an imaginary pallet called some_pallet
in addition to frame_system
:
pub trait Config: frame_system::Config + some_pallet::Config {
// --snip--
}
This is very similar to using class inteheritance in object oriented programming.
Supplying this trait bound implies that this pallet can only be installed in a runtime that also
has some_pallet
pallet installed. Similar to with frame_system
, this approach would require
specifying some_pallet
in the coupled pallet's Cargo.toml file.
Tight coupling has several disadvantages developers should take into account:
- Maintainability: changes in one pallet will often result in needing to modify the other pallet.
- Reusability: both modules must be included for one to be used, making it more difficult to integrate a tightly coupled pallet.
Loosely coupled pallets
Unlike tight coupling, in loose coupling pallet we just specify traits and function interfaces that certain types need to be bound by.
The actual implementation of such types happens outside of the pallet during our runtime configuration
(usually in the /runtime/src/lib.rs
file). Here one may choose to configure it with
another pallet that has implemented these traits, or declare a totally new struct,
implement those traits, and assign it when implementing the pallet config in runtime.
Let's go through an example. Say in one pallet we want to tap into one's account balance and
make a transfer to another account. We first define a Currency
trait, which has an abstract
function interface that is agreed will implement the actual transfer logic later.
pub trait Currency<AccountId> {
// -- snip --
fn transfer(
source: &AccountId,
dest: &AccountId,
value: Self::Balance,
// don't worry about the last parameter for now
existence_requirement: ExistenceRequirement,
) -> DispatchResult;
}
Then inside our own pallet, we define MyCurrency
associated type and bound it by
Currency<Self::AccountId>
so we can tap into the balance tranfer logic by calling
T::MyCurrency::transfer(...)
.
pub trait Config: frame_system::Config {
type MyCurrency: Currency<Self::AccountId>;
}
impl<T: Config> Pallet<T> {
pub fn my_function() {
T::MyCurrency::transfer(&buyer, &seller, price, ExistenceRequirement::KeepAlive)?;
}
}
Notice that at this point, we have not specified how the Currency::transfer()
logic will be implemented.
It is only agreed upon that it will be implemented somewhere.
Now in our runtime configuration (i.e. runtime/src/lib.rs
), we have our runtime implements the
pallet, and concretely specify the type to be Balances
.
impl my_pallet::Config for Runtime {
type MyCurrency = Balances;
}
Balances
, which is specified in construct_runtime!
macro, is of type
pallet_balances
that implements Currency
trait.
By now we have closed the loop and provide on implementation to Currency<AccountId>
trait.
Further notes
Notice that many FRAME pallets are coupled to this Currency trait in this way.
In general, loose coupling will provide more flexibility than tight coupling and is considered better practice from a system design perspective. It guarantees better maintability, reusability, and extensibility of code. Yet, tight coupling can be a good first choice for scenarios where the pallets are minimally complex and have more overlap in methods and types than differences.
In FRAME, there exists 2 pallets that are tightly coupled to
pallet_treasury
: the
Bounties pallet and the
Tipping pallet.
As a general rule, the more complex a pallet is, the less desirable it would be to tightly couple it. This evokes a concept in computer science called cohesion, a metric used to examine the overall quality of a software system.
Learn more
- Read the Object Oriented Programming Features of Rust section of the Rust Book
- Learn how to use loose coupling and tight coupling in your runtimes