You need to deal with incompatible interfaces given existing code bases.(The client expects to use the same interface as before)
Here, I'll give a simple yet practical example involving electrical plugs and receptacles.
First, let's define our receptacle trait and implement it for Japanese and Korean receptacles:
// Receptacle is what the adapter will adapt to. It is the interface that the adapter will implement.
pub trait TReceptacle {
const VOLT: u8;
fn receive(&self, volt: u8) -> Result<(), &'static str> {
if volt != Self::VOLT {
return Err("Receptacle cannot receive the plug.");
}
println!("Receptacle received the plug.");
Ok(())
}
fn bolt_to_accept(&self) -> u8 {
Self::VOLT
}
}
pub struct JapaneseReceptacle;
pub struct KoreanReceptacle;
impl TReceptacle for JapaneseReceptacle {
const VOLT: u8 = 110;
}
impl TReceptacle for KoreanReceptacle {
const VOLT: u8 = 220;
}
Next, we define our plug trait and implement it for Japanese and Korean plugs
// plug is what we want to put to the receptacle. It is the interface that the adapter will take as dependency.
pub trait TPlug {
fn plug(&self) -> u8;
}
pub struct JapanesePlug;
impl TPlug for JapanesePlug {
fn plug(&self) -> u8 {
110
}
}
pub struct KoreanPlug;
impl TPlug for KoreanPlug {
fn plug(&self) -> u8 {
220
}
}
Now, we create PowerAdapter
struct that will act as intermediary layer between the plug and the receptacle. It will handle the voltage conversion if needed.
struct PowerAdapter<T: TPlug> {
port: T,
}
impl<T> PowerAdapter<T>
where
T: TPlug,
{
fn new(port: T) -> Self {
PowerAdapter { port }
}
fn convert<U: TPlug>(self, port: U) -> PowerAdapter<U> {
PowerAdapter { port }
}
fn convert_bolt(&self) -> u8 {
if self.port.plug() == 110 {
220
} else {
110
}
}
fn connect(&self, receptacle: impl TReceptacle) -> Result<(), &'static str> {
let volt = self.port.plug();
if volt == receptacle.bolt_to_accept() {
receptacle.receive(volt)
} else {
let volt = self.convert_bolt();
receptacle.receive(volt)
}
}
}
SRP - each component in our example(plugs, receptacles, and the adapter) has a single responsibility.
OCP - the code is open for extension but closed for modification. We can add new types of plugs and receptacles without modifying existing code, simply by implementing the relevant traits.
LSP - no need to explain.
DIP - adapter depends only on abstractions(generic and trait bound)
ISP - the traits(TPlug
and TReceptacle
) are specific to their functionality and do not force the implementing types to depend on methods they may not use.