러스트 객체지향
- 러스트는 객체지향 언어가 아니어서, 객체지향의 캡슐화, 다형성, 상속 등을 완벽하게 지원하지는 못한다
- 그러나 trait 같은 형태로 객체지향의 핵심적인 개념을 사용할 수 있다
캡슐화
- 객체 내부의 정보를 숨겨 외부에서 접근할 수 없도록 제어하는 것
pub 키워드를 사용해서 캡슐화 할 수 있다
다형성
- 객체가 문맥에 따라서 다른 자료형으로 형태를 취할 수 있는 것
trait, dyn 키워드로 다형성을 사용할 수 있다
trait
trait Hello {
fn hello_msg(&self) -> String;
}
fn print_hello(say: &dyn Hello) {
println!("{}", say.hello_msg());
}
struct Student {}
impl Hello for Student {
fn hello_msg(&self) -> String {
String::from("hello student")
}
}
struct Teacher {}
impl Hello for Teacher {
fn hello_msg(&self) -> String {
String::from("hello teacher")
}
}
fn main() {
let student = Student {};
let teacher = Teacher {};
print_hello(&student);
print_hello(&teacher);
}
- trait은 함수를 추상화해 트레잇을 구현하는 구조체들이 같은 이름의 API를 제공하도록 한다
- dyn은 컴파일러에 트레잇의 구현체를 런타임에 찾을 것을 의미한다
- trait은
impl 트레잇 이름 for 구조체 이름 형식으로 정의한다
상속
- rust에는 상속이 없다
- 데코레이터나 프록시 패턴으로 상속을 유사하게 사용하는 것이 최선이다
디자인 패턴
| 범주 | 설명 | 주요 패턴 |
|---|
| 생성 | 객체 생성과 관련된 패턴 | 빌더, 팩토리 메서드, 싱글턴 등 |
| 구조 | 객체 간 구조화와 관련된 패턴 | 어댑터, 브릿지, 컴포지트, 데코레이터, 플라이웨이트 등 |
| 행동 | 알고리즘, 객체 간 작동에 관련된 패턴 | 반복자, 옵저버, 상태, 전략 등 |
디자인 패턴 - 생성
팩토리 메서드
trait Pizza {
fn eat(&self);
}
enum PizzaType {
Bulgogi,
Hawaiian,
}
trait PizzaFactory {
fn create(pizza_type: PizzaType) -> Box<dyn Pizza>;
}
struct ConcretePizzaFactory {}
impl PizzaFactory for ConcretePizzaFactory {
fn create(pizza_type: PizzaType) -> Box<dyn Pizza> {
match pizza_type {
PizzaType::Bulgogi => Box::new(BulgogiPizza {}),
PizzaType::Hawaiian => Box::new(HawaiianPizza {})
}
}
}
struct BulgogiPizza {}
impl Pizza for BulgogiPizza {
fn eat(&self) {
println!("eat bulgogi pizza");
}
}
struct HawaiianPizza {}
impl Pizza for HawaiianPizza {
fn eat(&self) {
println!("eat hawaiian pizza");
}
}
fn main() {
let bulgogi = ConcretePizzaFactory::create(PizzaType::Bulgogi);
let hawaiian = ConcretePizzaFactory::create(PizzaType::Hawaiian);
bulgogi.eat();
hawaiian.eat();
}
- 팩토리 메서드 패턴은 객체의 생성을 별도 생성 모듈에 위임해, 객체 생성 과정 및 관리를 생성 모듈이 제어한다
싱글턴 패턴
[dependencies]
lazy_static = "1.4.0"
#[macro_use]
extern crate lazy_static;
lazy_static! {
static ref INSTANCE: MySingleton = {
MySingleton::new(String::from("singleton"))
};
}
struct MySingleton {
name: String
}
impl MySingleton {
fn new(name: String) -> MySingleton {
MySingleton { name }
}
fn call(&self) {
println!("{}", self.name);
}
}
fn main() {
INSTANCE.call();
}
- 싱글턴 패턴은 시스템에 하나의 인스턴스만 생성되도록한다
- lazy_static 크레이트를 사용해서 구현할 수 있다
빌더 패턴
struct Burger {
bun: String,
patties: i32,
sauce: String,
extra: Vec<String>
}
struct BurgerBuilder {
bun: String,
patties: i32,
sauce: String,
extra: Vec<String>
}
impl BurgerBuilder {
fn new() -> BurgerBuilder {
BurgerBuilder { bun: String::from(""), patties: 0, sauce: String::from(""), extra: vec![] }
}
fn bun(mut self, bun: String) -> BurgerBuilder {
self.bun = bun;
self
}
fn patties(mut self, patties: i32) -> BurgerBuilder {
self.patties = patties;
self
}
fn sauce(mut self, sauce: String) -> BurgerBuilder {
self.sauce = sauce;
self
}
fn add_extra(mut self, extra: String) -> BurgerBuilder {
self.extra.push(extra);
self
}
fn build(self) -> Burger {
Burger { bun: self.bun, patties: self.patties, sauce: self.sauce, extra: self.extra }
}
}
fn main() {
let burger = BurgerBuilder::new()
.bun(String::from("bun"))
.patties(2)
.sauce(String::from("sauce"))
.add_extra(String::from("extra"))
.build();
}
- 빌더 패턴은 복잡한 파라미터를 가진 객체를 체인과 같은 형태로 쉽게 만들 수 있게 해준다
디자인 패턴 - 구조
adapter 구조
struct Adaptee {}
impl Adaptee {
fn new() -> Adaptee {
Adaptee {}
}
fn specific_api(&self) {
println!("벤더가 정의한 API")
}
}
struct Adapter {}
impl Adapter {
fn new() -> Adapter {
Adapter {}
}
fn call_api(&self) {
Adaptee::new().specific_api();
}
}
fn main() {
let adapter = Adapter::new();
adapter.call_api();
}
- 기존 구현체(Adaptee)가 시스템 인터페이스와 호환되지 않는 경우
- 기존 구현체의 인터페이스를 감싸는 Adapter를 정의해서 사용
Composite 패턴
trait Control {
fn draw(&self) -> String;
}
struct Button {
name: String
}
impl Button {
fn new(name: String) -> Box<Button> {
Box::new(Button { name })
}
}
impl Control for Button {
fn draw(&self) -> String {
format!("Button - {}", self.name)
}
}
struct Panel {
name: String,
controls: Vec<Box<dyn Control>>
}
impl Panel {
fn new(name: String) -> Box<Panel> {
Box::new(Panel { name, controls: vec![] })
}
fn add_control(&mut self, control: Box<dyn Control>) {
self.controls.push(control);
}
}
impl Control for Panel {
fn draw(&self) -> String {
let mut txt = format!("Panel - {}", self.name);
for control in self.controls.iter() {
txt = format!("{}\n\t{}", txt, control.draw())
}
txt
}
}
fn main() {
let mut root = Panel::new(String::from("root"));
root.add_control(Button::new(String::from("button 1")));
let mut panel = Panel::new(String::from("panel 1"));
panel.add_control(Button::new(String::from("button 2")));
root.add_control(panel);
println!("{}", root.draw());
}
- 객체 그룹을 단일 객체와 동일한 방식으로 처리할 수 있는 디자인 패턴
- 객체 그룹은 트리 형태로 구성된다
- 각 트리의 노드들은 객체 그룹이거나 단일 객체이다
decorator 패턴
trait Control {
fn draw(&self) -> String;
}
struct Button {
name: String,
controls: Vec<Box<dyn Control>>
}
impl Button {
fn new(name: String) -> Box<Button> {
Box::new(Button { name, controls: vec![] })
}
fn add_decorator(&mut self, control: Box<dyn Control>) {
self.controls.push(control);
}
}
impl Control for Button {
fn draw(&self) -> String {
let mut txt = format!("{}", self.name);
for control in self.controls.iter() {
txt = format!("{} and {}", txt, control.draw())
}
txt
}
}
struct Deco {
name : String
}
impl Control for Deco {
fn draw(&self) -> String {
format!("{}", self.name)
}
}
impl Deco {
fn new(name: String) -> Box<Deco> {
Box::new(Deco {name})
}
}
fn main() {
let mut button = Button::new(String::from("AAA"));
button.add_decorator(Deco::new(String::from("BBB")));
button.add_decorator(Deco::new(String::from("CCC")));
println!("{}", button.draw());
}
- 기존 객체에 컴포지션을 활용해서 새로운 기능을 동적으로 추가 및 확장
- 같은 클래스 다른 객체 작동에 영향을 주지 않고 비슷한 효과를 낼 수 있음
- 데코레이터 패턴을 사용하면 상속과 비슷한 효과를 낼 수 있음
Flyweight 패턴
use std::{collections::HashMap, rc::Rc};
trait Flyweight {
fn get_name(&self) -> String;
}
struct ConcreteFlyweight {
name: String
}
impl Flyweight for ConcreteFlyweight {
fn get_name(&self) -> String {
self.name.clone()
}
}
struct FlyweightFactory {
flyweights: HashMap<String, Rc<Box<dyn Flyweight>>>
}
impl FlyweightFactory {
fn new() -> FlyweightFactory {
FlyweightFactory { flyweights: HashMap::new() }
}
fn get_flyweight(&mut self, name: String) -> Rc<Box<dyn Flyweight>> {
if let Some(instance) = self.flyweights.get(&name) {
return instance.clone();
}
let instance = Box::new(ConcreteFlyweight {
name: name.clone()
});
let instance = Rc::new(instance as Box<dyn Flyweight>);
self.flyweights.insert(name, instance.clone());
instance.clone()
}
}
fn main() {
let mut factory = FlyweightFactory::new();
let flyweight1 = factory.get_flyweight(String::from("AAA"));
let flyweight2 = factory.get_flyweight(String::from("BBB"));
let flyweight3 = factory.get_flyweight(String::from("AAA"));
println!("{}, {}, {}", flyweight1.get_name(), flyweight2.get_name(), flyweight3.get_name());
}
- Flyweight 패턴은 객체 간 공통 부분이나 객체 전체를 공유해 시스템 메모리를 절약하는 패턴이다
- 위 예제에서는 hash map으로 캐싱하여 flyweight 객체 전체를 공유하였다
디자인 패턴 - 행동
옵저버 패턴
use std::rc::Rc;
#[derive(PartialEq)]
struct Listener {}
impl Listener {
fn on_event(&self, data: &str) {
println!("event: {}", data);
}
}
struct Subject {
observers: Vec<Rc<Listener>>
}
impl Subject {
fn subscribe(&mut self, observer: Rc<Listener>) {
self.observers.push(observer);
}
fn unsubscribe(&mut self, observer: Rc<Listener>) {
if let Some(index) = self.observers.iter().position(|o| *o == observer) {
self.observers.remove(index);
}
}
fn notify(&self, data: &str) {
for observer in &self.observers {
observer.on_event(data);
}
}
}
fn main() {
let mut subject = Subject {observers: vec![]};
let observer1 = Rc::new(Listener {});
let observer2 = Rc::new(Listener {});
subject.subscribe(observer1.clone());
subject.subscribe(observer2.clone());
subject.notify("event 1");
subject.unsubscribe(observer1);
subject.notify("event 2");
}
- 객체의 상태가 변경되었을 때 모든 관찰자에게 event를 전송하는 패턴
strategy 패턴
trait Render {
fn render(&self, title: String, body: String);
}
struct HTMLRender {}
impl Render for HTMLRender {
fn render(&self, title: String, body: String) {
println!("<html><title>{}</title><body>{}</body></html>", title, body);
}
}
struct MarkDownRender {}
impl Render for MarkDownRender {
fn render(&self, title: String, body: String) {
println!("# {}\n{}", title, body);
}
}
struct Page<T: Render> {
renderer: T
}
impl<T: Render> Page<T> {
fn new(renderer: T) -> Page<T> {
Page { renderer }
}
fn render(&self, title: String, body: String) {
self.renderer.render(title, body);
}
}
fn main() {
let html = Page::new(HTMLRender {});
html.render(String::from("title"), String::from("body"));
}
- 상황에 맞춰 필요한 동작을 런타임에서 선택하는 패턴
상태 패턴
trait State {
fn on_state_changed(self: Box<Self>) -> Box<dyn State>;
}
struct Start;
impl State for Start {
fn on_state_changed(self: Box<Self>) -> Box<dyn State> {
println!("current: Start");
Box::new(Running)
}
}
struct Running;
impl State for Running {
fn on_state_changed(self: Box<Self>) -> Box<dyn State> {
println!("current: Running");
Box::new(Stop)
}
}
struct Stop;
impl State for Stop {
fn on_state_changed(self: Box<Self>) -> Box<dyn State> {
println!("current: Stop");
self
}
}
struct Boot {
state: Option<Box<dyn State>>
}
impl Boot {
fn new() -> Boot {
Boot {state: Some(Box::new(Stop {}))}
}
fn boot(&mut self) {
self.state = Some(Box::new(Start {}))
}
fn next(&mut self) {
if let Some(cur) = self.state.take() {
self.state = Some(cur.on_state_changed())
}
}
}
fn main() {
let mut machine = Boot::new();
machine.boot();
machine.next();
machine.next();
machine.next();
}
- 상태 패턴은 내부 상태가 변경될 때 마다 해당 객체의 행동을 변경할 수 있도록 하는 디자인 패턴이다
- 내부 상태에 따라 다양한 작동을 하는 객체를 만들 때 유용하다