最近、ようやくオブジェクト指向らしい実装や設計が少しだけわかってきたので、書き留めようと思います。
オブジェクト
バリューオブジェクト
値オブジェクトともいわれる
プリミティブな型を使わずにシステムで使われる単位のオブジェクトを切り出すこと
例えば、次のようなコードがあったとき,
main.java
class main {
private Integer mYear = 2020;
private void updateYear(Integer year) {
if (year != null) {
if (year >= 1850) {
if (year <= 2025) {
mYear = year;
}
}
}
}
}
ここでのYearは"生まれ年"を表していました。
その場合、このコードは次のような問題を抱えています。
- yearの判定はクラス内で何度も行うことになるかも
- yearはなんの”年”を表しているかわからない
- 値が直接書かれている
- ネストが深い
そこで、バリューオブジェクトを適用しつつリファクタリングしてみる
main_mod.java
class BirthYear {
private static int DEFAULT_YEAR = 2020;
private static int MIN_YEAR = 1850; // 生値の利用をやめる
private final int mYear; // final で定義することで、書き換え不能にする(イミュータブルな値を担保する)
private BirthYear(final Integer year) {
// コンストラクタで異常な値を検知する
if (year == null) {
throw new IllegalArgumentException("null value");
}
if (year < MIN_YEAR) {
throw new IllegalArgumentException("Under min year");
}
final int currentYear = generateCurrentYear();
if (year > currentYear) {
throw new IllegalArgumentException("future year");
}
// 生まれ年としての正しさが担保された値のみが保持される
mYear = year;
}
private int generateCurrentYear() {
final Date currentDate = new Date();
return currentDate.getYear();
}
// コンストラクタではなく、生成するためのスタティックメソッドを用意する
public static BirthYear fromInt(final Integer year) { // 引数にはfinalをつける
return new BirthYear(year);
}
// これでデフォルト値も隠ぺいできる
public static BirthYear createDefault() {
return new BirthYear(DEFAULT_YEAR);
}
public int getValue() {
return mYear;
}
}
class main {
private BirthYear mBirthYear = BirthYear.createDefault();
private void updateBirthYear(final Integer year) {
try {
final BirthYear birthYear = BirthYear.fromInt(year);
mBirthYear = birthYear;
} catch(final IllegalArgumentException exception) {
// update failure
}
}
}
ファーストクラスコレクション
いわゆる配列もプリミティブ型の一つと捉えて、システムで使われる塊として作る
class MyApp {
List<Integer> mBirthYearList = new ArrayList<>();
public MyApp(List<Integer> birthYearList) {
mBirthYearList = birthYearList;
}
// 成人していて、平均より高齢の人だけにメールを送る
public void sendMailForHigherAdult() {
final List<Integer> adultList = createAdultList();
final Integer average = calcAverage(adultList);
for (final Integer birthYear : adultList) {
if (average > birthYear) {
// Send Mail
System.out.println("SendAdult" + birthYear);
}
}
}
private List<Integer> createAdultList() {
final List<Integer> result = new ArrayList<>();
for (final Integer birthYear : mBirthYearList) {
if ((2025 - birthYear) > 20) {
result.add(birthYear);
}
}
return result;
}
private Integer calcAverage(List<Integer> orgList) {
Integer sum = 0;
for (final Integer birthYear : orgList) {
sum += birthYear;
}
return sum / orgList.size();
}
}
public class Main {
public static void main(String[] args) throws Exception {
List<Integer> birthYearList = new ArrayList<>();
birthYearList.add(2030);
birthYearList.add(2020);
birthYearList.add(2010);
birthYearList.add(2000);
birthYearList.add(1990);
birthYearList.add(1980);
birthYearList.add(1970);
MyApp app = new MyApp(birthYearList);
app.sendMailForHigherAdult();
}
}
配列に対する整形や計算をprivateメソッドで対応していますが、ファーストクラスコレクションを導入することで本質的な個所のみが残り可読性が上がります。
class BirthYear {
private static int DEFAULT_YEAR = 2020;
private static int MIN_YEAR = 1850;
private static int ADULT_LIMIT = 20;
private final int mYear;
private BirthYear(final Integer year) {
// コンストラクタで異常な値を検知する
if (year == null) {
throw new IllegalArgumentException("null value");
}
if (year < MIN_YEAR) {
throw new IllegalArgumentException("Under min year");
}
final int currentYear = generateCurrentYear();
if (year > currentYear) {
throw new IllegalArgumentException("future year:" + currentYear);
}
mYear = year;
}
private int generateCurrentYear() {
// final Date currentDate = new Date();
// return currentDate.getYear();
return 2025;
}
public static BirthYear fromInt(final Integer year) {
return new BirthYear(year);
}
public static BirthYear createDefault() {
return new BirthYear(DEFAULT_YEAR);
}
public boolean isAdult() {
final int age = generateCurrentYear() - mYear;
if (age >= ADULT_LIMIT) {
return true;
}
return false;
}
public int getValue() {
return mYear;
}
}
class BirthYearList {
final List<BirthYear> mBirthYearList;
private BirthYearList(final List<BirthYear> yearList) {
if (yearList == null) {
throw new IllegalArgumentException("null value");
}
mBirthYearList = yearList;
}
public static BirthYearList fromIntList(final List<Integer> yearList) {
final List<BirthYear> birthYearList = new ArrayList<>();
for (final Integer year : yearList) {
try {
BirthYear birthYear = BirthYear.fromInt(year);
birthYearList.add(birthYear);
} catch(final IllegalArgumentException ignore) {
// 無視する
}
}
return new BirthYearList(birthYearList);
}
public BirthYearList getAdultList() {
final List<BirthYear> adultList = new ArrayList<>();
for (final BirthYear birthYear : mBirthYearList) {
if (birthYear.isAdult()) {
adultList.add(birthYear);
}
}
return new BirthYearList(adultList);
}
public BirthYearList getUnderThanList(int limitValue) {
final List<BirthYear> limitList = new ArrayList<>();
for (final BirthYear birthYear : mBirthYearList) {
if (limitValue > birthYear.getValue()) {
limitList.add(birthYear);
}
}
return new BirthYearList(limitList);
}
public int getAverage() {
int sum = getSum();
int size = mBirthYearList.size();
if (size <= 0) {
return 0;
}
return sum / mBirthYearList.size();
}
private int getSum() {
int sum = 0;
for (final BirthYear birthYear : mBirthYearList) {
sum += birthYear.getValue();
}
return sum;
}
public List<BirthYear> getList() {
return mBirthYearList;
}
}
class MyApp {
final BirthYearList mBirthYearList;
public MyApp(List<Integer> birthYearList) {
mBirthYearList = BirthYearList.fromIntList(birthYearList);
}
// 成人していて、平均より高齢の人だけにメールを送る
public void sendMailForHigherAdult() {
final BirthYearList adultList = mBirthYearList.getAdultList();
final int average = adultList.getAverage();
final BirthYearList limitList = adultList.getUnderThanList(average);
for (final BirthYear birthYear : limitList.getList()) {
// Send Mail
System.out.println("SendAdult:" + birthYear.getValue());
}
}
}