Encapsulation with Package in Java
現在 Android を開発していて、色々なプロジェクトをみていると設計が考えられてない物が多く、「糞コード」と発狂することが度々あります。
しかし、なぜ「糞コード」だと論理的に説明する事は、なかなか難しいものです。
「糞コード」が生まれてしまう理由の一点としては、Web の便利な MVC フレームワークに慣れすぎてしまい、もっとベースにある__ソフトウェア設計__という根幹部分を忘れてしまったか、または考えられてない事ではないでしょうか。
そんな大人の階段を登り切った僕が、もう一度設計とは何かを考えなおし、これは「糞コード」だよと言うために、オブジェクト思考の重要で基本的な要素であるカプセル化とパッケージを軸とした考えをまとめたので共有します。
参考にした Web サイトも是非見てください。
Encapsulation
カプセル化を行なう事で、開発メンテナンス性や、再利用性、柔軟性を上げることができます。
主な目的としては以下の 3 点です。
カプセル化とは、情報の隠蔽だけが目的ではありません。
Java spec. for Encapsulation
まずは、Java でカプセル化を行なう上で重要な言語仕様を 2 つ紹介します。
- アクセス修飾子
- パッケージ
多言語でも同等の仕様が存在しますが、Java においての仕様を知ることは設計の手助けとなります。
Access modifiers
アクセス修飾子を付けることで、オブジェクトへのアクセス制御を設定することが可能です。
Java には、以下 4 つの設定が存在します。
- Private
- Default (修飾子無し)
- Protected
- Public
4 つの設定の影響範囲は、クラスの継承関係やクラス内外の参照範囲の制御以外に、__パッケージによるアクセス制御が関わる__ので注意が必要です。しかも、この存在を知らなければ Java でのカプセル化は不完全になってしまうでしょう。
以下が、4 つのアクセス制御の一覧です。特に、修飾子無しの __Default の動作には注意__が必要です。
Access Modifier | within class | within package | outside package by subclass only | outside package |
---|---|---|---|---|
Private | Y | N | N | N |
Default | Y | Y | N | N |
Protected | Y | Y | Y | N |
Public | Y | Y | Y | Y |
よく、初心者向けの教科書に「とりあえずprivate
を指定し、必要な物はpublic
にしましょう。」と書いてありますが、これは大きな間違いです。
最初にアクセス修飾子を熟知しておかなければ、Java という言語を扱う上で最良の設計を行なうことは難しいでしょう。
そんな教科書は今すぐ窓から投げ捨てるか、ちり紙代わりに使いましょう。
Package
パッケージは Java のクラス郡をまとめるための仕組みです。主に利用する目的として、以下の 2 点ががあります。
- 名前の衝突を避ける事が出来る。
- パッケージによるアクセス制御を行なえる。
これらを利用する事で Facade デザインパターンを忠実に実現することができます。
Java のカプセル化においてこの仕組みは必要不可欠でしょう。
Design patterns
次に、ソフトウェア設計において基本的な 2 パターンを紹介します。
- Facade パターン
- Whole-Part パターン
両パターンとも、クライアント1が操作するインターフェースを提供し、複雑な処理を隠蔽するという点で似てます。
その違いを理解することで、「カプセル化とはどのようなものか」という理解が深まるでしょう。
Facade desing pattern
複雑な内部処理を隠蔽し、利用するクライアントにシンプルな操作を提供するパターンです。
基本的に Facade はクライアントの意思を翻訳し、それぞれの意思をサブシステムに伝達する役目を担います。
逆に言えば、Facade 自体はサブシステムを用いて複雑な処理を行いません。
Facade パターンは、必要ならばサブシステムを直接扱うことが可能です。
package shape;
// Subsystem
public Line extends Shape {
public void draw(Point from, Point to) { ... }
}
// Subsystem
public Circle extends Shape {
public void draw(Point point) { ... }
}
package widget;
// Facade
public AwesomeWindow {
private Line line = new Line();
private Circle circle = new Circle();
public void draw() {
Display display = Display.getInstance();
line.draw(display.getCornerTopLeft(), display.getCornerTopRight()));
line.draw(display.getCornerTopLeft(), display.getCornerBottomLeft()));
line.draw(display.getCornerTopRight(), display.getCornerBottomRight()));
line.draw(display.getCornerBottomRight(), display.getCornerBottomLeft()));
circle.draw(display.getCenter());
}
}
Whole-Part desing pattern
POSA に掲載されている、ソフトウェアを設計することで基本となるデザインパターンです。
Whole-Part パターンは、大きなクラスを小さなクラスに分け、複数クラスを組み合わせた処理を包括したクラスに実装するイメージです。
Part はその部品が責任を持つ狭い範囲の実装に集中し、Whole は複数の Part を隠蔽し、クライアントが必要としている実装だけに集中させます。
このデザインを適用することにより、ソフトウェア設計の基本原則の 1 つである__関心の分離__を行なうことが可能で、さらに部品を小さくすることで__再利用性__や__変更の容易性__が高まります。
- Part : 単純で明確な役割を持つオブジェクト
- Whole : Part を集約して、何かの処理を提供するオブジェクト
Facade パターンと似ていますが、Facade は部品の隠蔽を強制していないので、Part を直接扱うことが可能です。
Whole-Part パターンでは、以下の 3 タイプの関係を実現出来ます。
- Assembly - Parts
- Container - Contents
- Collection - Members
Assembly - Parts
部品 (Part) とそれを組み立てて出来た製品 (Assembly) の関係です。
製品には必要な全ての部品が揃っており、追加したり、変更することは出来ません。
完全な部品が必要なため、コンストラクタが冗長になるので、生成は Factory クラスに委譲すると良いでしょう。
完成され、変更不可能な製品を生成することで、個々の部品への関心を除去することが目的となります。
ex) 分子とそれを構成する原子 / Android とその部品のCPUやメモリ、液晶、バッテリ...
package assembly;
// パッケージ内からのみアクセス許可
class Processor { ... }
class Memory { ... }
class Display { ... }
public class Phone {
public static class Factory {
public static Phone create(ProcessorType processorType, int memorySize) {
return new Phone(new Processor(processorType), new Memory(memorySize));
}
}
}
private Processor processor;
private Memory memory;
private Display display;
Phone(ProcessorType processortype, int memorySize) {
processor = new Processor(processortype);
memory = new Memory(memorySize);
display = new Display();
}
public render() {
processor.render(display, memory);
}
// Setter 等の状態変更はさせてはならない
...
}
Container - Contents
入れ物 (Container) と内容物 (Content) の関係です。
入れ物は空でも良く、なんらかの約束にそって内容物を追加したり、変更が可能で、内容物の種類が異なっていても問題ありません。
入れ物の状態を管理して、その状態による判断情報を提供します。判断基準等は、動的に変更できても問題ありません。
Assembly - Parts ほど、クラス間の結合は強くありません。
入れ物の状態を判断、管理し、適切な状態を保つことで、内容物の状態への関心を除去するのが目的です。
ex) カバンとそれに入れてるハンカチ、飲み物、筆箱、名刺入れ...
package person;
class Name { ... }
class Address { ... }
class Interest { ... }
public class Profile {
public static class Factory {
...
}
private boolean requiredName;
private Name name;
private List<Interest> interests;
private List<Address> addresses;
// 興味が追加出来るか
public boolean canAddInterest() {
return interests.size() > 5;
}
// 名前を必須項目にするか
public void setRequiredName(boolean required) {
requiredName = required;
}
// プロフィールが完全か
public boolean hasCompleted() {
return (requiredName && name != null) && interests.size() > 0 && addresses.size() > 0;
}
public void addInterest(String interestText) {
if (!canAddInterest()) { throw new Exception(); }
interests.add(new Interest(interestText));
}
...
}
Collection - Members
集合 (Collection) と要素 (Members) の関係です。
同じ属性の要素の集合で、同じ属性であれば自由に追加したり、変更が出来ます。
個々の要素に違いはなく、すべての要素が同等に扱われます。
集合全体への操作を提供して、個々の要素への関心を除去するのが目的です。
ex) 動物とそれに属するネコや犬や鶏...
package zoo;
interface Animal {
String getSound();
}
public class Animal {
public static class Factory {
...
}
private List<Animal> animals;
// クライアントにとって解りやすいインタフェースを提供する
public void makeSounds() {
for (Animal animal : animals) {
System.out.println(animal.getSound())
}
}
...
}
Container - Content は入れ物の状態を管理、制御する事が主な目的ですが、Collection - Members はList<T>
等の標準インターフェースをラップし、クライアントに意味のあるインターフェースを提供するのが目的となります。
Packaging
Java ではどのようにパッケージするか、ということが設計の基本になり、そして、多くの迷いとなりますが、パッケージを上手に扱いカプセル化を行なうことで、ソフトウェアをもっとより良いものに仕上げることが可能となります。
ここでは、パッケージ分割を行なうために重要な観点を優先度の高い順に紹介しますが、優先度が高いものだけを考慮すれば良いわけではなく、設計時に全ての観点を総合的に判断することが必要です。
1. Cohesion - Coupling
凝集度、結合度は、一般的にクラス間やクラス内のコードを測る尺度ですが、パッケージにおいても、パッケージの責務にそのクラス群が集中しているか、パッケージ間の影響範囲が明確に整理できているかという点で、この凝集度と結合度の概念があてはまります。
Cohesion
凝集度とは、あるパッケージ内のクラス群が、与えられた責務にどれだけ集中しているかを示す尺度です。凝集度はいくつかに分類されていますが、詳しくはここで説明しません。
凝集度が低い場合には、クラス郡の共通性がなくなり、無駄な依存関係が結ばれ、以下の様な問題が発生してしまいます。
- そのパッケージの役割を理解するのに時間がかかる。
- 複数パッケージに影響範囲が及ぶ可能性が高くなるため、修正が難しくなる。
- パッケージに一貫性がなり、再利用が難しくなる。
Coupling
結合度とは、各パッケージ間での影響範囲が明確に判断できるように、整理、分割がされているかを示す尺度です。結合度はいくつかに分類されていますが、詳しくはここで説明しません。
パッケージ間の結合度が高くなると、それぞれのパッケージの依存関係を伝わり、意図しない箇所で不具合が起きてしまいます。
そのため、1 つの修正で複数のパッケージの修正を行なう必要が出てくる可能性が高くなります。
Packaging
凝集度と結合度を念頭に設計しパッケージする場合、1 クラスに 1 つの責任を与えるのと同じように、__1 パッケージに 1 つの責任__を与えるようにします。
すると、変更する頻度がパッケージ内で同頻度となりパッケージの安定性が高まります。
さらに、関心の分離を行なえて、責任範囲が整理されるため、変更箇所の特定が容易になります。
ある機能を修正する場合に、複数のパッケージをまたいで修正する必要がある場合は、設計の変更を考えたほうがよいでしょう。
2. Stability
安定性とは、そのクラスを変更する可能性がどれだけ高いのか低いのか、その頻度の事です。
安定性を考えずに設計した場合、更新頻度が低いパッケージに高い頻度のクラスが含まれ、低いはずのパッケージの更新頻度も高める事になってしまいます。
Packaging
安定性を念頭に設計しパッケージする場合、更新頻度が同程度のクラス毎にパッケージします。
その際、必要のない更新頻度を高めないように、頻度が高いものが低いものへ依存するように設計するべきです。
これは、MVC のでの設計でも見ることができます。
Model は更新頻度が低く、 Controller は中程度、View は高くなり、View は Controller に依存し、Controller は Model に依存するといった具合です。
3. Dependencies
依存関係は、あるパッケージが他のパッケージをどれだけ利用しているかどうかということです。
Packaging
依存関係を念頭に設計しパッケージする場合、パッケージ分割した後に依存していた場合はパッケージ分割すべきではありません。
それらが相互に依存していないか、もしくは他クラスを介して循環して依存していないかを確認して、依存している場合は同じパッケージにしましょう。
もし、他の観点と競合する場合は、Observer や Listener パターンをつかって依存関係を逆転し一方向に修正しましょう。
4. Reusability
再利用性は、我々の大好きな車輪の再発明を行わないよう、既存の機能はなるべくその機能を使うようにする事。
再利用性を考えずに設計すると、同じ処理が複数のクラスに分散し、1 つの修正により複数のクラスの修正を行なう必要がでてくる可能性があります。
Packaging
再利用性を念頭に設計しパッケージする場合、再利用可能なクラスやメソッドは個別のパッケージに分割するようにします。
ただし、再利用できるかどうかは現時点で不明な事が多く、先行投資となる可能性があるので、安定性によるパッケージ分割を優先したほうがいいでしょう。
5. Name
名前は、プログラミングにおいてとても重要な要素です。名前によってメソッドやクラス、そしてパッケージの責務が変わってしまうからです。
名前が考えられていないと、ドキュメント性が失われ役割がわからなくなり、開発自体に支障が出ます。
Packaging
名前を念頭にパッケージングする事は、とても自然な考え方です。パッケージから、それに紐付いたクラスを探しやすくなります。
しかし、これだけでは良い設計にはなりえません。パッケージの役割とは、動物の分類を犬や猫だと分ける様なものではありません。
もちろん、上記の優先度が高い観点を考慮した上で分類する事は全く問題ありませんが、名前による分類だけで満足してはいけません。
そんな動物図鑑は窓から投げ捨てましょう。
総括
以上で、パッケージとカプセル化の本当に重要なキーポイントは紹介出来たと思います。
紹介した以外にも、様々なソフトウェア工学やデザインパターンの知識はありますが、これらさえ考慮していれば「糞コード」を生む可能性は低くなります。
大事な事は、プログラミングする上で自分以外に自分の書いたソースコードを扱う、同僚やパートナー企業などの「クライアント」がいることを忘れずに考えられかどうかです。
参考
- カプセル化、情報隠蔽、データ隠蔽
- Access Modifiers in java
- パッケージ (Java)
- Facade パターン
- Facade パターン (複数のサブシステムの統一窓口となる高レベルなインタフェースを提供する)
- Encapsulation in Java
- 設計の基本パターン:Whole-Part(全体-部分)
- [デザインパターン][POSA]Whole-Part
- [Java][DDD] コードで学ぶドメイン駆動設計入門 ~ファクトリ編~Add Star
- ソフトウェア原則[5] - パッケージ分割
- 設計におけるオブジェクトの責務分配に有効なものさし ― 凝集度と結合度 ―
- The Common Closure Principle (CCP) 共通閉鎖原則
- Observerパターンとマルチスレッド
-
クライアントとは、あなたが書いたソースコードを利用する人のことです。 ↩