# 12차 러스트 스터디
## 담당
- 발표: 이명수 님
- 리뷰: 이지한 님
- 서기: 김기덕 님
## 과제 리뷰
### [멀티 스레드 활용해보기](https://github.com/JannLee/rust-study)
## 17. 러스트의 객체 지향 프로그래밍 기능들
- 러스트에서 객체 지향을 어떻게 다룰 것인가!
### 17.1. 객체 지향 언어의 특성
- 캡슐화, 상속
- 러스트는 상속이 따로 없고 trait을 사용해 상속의 동작을 구현할 수 있다.
#### 객체는 데이터의 동작을 담습니다.
- 아래 정의에 따르면, 러스트는 객체 지향적(구조체, 열거형)
```
객체-지향 프로그램은 객체로 구성된다. 객체는 데이터 및 이 데이터를 활용하는 프로시저를 묶는다. 이 프로시저들은 보통 메소드 혹은 연산 (operation) 으로 불린다.
```
#### 상세 구현을 은닉하는 캡슐화
- 캡슐화를 활용하면 리팩토링에 좋다.
- 일반적으로 OOP와 관련된 또다른 면은 캡슐화로, 그 의미는 객체를 이용하는 코드에서 그 객체의 상세 구현에 접근할 수 없게 한다.
- 유일하게 객체와 상호작용하는 방법은 이것의 공개 API를 통하는 것
- 러스트에서는 기본 비공개, `pub 키워드를 사용`하여 공개 여부를 결정한다.
#### 타입 시스템과 코드 공유로서의 상속
- 상속을 직접적으로 지원하지는 않는다.
- trait을 사용해 다형성을 구현했다.
- trait을 오버라이딩해 기능을 재정의 할 수 있다.
### 17.2. 트레잇 객체를 사용하여 다른 타입 간의 값 허용하기
- trait을 사용해 다형성을 구현하는 내용
- 다른 언어의 인터페이스와 유사
- draw 메소드를 호출하여 이를 화면에 그리는 그래픽 유저 인터페이스(GUI) 도구 제작
#### 공통된 동작을 위한 트레잇 정의하기
- 제네릭 타입 파라미터는 한 번에 하나의 구체 타입으로만 대입될 수 있는 반면, 트레잇 객체를 사용하면 런타임에 여러 구체 타입을 트레잇 객체에 대해 채워넣을 수 있습니다.
```rust
// 제네릭 타입 예시
// 이렇게하면 전부 Button 타입 혹은 전부 TextField 타입인 컴포넌트 리스트를 가지는 Screen 인스턴스로 제한
pub struct Screen<T: Draw> {
pub components: Vec<T>,
}
impl<T> Screen<T>
where T: Draw {
pub fn run(&self) {
for component in self.components.iter() {
component.draw();
}
}
}
```
```rust=
// 트레잇 객체 사용 예시
// 트레잇 객체를 사용하는 메소드를 이용할때는 하나의 Screen 인스턴스가 Box<Button> 혹은 Box<TextField>도 담을 수 있는 Vec<T>를 보유할 수 있습니다.
pub struct Screen {
pub components: Vec<Box<Draw>>,
}
```
#### 트레잇 구현하기
- Draw 트레잇을 구현하는 Button 구조체 예시
- Draw 트레잇을 구현하여 draw 메소드를 마치 상속한 것처럼 구현
```rust
pub struct Button {
pub width: u32,
pub height: u32,
pub label: String,
}
impl Draw for Button {
fn draw(&self) {
// code to actually draw a button
}
}
```
- components 벡터에 담기는 값의 타입을 Box\<Draw>로 특정함으로서 우리는 draw 메소드를 호출할 수 있는 값을 요구하는 Screen을 정의
- `이러한 개념 —값의 구체적인 타입이 아닌 값이 응답하는 메시지 만을 고려하는 개념—` 은 동적 타입 언어들의 duck typing 개념과 유사
```rust
use gui::{Screen, Button};
fn main() {
let screen = Screen {
components: vec![
Box::new(SelectBox {
width: 75,
height: 10,
options: vec![
String::from("Yes"),
String::from("Maybe"),
String::from("No")
],
}),
Box::new(Button {
width: 50,
height: 10,
label: String::from("OK"),
}),
],
};
screen.run();
}
```
#### 트레잇 객체는 동적 디스패치를 수행합니다
- 동적 바인딩, 정적 바인딩과 비슷한 개념으로 러스트에서는 동적, 정적 디스패치를 수행한다
#### 트레잇 객체에 대하여 객체 안전성이 요구됩니다.
- 안전한 객체만 트레잇 객체로 만들 수 있다.
- 안전한 객체
- 반환값의 타입이 Self가 아닌 객체.
- 제네릭 타입 매개변수가 없는 객체.
- 객체가 안전해야하는 이유
- self 타입을 명확히 알수 없어 컴파일 오류.
- 안전하지 않은 트레잇 메소드 예시
- Clone트레잇의 clone메소드
```rust
pub trait Clone {
fn clone(&self) -> Self;
}
```
### 17.3. 객체 지향 디자인 패턴 구현하기
- state pattern 연습
- 점진적인 방식으로 블로그에 게시물을 올리는 작업 흐름을 구현
- 상태 패턴을 사용한다는 것은 상태를 보유한 값의 코드 혹은 그밧을 사용하는 코드는 변경될 필요가 없음을 의미.
- blog 크레이트가 갖길 원하는 요구 동작들을 보여주는 코드
- 상태에 따라 content 메소드 출력이 다름을 볼 수 있음
```rust
extern crate blog;
use blog::Post;
fn main() {
let mut post = Post::new();
post.add_text("I ate a salad for lunch today");
assert_eq!("", post.content());
post.request_review();
assert_eq!("", post.content());
post.approve();
assert_eq!("I ate a salad for lunch today", post.content());
}
```
#### Post를 정의하고 초안 상태의 새 인스턴스 생성하기
- State 트레잇은 게시물의 상태 변화에 따라 달라지는 동작을 정의하고, `Draft`, `PendingReview`, 그리고 `Published` 상태는 모두 State 트레잇을 구현
```rust
pub struct Post {
state: Option<Box<State>>,
content: String,
}
impl Post {
pub fn new() -> Post {
Post {
state: Some(Box::new(Draft {})),
content: String::new(),
}
}
}
trait State {}
struct Draft {}
impl State for Draft {} // Draft에서 State 트레잇 구현
```
#### 게시물 콘텐츠의 글을 저장하기
- content의 String 상에서 push_str을 호출하고 text를 인자로 전달해 저장된 content에 추가
- add_text 메소드는 가변 참조자 self를 취하는데, 그 이유는 우리가 add_text를 호출하고 있는 해당 Post 인스턴스를 변경하기 때문
```rust
pub struct Post {
content: String,
}
impl Post {
// --snip--
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
```
#### 초안 게시물의 내용이 비어있음을 보장하기
- 항상 비어있는 스트링 슬라이스를 반환하는 Post의 content 메소드에 대한 껍데기 구현
```rust
impl Post {
// --snip--
pub fn content(&self) -> &str {
""
}
}
```
#### 게시물에 대한 리뷰 요청이 그의 상태를 변경합니다
- State 트레잇에 request_review 메소드를 추가
- 트레잇을 구현하는 모든 타입은 이제 request_review 메소드를 구현 필요
```rust
impl Post {
// --snip--
pub fn request_review(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.request_review())
}
}
}
trait State {
fn request_review(self: Box<Self>) -> Box<State>;
}
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<State> {
Box::new(PendingReview {}) // 리뷰 요청으로 인한 상태 변경
}
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<State> {
self // pending 상태에서는 상태 변경이 필요 없음
}
}
```
#### content의 동작을 변경하는 approve 메소드 추가하기
- approve 메소드를 통하여 state를 설정.
- approve 메소드를 State 트레잇에 추가
- State를 구현하는 새 구조체 Published 상태도 추가
```rust
#![allow(unused)]
fn main() {
pub struct Post {
state: Option<Box<State>>,
content: String,
}
impl Post {
// --snip--
pub fn approve(&mut self) {
if let Some(s) = self.state.take() {
self.state = Some(s.approve())
}
}
}
trait State {
fn request_review(self: Box<Self>) -> Box<State>;
fn approve(self: Box<Self>) -> Box<State>;
}
struct Draft {}
impl State for Draft {
fn request_review(self: Box<Self>) -> Box<State> {
Box::new(PendingReview {})
}
// --snip--
fn approve(self: Box<Self>) -> Box<State> {
self
}
}
struct PendingReview {}
impl State for PendingReview {
fn request_review(self: Box<Self>) -> Box<State> {
self
}
// --snip--
fn approve(self: Box<Self>) -> Box<State> {
Box::new(Published {})
}
}
struct Published {}
impl State for Published {
fn request_review(self: Box<Self>) -> Box<State> {
self
}
fn approve(self: Box<Self>) -> Box<State> {
self
}
}
```
- Post의 content 메소드를 갱신
- 상태가 Published이면, 우리는 게시물의 content 필드의 값을 반환.
- 그렇지 않다면, 우리는 빈 스트링 슬라이스를 반환
```rust
#![allow(unused)]
fn main() {
trait State {
fn content<'a>(&self, post: &'a Post) -> &'a str;
}
pub struct Post {
state: Option<Box<State>>,
content: String,
}
impl Post {
// --snip--
pub fn content(&self) -> &str {
self.state.as_ref().unwrap().content(&self)
}
// --snip--
}
}
```
- 우리가 &Box\<State\>의 content를 호출할 때, 역참조 강제는 &와 Box에 영향을 줘서 content 메소드가 궁극적으로 State 트레잇을 구현하는 타입 상에서 호출
- 우리는 빈 스트링 슬라이스를 반환하는 content 메소드의 기본 구현을 추가.
- Published 구조체는 content 메소드를 오버라이딩하고 post.content의 값을 반환.
```rust
#![allow(unused)]
fn main() {
pub struct Post {
content: String
}
trait State {
// --snip--
fn content<'a>(&self, post: &'a Post) -> &'a str {
""
}
}
// --snip--
struct Published {}
impl State for Published {
// --snip--
fn content<'a>(&self, post: &'a Post) -> &'a str {
&post.content
}
}
}
```
#### 상태 패턴의 기회비용
- 상태 패턴을 사용하면 추가 기능을 구현하기 쉬움
- 상태 패턴을 사용하지 않으면 상태가 추가될 때마다 여러 분기에 대한 케이스가 늘어남
- 상태 패턴을 사용하지 않으면 결과를 이해하기 위해 여러 곳을 전부 봐야함
- 상태 패턴 단점
- 상태가 상태 간의 전환을 구현하기 때문에, 몇몇 상태들이 서로 묶이게 됨
- 만약 우리가 PendingReview와 Published 사이에 Scheduled와 같은 상태를 추가하면, PendingReview에서 Scheduled로 전환되도록 코드를 변경해야 함
- 몇몇 로직을 중복
- 상태와 동작을 타입처럼 인코딩하기
- 이제 프로그램은 모든 게시물이 초안 게시물로 시작되고, 초안 게시물들은 그들의 내용을 출력할 능력이 없음을 보장
- 이 제약사항을 벗어나는 어떤 시도라도 컴파일러 에러로 끝나게 됨
```rust
pub struct Post {
content: String,
}
pub struct DraftPost {
content: String,
}
impl Post {
pub fn new() -> DraftPost {
DraftPost {
content: String::new(),
}
}
pub fn content(&self) -> &str {
&self.content
}
}
impl DraftPost {
pub fn add_text(&mut self, text: &str) {
self.content.push_str(text);
}
}
```
- 다른 타입으로 변환하는 것처럼 전환 구현하기
```rust
impl DraftPost {
// --snip--
pub fn request_review(self) -> PendingReviewPost {
PendingReviewPost {
content: self.content,
}
}
}
pub struct PendingReviewPost {
content: String,
}
impl PendingReviewPost {
pub fn approve(self) -> Post {
Post {
content: self.content,
}
}
}
```
### 17장 정리
- 트레잇 객체를 사용하여 몇가지 객체 지향 기능을 러스트 내에서 사용할 수 있다
## 다음 시간
### 스터디 범위
- 18. 값의 구조와 매칭되는 패턴
### 스터디 과제
- 여러 디자인 패턴 중 하나를 골라서 구현해보기
## 벌금현황
- 이지한: 0
- 김기덕: 0
- 유병조: 0
- 이명수: 0
- 이재현: 0
- 정연집: 0
- 허신: 10,000
## 회고
- 이지한: 못 올뻔 했는데, 다행히 참석했네요. crate 은 좀더 봐야겠어요.
- 김기덕: 서기를 처음하는데 힘드네요..ㅠ 앞으로 서기하시는 분들 많이 도와드리겠습니다...!
- 유병조: 객체 안전성 부분이 좀 헷갈려서 따로 찾아봐야겠네요
- 이명수: 다음부터는 2번씩 읽어 보고 와야겠어요 ㅎㅎ
- 이재현: 트레잇 부분은 볼때마다 잘 안와닿네요..
- 정연집: 앞에 트레잇부분 다시 봐야할 것 같습니다.
- 허신: