はじめに
良いコード/悪いコードで学ぶ設計入門 ―保守しやすい 成長し続けるコードの書き方 を読みました。
ありがちな悪いコードを良いコードに直していくというテーマでオブジェクト指向の設計が学べ、初心者にもおすすめできる内容でした。
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のコードが示されています。
import java.util.Currency;
class Money {
int amount;
Currency currency;
}
Currency
というクラスが出てきます。これはJavaのjava.util.Currency
という通貨を表現するライブラリのクラスのようです。C++でこのようなクラスを定義してもいいのですが、簡単のためenum class
として定義しておきます。
リスト3.1をC++で書いてみたものが以下です。
enum class Currency {
YEN,
DOLLAR,
};
class Money {
public:
int m_amount;
Currency m_currency;
};
分かりやすさのため、メンバ変数に接頭辞m_
をつけています。また、ここではまだ「悪いコード」なので、メンバ変数がすべてpublicです。
Moneyクラス「良いコード」
Java版
改善されたJava版はこちら
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++で書いてみるとこのようになります。
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_amount
をunsigned int
としました。その関係でコンストラクタでの非負チェックと例外送出が不要になりました。 - C++でメンバ変数をconstにするとコンストラクタの初期化子でしか初期化できないのでそうしました。
- コンストラクタでCurrencyを値渡しにしたのでJava版で行っているNullチェックは不要になりました。
- Currencyクラスがコピーにコストがかかるとしても、ポインタ渡しではなく参照渡し(const参照渡し)にすればどちらにせよNullチェックは不要ですね。
- メンバ関数
add()
はconstメンバ関数にできました。- この本で触れられている「不変」の考えを徹底すると、クラスのすべてのメンバ関数がconstメンバ関数になりますね。
個人的に気になる点
上記ではJavaのコードにできるだけ近い形でC++コードを書きましたが、気になる点がいくつかあります。
- 例外の使用。
C++では例外を使わない規約になっているチームも多いのではないでしょうか。 - add()関数で実体を返している。実体を返すとメモリのコピーが発生するためパフォーマンスが気になる。
パフォーマンスの話をするとそもそもadd()関数で新しいインスタンスを生成して返すというのが気になってしまいますが、これを許せないとなるとクラスをイミュータブルにすることができなくなり、コンセプトから外れてしまうためインスタンス生成は許すことにします。
気になる点を改善した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つです。
-
add()
関数の戻り値の型をstd::unique_ptr<Money>
とすることで、戻り値を取得する際のメモリコピーを削減。 - Currencyが異なる場合はnullptrを返すことで、例外を排除。
1を実施しないと2を実施できないのですが、そこは仕方なく。
改善はしたもののイマイチな点
-
add()
関数の戻り値がポインタになってしまったので使用側でnullチェックが必要になってしまった。
まあここはもともと例外チェックをしていたので仕方ないかと思います。 - 実は改善前のように
return
で値を返すようにしてもメモリコピーは発生しないらしい。
NRVO(Named Return Value Optimization) という最適化が働いて、コピーが発生しないように最適化されるとのことです。
このコードを作る過程でいろいろ調べていたら発見しました。知りませんでした。。
2があるため戻り値をポインタにする必要がなかったということになります。するとやはり例外が必要か?ということになります。
が、落ち着いて考えると通貨の違うお金を足そうとすること自体がよくないので、通貨ごとに型を変えるとか、m_amount
は統一した通貨で格納するとかが必要なのかもしれません。
おわりに
書籍で紹介された、Moneyクラスのadd関数さえイミュータブルにするという方法は目から鱗でした。
正直なところやりすぎ感は否めませんが、設計時の選択肢が増えたということにしておきます。