Rustを0から学んでみた 〜Part.2〜 trait / modの管理方法 編(TDD Boot Camp 課題2「開区間」)

「TDDBCの課題をRustで実施し、言語仕様を学んでいこう!」というシリーズの記事です。

こちらの記事読了が前提の内容となっております。

learn-rust-part1-struct-impl-test-thumb
Rustを0から学んでみた〜Part.1〜 struct, impl, テストコードの基礎編(TDD Boot Camp 課題1「閉区間」)


[ひとことで言うとこんな記事]

以下2点を学ぶ事ができます

  • trait(トレイト)を利用した、共通の振る舞いの定義
  • 基本的なmod(モジュール)の構成とファイル配置方法

[こんなひとにおすすめ]

  • 「これからRustを勉強していきます!」という方
  • 「Rustってclassないの?!」と驚いている方
  • Rustの初歩的なモジュール構成と配置方法を知りたい方

[目次]

  • 課題2: 整数の開区間(Open Range)
  • 学んだこと 1:trait(トレイト)を利用した、共通の振る舞いの定義
  • 学んだこと 2:モジュール(mod)の構成とファイル配置方法
  • 学んだこと その他:cargoがテストコードを認識するのは、tests配下のみ
  • 今回の実装の Pull Request
  • 次回:TDDBC「課題3:閉区間と開区間」 実施予定

課題2: 整数の開区間(Open Range)

課題2-1

  • 下端点と上端点を与えて開区間(new)生成
  • 開区間から下端、上端の取得
  • 開区間から文字列(to_string)取得

課題2-2

  • 開区間が指定した整数を含むか(contains)判定

課題2-3

  • 開区間が別の開区間と等しいか(equals)判定
  • 開区間が別の開区間と接続しているか(isConnectedTo)判定

学んだこと1:trait(トレイト)を利用した、共通の振る舞いの定義

今回の課題2を見てすぐに気がつくことは、 課題1と問題文が全部一緒 ってことですかね。違うのは「閉区間か開区間か」ってところです。

learn-rust-part2-trait-module-01

見ての通り、メンバ変数と振る舞いがまったく同じ種類用意することとなるので、共通定義しておいたほうが楽だろうと思った次第です。

こういうときに利用できるものとして、RustではTraitがございます。

参考:トレイト - Rust By Example 日本語版

Trait(トレイト)の利用方法

トレイトの準備:Rangeとして、閉区間と開区間での共通の振る舞いを定義

pub trait Range {
  fn new(lower: i8, upper: i8) -> Self;
  fn to_string(&self) -> String;
  fn contains(&self, number: i8) -> bool;
  fn equals(&self, range: Self) -> bool;
  fn is_connected_to(&self, range: Self) -> bool;
}

共通の振る舞いを上記のように定義しました。

Selfと記述している部分は、new()equals()等の関数は閉区間は閉区間、開区間は開区間を返却するのでそれを示したものとなります。

トレイトの利用:閉区間、開区間の実装

閉区間

// メンバ変数をtraitに定義することはできないそうです
#[derive(Clone)]
pub struct ClosedRange {
  pub lower: i8,
  pub upper: i8,
}

impl Range for ClosedRange {
  fn new(lower: i8, upper: i8) -> ClosedRange {  // Selfを返却
    /**
     * 〜実際の実装〜
     */
  }
}

開区間

// メンバ変数をtraitに定義することはできないそうです
#[derive(Clone)]
pub struct OpenRange {
  pub lower: i8,
  pub upper: i8,
}

impl Range for OpenRange {
  fn new(lower: i8, upper: i8) -> OpenRange {  // Selfを返却
    /**
     * 〜実際の実装〜
     */
  }
}

上記のように閉区間、開区間共に実装(impl)を準備してあげればOKです。

もちろん 未実装の振る舞いがある場合にはコンパイルで怒られますので、安心 ですね。

実行:実際にmain.rsで閉区間、開区間を利用する

use tddbc_rust_practice::range::closed_range::ClosedRange;
use tddbc_rust_practice::range::open_range::OpenRange;
use tddbc_rust_practice::range::Range; // Trait自体もuseしてあげないとfnが見つからない

fn main (){
  let sample_closed_range = ClosedRange::new(1, 5);
  let sample_open_range = OpenRange::new(1, 5);
}

Traitとして振る舞いを定義したもの自体もuseしてあげないと、コンパイル時に振る舞いが見つからずにエラー となります。

学んだこと2:モジュール(mod)の構成とファイル配置方法

膨れ上がっていくlib.rs

OpenRange,ClosedRangeを実装した初期のlib.rsが以下のようなものになっています。

GitHub - h0ng0yut0 tddbc-rust-practice/lib.rs

93行。ちょっと煩雑な気もしてきました。

RangeとOpenRange,ClosedRangeのモデル通りにモジュール分割

learn-rust-part2-trait-module-01

上記モデルを踏まえて、親 Range、子 ClosedRange OpenRangeの形でモジュール管理できればいいかなと考えました。

src配下のディレクトリ構成です。

.
├── lib.rs
├── main.rs
├── range
│   ├── closed_range.rs
│   └── open_range.rs
└── range.rs

lib.rs (module準備の大本となるファイル)では、rangeを呼び出すだけ

pub mod range;

range.rs には、共通の振る舞いを定義したtraitと、ClosedRangeとOpenRange の実装ファイルを呼び出しています。

pub mod closed_range;
pub mod open_range;

pub trait Range {
  fn new(lower: i8, upper: i8) -> Self;
  fn to_string(&self) -> String;
  fn contains(&self, number: i8) -> bool;
  fn equals(&self, range: Self) -> bool;
  fn is_connected_to(&self, range: Self) -> bool;
}

closed_range.rsopened_range.rs には実際の実装がなされています。

closed_rangeの例

// Range trait を利用することを明示
use crate::range::Range;

#[derive(Clone)]
pub struct ClosedRange {
  pub lower: i8,
  pub upper: i8,
}

impl Range for ClosedRange {
  /*
   * 実際の実装
   */
}

関連するモジュールのRustファイルの準備とファイル構成を、階層的に管理していくことが求められるそうです。

こちらのサイトに詳しいまとめ等が載っています。

参考:Rustのモジュールの使い方 | κeenのHappy Hacκing Blog

学んだこと その他:cargoがテストコードを認識するのは、tests配下のみ

testsディレクトリ配下も、モジュールと同じ構成にしてみたのですが、全くテストが走らず。。。

結果、落ち着いた構成は以下のような感じです。

.
├── Cargo.lock
├── Cargo.toml
├── README.md
├── src
│   ├── lib.rs
│   ├── main.rs
│   ├── range
│   │   ├── closed_range.rs
│   │   └── open_range.rs
│   └── range.rs
└── tests
    ├── closed_range.rs ← ディレクトリを掘れず tests 配下
    └── open_range.rs ← ディレクトリを掘れず tests 配下

今回の実装の Pull Request

課題2:開区間 by h0ng0yut0 · Pull Request #2 · h0ng0yut0/tddbc-rust-practice

次回:TDDBC「課題3:閉区間と開区間」 実施予定

今回は、trait, modとその管理方法 を学べました。次回の課題は何が学べるのか楽しみです。


[これから読んでみたい本]


一緒に様々なことを学んでいく仲間を募集しています。

このサイトのお問い合わせなどからご連絡いただけると幸いです。