Javaで考えたわたしの思う型との向き合い方
過去にweekend engineerで発表したLTの内容をまとめました.
プログラミングを初めて2年経ったので, 普段どんなことに気をつけながらプログラミングをしているかについてまとめました. 時々更新するかもしれません.
前置き
- 静的型付け言語に馴染みのない人への発表内容なので, 少々くどいかもしれません.
- 動的型付けが良い, 静的型付けが良いという話ではないので期待しないでください.
- わたしが勉強したこと, 経験したことの範疇なので, 誤りなどありましたらコメント欄に是非お願いします.
結論
型に使われたら負け.
型の力を使いこなしてこそ, 静的型付け言語のパワーを活かせていると言えます.
目次
- 静的型付けとは
- 静的型付けとの向き合い方
- データに対する意味付けをする.
- 契約に基づく安全なプログラミングをする.
- 振る舞いに制限と意味をもたせる.
静的型付けとは
静的型付けとは、プログラミング言語で書かれたプログラムにおいて、変数や、サブルーチンの引数や返り値などの値について、その型が、コンパイル時など、そのプログラムの実行よりも前にあらかじめ決められている、という型システムの性質のことである。
JavaScriptなどの動的型付け言語は, プログラムを実行する時に型をチェックします.
const str = 10;
const length = (str) => {
return str.length;
}
length(str);
// => undefinedが返る.
それに比較し, 静的型付け言語は実行するよりも前に型をチェックします.
コードレベルでは, 変数, 関数の仮引数, 戻り値の型を宣言しているところが動的型付け言語とは異なる点ですね.
Integer str = 10;
Integer length(String str) {
return str.length();
}
System.out.println(length(str));
// => コンパイル時, つまり実行前にエラーが発生する.
最近はJavaScriptなど動的型付け言語でもエディタが警告を出してくれることもありますが, それは静的構文解析の技術によるところが大きいのではないかしらと思います.
静的型付けとの向き合い方
静的型付けにはデメリットがあります. 予め変数の型を明示しないといけないとか面倒くさい. キャスト(変数の型を変換すること)するのも面倒くさい.
全部気持ちわかります. わたしもそうでした. でもそれは, わたし自身が型に使われているということを示していると最近気づきました. 静的型付け言語は, よりアグレッシブに型を定義する, わたしが型を使うことによって初めてメリットが生まれるのだと気づきました.
以下, 具体的にどういうふうに型を使っていくかを書きます.
データに対する意味付けをする.
String
やInt
などプリミティブな型を使うよりも, 自分で定義した型を使う方がプログラムでより多くのことを表現することが出来ます.
プリミティブな型をそのまま使う場合
String phoneNumber = "000-0000-0000";
自分で定義した型を使う場合
PhoneNumber phoneNumber = new PhoneNumber("000-0000-0000");
ただのString
型を使うと, その変数は文字列であるということしかわかりません.
二つ目の例のようにPhoneNumberという型の変数からは, どんな文字列が格納されているのかが予想できます. また, どのようなメソッドを呼び出すことができるかも想像できるのではないでしょうか. この場合, 何も呼び出せそうにないですが笑
契約に基づく安全なプログラミングをする.
契約プログラミングの詳細は以下の記事のほうが詳しいです.
- Java/Androidにおける例外設計、あるいは「契約による設計」によるシンプルさの追求
- [再度、契約による設計と例外について] (https://qiita.com/draftcode/items/d9c1aa0ef63b100923dd)
ここで言いたいのは, 変数に対して「ただの文字列ではなく電話番号の形式の文字列なので, 決められた命令に対しては例外を発生させることなく振る舞える」ということを保証できることにメリットがあるということです。
以下のクラスではPhoneNumberクラスのインスタンスを生成するときに、「ハイフン付きの電話番号形式の文字列」であるかどうかをチェックし, 例外を発生させています. 逆説的にPhoneNumber型の変数は必ず「ハイフン付きの電話番号形式の文字列」をデータとして持つことを型によって保証できます.
/**
* 電話番号を表すクラス.
* String
*/
class PhoneNumber {
private static final Pattern pattern =
Pattern.compile("^0\\d{2,3}-\\d{1,4}-\\d{4}$");
private final String value;
/**
* 引数に指定されたハイフン付き電話番号をもとに新しい電話番号型のインスタンスを戻す.
* @param value 電話番号
* @return 電話番号型のインスタンス
* @throws IllegalArgumentException ハイフン付き電話番号以外が引数の場合
*/
PhoneNumber(String value){
Objects.requireNonNull(value);
if (!value.matches(pattern)) {
throw new IllegalArgumentException();
}
this.value = value;
}
String getValue() {
return this.value;
}
}
振る舞いに制限と意味をもたせる
String型のような言語から提供されている型には様々なメソッドや関数が定義されています. 果たしてその全てが必要でしょうか.
電話番号に対して, 文字コードを取得したり, 文字列の順番を逆にできるのは適切でしょうか. 敢えて電話番号に対して型を定義するのは, 変数に対する操作を限定し, 間違いを防ぐためでもあります.
逆に振る舞いを増やすこともあります. 電話番号の市内局番を取得することを想定します.
String型の変数に電話番号を代入して使用するとき, 以下のようなプログラムになります.
String phoneNumber = "000-0000-0000";
String cityCode = phoneNumber.split("-")[1];
PhoneNumber型に市内局番を戻す振る舞いを定義すると, 以下のようなプログラムになります.
PhoneNumber phoneNumber = new PhoneNumber("000-0000-0000");
String cityCode = phoneNumber.getCityCode();
class PhoneNumber {
private final String value;
// 略
String getCityCode() {
return value.split("-")[1];
}
}
余談
ここまでかなり冗長にソースコードを書いてきましたが、実際にはLombok、Bean Validationのアノテーション使ってコードを簡略化することができます。
/**
* 電話番号を表すクラス.
* String
*/
@Value
class PhoneNumber {
@NotNull
@Pattern(regexp="^0\\d{2,3}-\\d{1,4}-\\d{4}$");
private final String value;
}
さいごに
ここまでかなりミクロな単位(電話番号)について書いてきましたが, 必ずしもこの単位で型を作る必要はないと思っています. 整合性の担保が必要な単位で型を作るとメリットが大きいです. この記事では, 画面からの入力値を全部ひっくるめて型として定義し, 計算ロジックの引数にしています. 型やコンストラクタによる制限と保証の手法は今後も活かしていければと.