19
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

過去に経験したシステムがなぜ苦しかったか(現場で役立つシステム設計の原則~第1章 を読んで)

Last updated at Posted at 2020-01-20

はじめに

この本の第1章についてです。
書籍情報現場で役立つシステム設計の原則

前職はSIerで働いてましたが、 変更が苦しくて危険なシステム の保守ばかり経験した私にとって、この本を読んだ時はとても感動しました。なぜかというと、本書に記載の、変更が大変な理由やコード例が、今まで携わったシステムにとても当てはまっていたからです。

Qiita内でも既に沢山の人が話題にしてますが、今回は、社内勉強会用に第1章についてだけ、過去のシステムのエピソードを交えながらまとめてみました。内容を取り間違えしないよう、めじるしとして、アイコンをつけて区別してます。

  • 変更が苦しくて危険に関する記述... :skull:
  • 変更が楽で安全に関する記述... :sparkles:

ソフトウェアの設計とは

:skull: 設計に問題があるシステム

  • どこに何が書いてあるか理解するまでコードをじっくり調べる必要がある
  • ちょっとした修正なのに、変更すべき箇所があちこちに散らばっている
  • 広い範囲のテストが必要になる
  • 慎重に修正したはずなのに、思わぬ副作用に苦しむ

過去に苦しかったシステムはこれに全部あてはまってました。。。:sob:

:sparkles: 上手く設計されたシステム

  • 変更が :sparkles: 楽で安全 :sparkles:
  • 変更すべき箇所がかんたんにわかる
  • 変更するコード量が少ない
  • 変更の影響範囲を狭い範囲に限定できる

プログラムの修正に3日かかるか、半日で済むか、その違いを生むのが「設計」なのです

:skull: 変更が苦しくて危険なシステム

変更が大変なクラスの特徴

  • メソッドが長い
  • クラスが大きい
  • 引数が多い

私が過去に携わってたシステムは、数百行のメソッド、数千行のクラスが普通にありました :sob:

:skull: 変更が苦しくて危険なシステムに成長した理由

:skull: ちょっとしたコードの修正や機能の拡張を繰り返すから

アプリケーションを無事リリースできて、利用者が使い始めると、

  • さまざまな改善要望がでてくる
  • 発見された不具合の修正も必要

そのたびに、

  • メソッドが数行だけ長くなる
  • クラスが少し膨らみ
  • 引数がじわじわと増える

その結果、
どこに何が書いてあるか、時間とともにわかりづらくなっていく

それの積み重ねでシステム改修時の、コードの解析:実装の比率が、9:1くらいのシステムに成長してました :sob:

数文字とか数行の変更で要件を満たすことができる場合、リファクタリングによる影響調査等の工数や、修正箇所が増えることよりも、数文字か数行の変更の方をとっていたのです。

:sparkles: 変更が楽になる書き方

:sparkles: わかりやすい名前をつかう

業務で使っている言葉をそのままプログラム要素の名前に使うことで、プログラムの変更が :sparkles: 楽で安全 :sparkles: になります。

int a; // ✕ 1文字の変数名
int qty; // △ 省略した変数名
int quantity; // ◎ 業務で使われる単語を使った変数名
  • :skull: 1文字の変数名

    • 何を意味しているかわからない。
  • :skull: 省略した変数名

    • プログラムの前後で意味を推測できる。
    • しかし、日常会話や文章で使う表現からかけ離れている。
    • 略語はいろいろな意味に解釈できる為、正しい意味の推測に時間がかかる。
    • 人によって意味の取り違えが起きそう。
  • :sparkles: 業務で使われる単語を使った変数名

    • どのような業務の為に何をしているかわかりやすい。
    • 改修箇所の調査が簡単で確実になる。

:sparkles: 目的ごとに変数を用意する

「説明用の変数」を導入し、「破壊的な代入」がなくなれば、改修時の影響範囲が減ります。

  • :skull: 破壊的代入 ... 一つの変数を使いまわして代入を繰り返す書き方

    • ローカル変数を複数の目的で使い回すと、途中で何をやっているかわかりにくくなる。
    • 変更の影響範囲が広がる
  • :sparkles: 説明用の変数の導入 ... 目的別に専用のローカル変数を用意し、コードの意図を変数名で説明する。

    • 段落ごとの独立性が高まる。
    • 変更の影響範囲を局所化できる。

