第17章: レンタルビデオシステム¶
はじめに¶
本章では、レンタルビデオシステムを通じて、価格計算、明細書生成、ポイント計算など、複雑なビジネスロジックを関数型で実装します。Strategy パターンを使って、映画の種類ごとに異なる価格計算を行います。
1. データ構造¶
映画とレンタル¶
use rust_decimal::Decimal;
/// 映画の価格コード
#[derive(Debug, Clone, PartialEq)]
pub enum PriceCode {
Regular,
NewRelease,
Children,
}
/// 映画
#[derive(Debug, Clone)]
pub struct Movie {
pub title: String,
pub price_code: PriceCode,
}
/// レンタル
#[derive(Debug, Clone)]
pub struct Rental {
pub movie: Movie,
pub days_rented: i32,
}
/// 顧客
#[derive(Debug, Clone)]
pub struct Customer {
pub name: String,
pub rentals: Vec<Rental>,
}
2. 価格計算 Strategy¶
/// 価格計算トレイト
pub trait PricingStrategy {
fn calculate_price(&self, days_rented: i32) -> Decimal;
fn calculate_points(&self, days_rented: i32) -> i32;
}
/// 通常料金
pub struct RegularPricing;
impl PricingStrategy for RegularPricing {
fn calculate_price(&self, days_rented: i32) -> Decimal {
let mut amount = Decimal::from(2);
if days_rented > 2 {
amount += Decimal::from(days_rented - 2) * Decimal::from_str("1.5").unwrap();
}
amount
}
fn calculate_points(&self, _days_rented: i32) -> i32 {
1
}
}
/// 新作料金
pub struct NewReleasePricing;
impl PricingStrategy for NewReleasePricing {
fn calculate_price(&self, days_rented: i32) -> Decimal {
Decimal::from(days_rented) * Decimal::from(3)
}
fn calculate_points(&self, days_rented: i32) -> i32 {
if days_rented > 1 { 2 } else { 1 }
}
}
/// 子供向け料金
pub struct ChildrenPricing;
impl PricingStrategy for ChildrenPricing {
fn calculate_price(&self, days_rented: i32) -> Decimal {
let mut amount = Decimal::from_str("1.5").unwrap();
if days_rented > 3 {
amount += Decimal::from(days_rented - 3) * Decimal::from_str("1.5").unwrap();
}
amount
}
fn calculate_points(&self, _days_rented: i32) -> i32 {
1
}
}
/// 価格コードから Strategy を取得
pub fn get_pricing_strategy(price_code: &PriceCode) -> Box<dyn PricingStrategy> {
match price_code {
PriceCode::Regular => Box::new(RegularPricing),
PriceCode::NewRelease => Box::new(NewReleasePricing),
PriceCode::Children => Box::new(ChildrenPricing),
}
}
3. 関数型アプローチ¶
/// レンタル料金を計算
pub fn calculate_rental_amount(rental: &Rental) -> Decimal {
match rental.movie.price_code {
PriceCode::Regular => {
let mut amount = Decimal::from(2);
if rental.days_rented > 2 {
amount += Decimal::from(rental.days_rented - 2) * Decimal::from_str("1.5").unwrap();
}
amount
}
PriceCode::NewRelease => {
Decimal::from(rental.days_rented) * Decimal::from(3)
}
PriceCode::Children => {
let mut amount = Decimal::from_str("1.5").unwrap();
if rental.days_rented > 3 {
amount += Decimal::from(rental.days_rented - 3) * Decimal::from_str("1.5").unwrap();
}
amount
}
}
}
/// ポイントを計算
pub fn calculate_rental_points(rental: &Rental) -> i32 {
let mut points = 1;
if rental.movie.price_code == PriceCode::NewRelease && rental.days_rented > 1 {
points += 1;
}
points
}
/// 顧客の合計金額を計算
pub fn calculate_total_amount(customer: &Customer) -> Decimal {
customer.rentals.iter().map(calculate_rental_amount).sum()
}
/// 顧客の合計ポイントを計算
pub fn calculate_total_points(customer: &Customer) -> i32 {
customer.rentals.iter().map(calculate_rental_points).sum()
}
4. 明細書生成¶
テキスト形式¶
/// 明細書を生成(テキスト形式)
pub fn statement(customer: &Customer) -> String {
let mut result = format!("Rental Record for {}\n", customer.name);
for rental in &customer.rentals {
let amount = calculate_rental_amount(rental);
result.push_str(&format!(
"\t{}\t{}\n",
rental.movie.title,
amount
));
}
let total_amount = calculate_total_amount(customer);
let total_points = calculate_total_points(customer);
result.push_str(&format!("Amount owed is {}\n", total_amount));
result.push_str(&format!("You earned {} frequent renter points", total_points));
result
}
HTML 形式¶
/// 明細書を生成(HTML形式)
pub fn html_statement(customer: &Customer) -> String {
let mut result = format!("<h1>Rental Record for <em>{}</em></h1>\n", customer.name);
result.push_str("<ul>\n");
for rental in &customer.rentals {
let amount = calculate_rental_amount(rental);
result.push_str(&format!(
"<li>{} - {}</li>\n",
rental.movie.title,
amount
));
}
result.push_str("</ul>\n");
let total_amount = calculate_total_amount(customer);
let total_points = calculate_total_points(customer);
result.push_str(&format!("<p>Amount owed is <strong>{}</strong></p>\n", total_amount));
result.push_str(&format!("<p>You earned <strong>{}</strong> frequent renter points</p>", total_points));
result
}
5. フォーマッター Strategy¶
/// 明細書フォーマッター
pub trait StatementFormatter {
fn format_header(&self, customer_name: &str) -> String;
fn format_rental(&self, movie_title: &str, amount: Decimal) -> String;
fn format_footer(&self, total_amount: Decimal, total_points: i32) -> String;
}
/// テキストフォーマッター
pub struct TextFormatter;
impl StatementFormatter for TextFormatter {
fn format_header(&self, customer_name: &str) -> String {
format!("Rental Record for {}\n", customer_name)
}
fn format_rental(&self, movie_title: &str, amount: Decimal) -> String {
format!("\t{}\t{}\n", movie_title, amount)
}
fn format_footer(&self, total_amount: Decimal, total_points: i32) -> String {
format!(
"Amount owed is {}\nYou earned {} frequent renter points",
total_amount, total_points
)
}
}
/// HTML フォーマッター
pub struct HtmlFormatter;
impl StatementFormatter for HtmlFormatter {
fn format_header(&self, customer_name: &str) -> String {
format!("<h1>Rental Record for <em>{}</em></h1>\n<ul>\n", customer_name)
}
fn format_rental(&self, movie_title: &str, amount: Decimal) -> String {
format!("<li>{} - {}</li>\n", movie_title, amount)
}
fn format_footer(&self, total_amount: Decimal, total_points: i32) -> String {
format!(
"</ul>\n<p>Amount owed is <strong>{}</strong></p>\n<p>You earned <strong>{}</strong> frequent renter points</p>",
total_amount, total_points
)
}
}
/// フォーマッターを使って明細書を生成
pub fn statement_with_formatter(customer: &Customer, formatter: &dyn StatementFormatter) -> String {
let mut result = formatter.format_header(&customer.name);
for rental in &customer.rentals {
let amount = calculate_rental_amount(rental);
result.push_str(&formatter.format_rental(&rental.movie.title, amount));
}
let total_amount = calculate_total_amount(customer);
let total_points = calculate_total_points(customer);
result.push_str(&formatter.format_footer(total_amount, total_points));
result
}
6. パターンの適用¶
- Strategy: 価格計算、明細書フォーマット
- ADT: PriceCode による映画種別の表現
- 不変データ: Movie, Rental, Customer
- 高階関数: map, sum による集計
まとめ¶
本章では、レンタルビデオシステムを通じて:
- Strategy パターンによる価格計算の切り替え
- ADT による映画種別の型安全な表現
- フォーマッター Strategy による出力形式の切り替え
- 純粋関数による計算ロジック
を学びました。
参考コード¶
- ソースコード:
apps/rust/part6/src/chapter17.rs
次章予告¶
次章では、並行処理システムを通じて、関数型での並行プログラミングを学びます。