LoginSignup
4
1

More than 1 year has passed since last update.

良いコード/悪いコードで学ぶ設計入門(ミノ駆動本)のMoneyクラスをC++で書く

Posted at

はじめに

良いコード/悪いコードで学ぶ設計入門 ―保守しやすい 成長し続けるコードの書き方 を読みました。
ありがちな悪いコードを良いコードに直していくというテーマでオブジェクト指向の設計が学べ、初心者にもおすすめできる内容でした。
1点、この本のサンプルコードがJavaなのですが、業務では主にC++を使っているためそのまま適用というわけにはいきません。Javaを知らない人におすすめするときも、Javaという理由だけで敬遠してほしくないため、C++で実践するとどうなるかなというのを考えてみました。
書籍の第3章「クラス設計」ではサンプルコードとしてMoneyというクラスを作成しています。これをC++で書きます。本当は全部のサンプルコードをC++で書き直して解説したかったのですが、力尽きたのでMoneyクラスだけになりました。。

JavaとC++の違い

文法は似ており、どちらもオブジェクト指向の書き方が可能です。
異なる点はたくさんあるのですが、説明に必要な部分の違いだけ挙げていきます。

用語の違い

C++ Java 補足
メンバ変数 インスタンス変数
メンバ関数 メソッド C++でもメソッドと言いますが。
nullptr null
const final 不変(イミュータブル)にするために使います。

ガベージコレクタ(GC)の有無

JavaにはGCがありますが、C++はGCがありません。ここが大きな違いです。
C++ではnewしたメモリは自分で責任を持ってdeleteしなければなりません。
unique_ptrなどのスマートポインタを使えばメモリ管理の手間は少し軽減されますが、今度は所有権などを気にしなければいけなくなります。

MoneyクラスをC++で書く

書籍の第3章「クラス設計」ではサンプルコードとしてMoneyというクラスを作成しています。これをC++で書きます。

Moneyクラス「悪いコード」

まず悪いコードとしてリスト3.1のコードが示されています。

書籍サンプル リスト3.1 金額を表すクラス
import java.util.Currency;

class Money {
  int amount;
  Currency currency;
}

Currencyというクラスが出てきます。これはJavaのjava.util.Currencyという通貨を表現するライブラリのクラスのようです。C++でこのようなクラスを定義してもいいのですが、簡単のためenum classとして定義しておきます。
リスト3.1をC++で書いてみたものが以下です。

リスト3.1 C++版
enum class Currency {
  YEN,
  DOLLAR,
};

class Money {
public:
  int m_amount;
  Currency m_currency;
};

分かりやすさのため、メンバ変数に接頭辞m_をつけています。また、ここではまだ「悪いコード」なので、メンバ変数がすべてpublicです。

Moneyクラス「良いコード」

Java版

改善されたJava版はこちら

リスト3.18 関連ロジックを凝集した変更に強いMoneyクラス
import java.util.Currency;

class Money {
  final int amount;
  final Currency currency;

  Money(final int amount, final Currency currency) {
    if (amount < 0) {
      throw new IllegalArgumentException("金額が0以上でありません。");
    }
    if (currency == null) {
      throw new NullPointerException("通貨を指定してください。");
    }

    this.amount = amount;
    this.currency = currency;
  }

  Money add(final Money other) {
    if (!currency.equals(other.currency)) {
      throw new IllegalArgumentException("通貨単位が違います。");
    }

    final int added = amount + other.amount;
    return new Money(added, currency);
  }
}

C++版

できるだけJavaのコード近い形で C++で書いてみるとこのようになります。

MoneyクラスのC++版
enum class Currency {
  YEN,
  DOLLAR,
};

class Money
{
  const unsigned int m_amount;
  const Currency m_currency;
public:
  explicit Money(unsigned int amount, Currency currency)
    : m_amount(amount), m_currency(currency)
  {
  }
  Money add(const Money& other) const
  {
    if (m_currency != other.m_currency)
    {
      throw std::invalid_argument("通貨単位が違います");
    }
    const unsigned int added = m_amount + other.m_amount;
    return Money(added, m_currency);
  }
};

Java版との違い

  • C++ではunsignedが使えるので、負の値をとらないm_amountunsigned intとしました。その関係でコンストラクタでの非負チェックと例外送出が不要になりました。
  • C++でメンバ変数をconstにするとコンストラクタの初期化子でしか初期化できないのでそうしました。
  • コンストラクタでCurrencyを値渡しにしたのでJava版で行っているNullチェックは不要になりました。
    • Currencyクラスがコピーにコストがかかるとしても、ポインタ渡しではなく参照渡し(const参照渡し)にすればどちらにせよNullチェックは不要ですね。
  • メンバ関数add()はconstメンバ関数にできました。
    • この本で触れられている「不変」の考えを徹底すると、クラスのすべてのメンバ関数がconstメンバ関数になりますね。

個人的に気になる点

上記ではJavaのコードにできるだけ近い形でC++コードを書きましたが、気になる点がいくつかあります。

  1. 例外の使用。
    C++では例外を使わない規約になっているチームも多いのではないでしょうか。
  2. add()関数で実体を返している。実体を返すとメモリのコピーが発生するためパフォーマンスが気になる。
    パフォーマンスの話をするとそもそもadd()関数で新しいインスタンスを生成して返すというのが気になってしまいますが、これを許せないとなるとクラスをイミュータブルにすることができなくなり、コンセプトから外れてしまうためインスタンス生成は許すことにします。

気になる点を改善したC++コード

MoneyクラスのC++版 改善版
class Money
{
  const unsigned int m_amount;
  const Currency m_currency;
public:
  explicit Money(unsigned int amount, Currency currency)
    : m_amount(amount), m_currency(currency)
  {
  }
  std::unique_ptr<Money> add(const Money& other) const
  {
    if (m_currency != other.m_currency)
    {
      return nullptr;
    }
    const unsigned int added = m_amount + other.m_amount;
    return std::make_unique<Money>(added, m_currency);
  }
};

改善した点は以下の2つです。

  1. add()関数の戻り値の型をstd::unique_ptr<Money>とすることで、戻り値を取得する際のメモリコピーを削減。
  2. Currencyが異なる場合はnullptrを返すことで、例外を排除。

1を実施しないと2を実施できないのですが、そこは仕方なく。

改善はしたもののイマイチな点

  1. add()関数の戻り値がポインタになってしまったので使用側でnullチェックが必要になってしまった。
    まあここはもともと例外チェックをしていたので仕方ないかと思います。
  2. 実は改善前のようにreturnで値を返すようにしてもメモリコピーは発生しないらしい。
    NRVO(Named Return Value Optimization) という最適化が働いて、コピーが発生しないように最適化されるとのことです。
    このコードを作る過程でいろいろ調べていたら発見しました。知りませんでした。。

2があるため戻り値をポインタにする必要がなかったということになります。するとやはり例外が必要か?ということになります。
が、落ち着いて考えると通貨の違うお金を足そうとすること自体がよくないので、通貨ごとに型を変えるとか、m_amountは統一した通貨で格納するとかが必要なのかもしれません。

おわりに

書籍で紹介された、Moneyクラスのadd関数さえイミュータブルにするという方法は目から鱗でした。
正直なところやりすぎ感は否めませんが、設計時の選択肢が増えたということにしておきます。

4
1
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
1