:sparkles: 長いメソッドは段落にわけて読みやすくする

前後の行と意味が異なる場所を見つけたら、空白行を追加して 段落に分ける と一つひとつの段落の独立性が高くなります。
変更の対象範囲が特定しやすくなります。

:sparkles: メソッドとして独立させる (メソッドの抽出

段落 に埋もれていたロジックやデータをメソッドに独立させ、変更箇所を1ヶ所に閉じ込めます。
重複をなくし、変更箇所を1ヶ所に閉じ込めたロジックの変更は、 :sparkles: 楽で安全 :sparkles: になります。

:sparkles: 異なるクラスに重複したコードをなくすリファクタリング

2つのクラスの重複コードを解消する手順は以下になります。

  1. それぞれのクラス内で、重複コードを、メソッドに抽出する。
  2. 2つのクラスに参照関係がある場合 ... 参照する側で抽出したメソッド呼び出しを、参照先のオブジェクトのメソッド呼び出しに書き換える。
  3. 2つのクラスに参照関係がない場合 ... 共通のメソッドの置き場所として、別クラスを新たに作成し、元のクラスで抽出したメソッドを移動する。
  4. 元の2つのクラスのメソッド呼び出しを、それぞれの新しいクラスの共通メソッドを利用するよう、書き換える。

過去に携わってた大規模なシステムの改修で印象に残っているのが、クラスまたいで数千ヶ所に散らばったif判定処理に、||(または)をつけていく作業があり辛かったことがありました。専用クラスを作らず、ロジックを一箇所に閉じ込めていなかったからなんですね。。。

:sparkles: 狭い関心事に特化したクラスにする

業務で使われる用語に合わせて、その用語の関心事に対応するクラスをドメインオブジェクトと呼びます。

  • 業務を理解するために要求を分析し、そこで発見した業務の関心事の単位を、そのままプログラミング単位としてクラスで表現する。
  • 分析で発見した業務の構造とプログラムの構造が一致していれば、変更が :sparkles: 楽で安全 :sparkles: になります。

:sparkles: 小さなクラスでわかりやすく安全に

データとロジック

業務アプリケーションは、基本データ型とそれを使った 判断/加工/計算 のロジックを最小単位として、それらを組み合わせたものです。

  • 業務アプリケーションで演算の対象になる基本データ型

    • 数値 ... ID、金額
    • 日付 ... 登録日、開始日、終了日
    • 文字 ... 氏名、備考
  • 業務に使うデータと業務ロジックの例

    • 金額 ... 合計、3桁ごとにカンマをつける など
    • 開始日 ... 終了日より過去、ハイフン区切りで表示、西暦・和暦 表示など
    • 氏名 ... 苗字と名前を半角スペースでつなぐ、最大文字数 など

:sparkles: 「値」を扱うための専用クラスをつくる

値を扱う為の専用クラスを作るやり方を 値オブジェクト(Value Object) と呼びます。
値オブジェクトとは、基本データ側のインスタンス変数を1つか2つ持つだけの小さなクラスです。

過去のシステム経験で、クラス内でidとかnameっていうフィールドを持つ場合は、基本データ型で持つものと思ってたので、専用クラスをつくるやり方を知った時はとても衝撃をうけました。
今までやってた書き方と値オブジェクトの書き方の例を以下に記載します。

例:学校クラス(School)

:skull: 今までやってきた書き方

  • Schoolクラスの中にint型のidとString型のnameのフィールドと、各setterとgetterのみがあるクラスを作っていた。
  • 文字数チェックなどのロジックは別クラスに記述していた。

学校クラス(School)

public class School {

    private int id;
    private String name;

    public School(int id, String name) {
        this.id = id;
        this.name = name;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getId() {
        return this.id;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getName() {
        return this.name;
    }
}

:sparkles: 値オブジェクトの書き方

idとかnameは基本データ型ではなく、id用の SchoolId、name用の SchoolName という専用クラスをつくってSchoolクラスのフィールドに設定します。

  • クラス名を業務で扱う名称、業務上の判断や計算に使う用語と一致させる。
  • 変更が必要になったとき、クラス名と業務の用語が一致していればプログラム上で変更が必要な箇所を直感的に特定できる。
  • コンストラクタ、メソッドの引数を専用クラス型にすることにより、引数の入れ間違いを防ぐ。
  • 学校名称として妥当な長さや文字種のルールを明示的に記述し、業務的に不適切な値が混入することを防ぐ。

学校名クラス(SchoolName)

public class SchoolName {

    static final int MIN_LENGTH = 1;
    static final int MAX_LENGTH = 50;

    private String value;

    public SchoolName(String value) {
        if (value.length() < MIN_LENGTH || value.length() > MAX_LENGTH) {
            // 例外スロー
        }
        this.value = value;
    }

    public String value() {
        return value;
    }
}

※SchoolIdクラスも同じような感じなのでコード省略します

学校クラス(School)

public class School {
    // フィールドの型を専用クラスに
    private SchoolId id;
    private SchoolName name;

    // コンストラクタ引数を専用クラスに
    public School(SchoolId id, SchoolName name) {
        this.id = id;
        this.name = name;
    }
    
    public SchoolId id() {
        return id;
    }

    public SchoolName name() {
        return name;
    }
}

:sparkles: 値オブジェクトは「不変」にする(完全コンストラクタ)

完全コンストラクタにすると、プログラムの途中で内部の値が変化するときに起きがちな副作用を防ぐことができます。

  • インスタンス変数はコンストラクタでオブジェクトの生成時に設定する
  • インスタンス変数を変更するsetterメソッドを作らない
  • 別の値が必要であれば、別のインスタンスを作る

:skull: 過去に経験したjavaシステムで、学校とか生徒などのモノを表すクラスは、基本データ型のフィールドとsetterとgetterのみが定義されたクラスを作るっていうのがどこの現場でも定石でした。
(その理由が本書のP75にコラムに記載がありました。)

:sparkles: コレクション型を扱うロジックを専用クラスに閉じ込める

:skull: 配列やコレクションを扱うコードは複雑になりがち

  • プログラムのある場所でコレクションに要素を追加する
  • 別の場所で、同じコレクションの要素を削除する
  • さらに別の場所でコレクションの要素のを書き換える

↑のように

プログラムのあちこちで add() とか remove() ができてしまうと、変更の影響範囲が把握できなくなり、期待した動作にならなかったり思わぬ副作用が起きがちです。

:skull: 今までやってきた書き方

学校に属する生徒一覧を例にしてみます。

学校の中に生徒リストを直接作ってました。
生徒リストを参照や追加する場合、 getList()add() メソッドでリストを直接操作します。

public class School {
  private int id;
  private String name;
  private List<Student> students;

  // 以降にコンストラクタ、setterとgetterがある想定
  ...
}

:sparkles: 専用クラスをつくる書き方

コレクション型を扱う専用クラスをつくります。
変更の影響範囲をコントロールするために、コレクションの変更は、専用クラス内でおこないます。

生徒一覧用の専用クラスをつくり、学校クラスの中に生徒一覧クラスのフィールドを追加します。

生徒一覧クラス(Students)

public class Students {

    private List<Student> list;

    public Students(List<Student> list) {
        this.list = list;
    }

    ...

    public Students add(Student student) {
        // 要素を変化させる操作はStudentsオブジェクト内でやる
        List<Student> result = new ArrayList<>(this.list);
        return new Students(result.add(students));
    }
}

↑の add() のように

要素を変化させる操作をしても別オブジェクトになるので、内部のコレクションの状態が変化しない不変スタイルのオブジェクトになります。副作用が起きにくくなりプログラムの動作が安定します。

学校クラス(School)

public class School {

    private Schoold id;
    private SchoolName name;
    private Students students;

    public School(SchoolId id, SchoolName name, Students students) {
        this.id = id;
        this.name = name;
        this.students = students
    }

    ...

    public Students students() {
        // List<Student>ではなく、生徒一覧の専用クラスのオブジェクトを返す
        return this.students;
    }
}

第1章のまとめ

本のままですが、まとめです。

  • オブジェクト指向設計は変更を :sparkles: 楽で安全 :sparkles: にする工夫
  • コードの整理の基本は名前と段落
  • 短いメソッド、小さなクラスを使ってコードを整理する
  • 値オブジェクトでわかりやすく安全にする
  • コレクションオブジェクトで、複雑なロジックを集約して整理する
  • クラス名やメソッド名と業務の用語が一致するほど、プログラムの意図がわかりやすくなり、変更が :sparkles: 楽で安全 :sparkles: になる

さいごに

システムの変更が苦しくて危険と感じている方はぜひオススメしたい本です。:cat:

19
14
0

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
19
14

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?