はじめに
私は、本を紹介したいと思います。紹介する本は、
「アジャイルソフトウェア開発の奥義 オブジェクト指向開発の神髄と匠の技 第2版」です。
この本に出てくるコードは、javaとC++で書かれています。
この本は、アジャイル開発・SOLID原則・デザインパターン・パッケージ設計の原則に
ついて学ぶことができます。
私がこの本を読もうと思ったきっかけは
美しいコードを書けるようにないたいと考えるようになったからです。
というのも、学生時代に作成したコードを見返したときに
「なんて汚い読みにくいコードなんだ!!」と衝撃を受けたのがきっかけです。
この本を読んでのSOLID原則について、私なりに簡単に説明したいと思います。
ただ、プログラミング初心者には、少し難しい内容になっています。
SOLID原則とは、オブジェクト指向に即してコードを整理するための5つの原則で
それぞれの原則の頭文字をとってこう名付けられています。以下、順に紹介していきます。
単一責任の原則(SRP)
クラスを変更する理由は1つ以上存在してはならない
こちらは、SRPに違反しているコードです。
public class Employee {
String itsName;
int itsAge;
FileWriter fileWriter;
public void setName(String name) {
itsName = name;
}
public String getName() {
return itsName;
}
public void setAge(int age) {
itsAge = age;
}
public int getAge() {
return itsAge;
}
public void save() {
try {
fileWriter = new FileWriter("employee.csv");
fileWriter.append(itsName);
fileWriter.append(",");
fileWriter.append(String.valueOf(itsAge));
fileWriter.append("\r\n");
fileWriter.flush();
fileWriter.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
このコードは、変更する理由を2つ持っています。
- 「従業員情報管理」と「従業員情報をcsv出力」という2つの役割
- この2つの役割は、それぞれ別の理由で変更される
これが、SRPに違反している状態です。
このクラスを複数のクラスが利用していて従業員情報に変更があった場合
- saveメソッドだけを使用しているクラスもリビルド、再テスト、再ロードすることになる
- うっかり忘れると、アプリケーションの振る舞いが予測不可能な状態になる
では、どのようにすれば良いか。
public class Employee {
String itsName;
int itsAge;
public void setName(String name) {
itsName = name;
}
public String getName() {
return itsName;
}
public void setAge(int age) {
itsAge = age;
}
public int getAge() {
return itsAge;
}
public return getExcelString() {
return itsName + "," + String.valueOf(itsAge) + "\r\n";
}
public interface EmployeeSaveInterface {
public void save(Employee employee);
}
public class EmployeeSave implements EmployeeSaveInterface{
public FileWriter fileWriter;
public void save(Employee employee) {
try {
fileWriter = new FileWriter("/employee.csv");
fileWriter.append(employee.getExcelString());
fileWriter.flush();
fileWriter.close();
} catch (IOException e1) {
e1.printStackTrace();
}
}
}
このようにすることで、従業員情報を変更するときは、Employeeクラスを変更して
saveメソッドを変更するときは、EmployeeSaveクラスを変更するようになります。
それぞれのクラスの変更理由が1つになります。
そもそも、役割とは、何なのかという人もいると思います。
「変更理由=役割」ということです。
いくつか機能を持っているクラスでも、同じ変更理由で変更されるのであれば
そのクラスの役割は、1つということになります。
オープン・クローズドの原則(OCP)
ソフトウェアの構成要素(クラス、モジュール、関数など)は拡張に対して開いて(Open)いて、
修正に対して閉じて(Closed)いなければならない
こちらは、OCPに違反しているコードです。
public class DrawingTool {
public void drawAllShapes(ArrayList<String> list) {
Circle circle = new Circle();
Triangle triangle = new Triangle();
for(int i = 0; i<list.size(); i++) {
switch (list.get(i)) {
case "Circle":
circle.drawCircle();
break;
case "Triangle":
triangle.drawTriangle();
break;
}
}
}
}
public class Circle {
public void drawCircle() {
System.out.println("Circle");
}
}
public class Triangle {
public void drawTriangle() {
System.out.println("Triangle");
}
}
このコードは、修正に対して閉じていません。
注目してほしいのは、drawAllShapesメソッドのswitch文です。
ここに新しい図形クラスを追加しようとすると以下のようになります。
- switch文に新しい図形クラスに対応する処理を追加
- 機能を拡張しようとすると、既存のクラスを修正しなければならない状態
- 図形を追加する度にswitch文を修正するのは、手間が掛かる
これが、OCPに違反している状態です。
では、どのようにすれば良いか。
public class DrawingTool {
public void drawAllShapes(ArrayList<Shape> list) {
for(Shape shape : list) {
shape.draw();
}
}
}
public abstract class Shape {
public abstract void draw();
}
public class Circle extends Shape {
@Override
public void draw() {
System.out.println("Circle");
}
}
public class Triangle extends Shape {
@Override
public void draw() {
System.out.println("Triangle");
}
}
このコードは、修正に対して閉じています。
このように各図形のクラスに共通のメソッドを継承させることで、新しい図形を追加する場合
- 新しい図形クラスでShapeクラスを継承して
新しい図形クラスでdrawメソッドをオーバーライドするだけで済む - drawAllShapesメソッドに変更を加える必要はない
- 既存のクラスへの影響を気にせず、クラスを追加するだけで機能を拡張できる
これが、OCPに準拠している状態です。
リスコフの置換原則(LSP)
派生型はその基本型と置換可能でなければならない
こちらは、LSPに違反しているコードです。
public class Rectangle {
private int itsHeight;
private int itsWidth;
public void setHeight(int height) {
itsHeight = height;
}
public void setWidth(int width) {
itsWidth = width;
}
public int getArea() {
return itsHeight * itsWidth;
}
}
public class Square extends Rectangle {
private int itsHeight;
private int itsWidth;
public void setHeight(int height) {
itsHeight = height;
itsWidth = height;
}
public void setWidth(int width) {
itsHeight = width;
itsWidth = width;
}
public int getArea() {
return itsHeight * itsWidth;
}
}
Rectangle.setHeight(20) Square.setHeight(20)
Rectangle.setWidth(30) Square.setWidth(30)
Rectangle.getArea() => 600 Square.getArea() => 900
このコードは、Rectangle(基本型)とSquare(派生型)のgetAreaの振る舞いが変わっています。
- SquareとRectangleでgetAreaを同じ方法で呼び出した結果が同じにならない
- Squareは、Rectangleと置換が不可能
これが、LSPに違反している状態です。
では、どのようにすれば良いか。
public interface Shape {
public void setHeight(int height);
public void setWidth(int width);
public int getArea();
}
public class Rectangle implements Shape {
private int itsHeight;
private int itsWidth;
public void setHeight(int height) {
itsHeight = height;
}
public void setWidth(int width) {
itsWidth = width;
}
public int getArea() {
return itsHeight * itsWidth;
}
}
public class Square implements Shape {
private int itsHeight;
private int itsWidth;
public void setHeight(int height) {
itsHeight = height;
itsWidth = height;
}
public void setWidth(int width) {
itsHeight = width;
itsWidth = width;
}
public int getArea() {
return itsHeight * itsWidth;
}
}
RectangleとSquareを継承を使わずに分離します。
しかし、似たような性質を持っているため、完全には分離しないようにします。
- 派生型が基本型に置換可能ということは、拡張に対して開いており、
修正に対して閉じていることを表している
LSPに準拠することでOCPを準拠することができます。
インターフェース分離の原則(ISP)
クライアントに、クライアントが利用しないメソッドへの依存を強制してはならない
こちらは、ISPに違反しているコードです。
public interface Motion {
public void fly();
public void run();
public void swim();
}
public class Human implements Motion {
public void fly() {
System.out.println("飛ぶ");
}
public void run() {
System.out.println("走る");
}
public void swim() {
System.out.println("泳ぐ");
}
}
このコードは、Humanクラスを作成したときにMotionを実装すると、
Humanクラスでは、必要としないメソッドの実装を強制されます。
必要としないメソッドは、flyメソッドです。
人は、空を飛びませんよね?
- 利用しないメソッドを実装していると、そのメソッドの変更の影響を受ける可能性がある
- 利用しないメソッドであれば、本来その変更に無関係なはずだが、それを実装していると不用意に影響を受けてしまう
これがISPに違反している状態です。
では、どのようにすれば良いか。
interface MotionFly
{
public void fly();
}
interface MotionRun
{
public void run();
}
interface MotionSwim
{
public void swim();
}
このように、必要としないメソッドがある場合は、インターフェースを細かくして、
分離することです。そして、必要なメソッドだけを実装するようにします。
- 無関係なメソッド、クラスからの変更の影響を受けることがない
これが、ISPに準拠している状態です。
依存関係逆転の原則(DIP)
a. 上位のモジュールは下位のモジュールに依存してはならない
どちらのモジュールも「抽象」に依存すべきである
b. 「抽象」は実装の詳細に依存してはならない。実装の詳細が「抽象」に依存すべきである
こちらは、DIPに違反しているコードです。
public class Button {
private Lamp itsLamp;
public Button(Lamp lamp) {
itsLamp = lamp;
}
public void pull() {
if(!itsLamp.getStatus()) {
itsLamp.turnOn();
} else {
itsLamp.turnOff();
}
}
}
public class Lamp {
private boolean itsStatus = false;
public void turnOn() {
itsStatus = true;
System.out.println("ON");
}
public void turnOff() {
itsStatus = false;
System.out.println("OFF");
}
public boolean getStatus() {
return itsStatus;
}
}
このコードは、Buttonクラスは、Lampクラスに依存しています。
Lampクラスは、「実装の詳細」であり、「抽象」ではありません。
- ButtonクラスがLampクラスという「実装の詳細」に依存していると、
ButtonクラスはLampクラスしか扱えない - Buttonクラスを他のクラスで使用することが出来ない
これが、DIPに違反している状態です。
では、どのようにすれば良いか。
public interface ButtonInterface {
public void turnOn();
public void turnOff();
public boolean getStatus();
}
public class Button {
private ButtonInterface itsLamp;
public Button(ButtonInterface lamp) {
itsLamp = lamp;
}
public void pull() {
if(!itsLamp.getStatus()) {
itsLamp.turnOn();
} else {
itsLamp.turnOff();
}
}
}
public class Lamp implements ButtonInterface {
private boolean itsStatus = false;
public void turnOn() {
itsStatus = true;
System.out.println("ON");
}
public void turnOff() {
itsStatus = false;
System.out.println("OFF");
}
public boolean getStatus() {
return itsStatus;
}
}
このように抽象クラスであるButtonInterfaceクラスを作成して、
ButtonクラスをButtonInterfaceに依存させることで「抽象」に依存させることができます。
- ButtonInterfaceクラスを実装することで、どのクラスでもButtonクラスを利用できる
これが、DIPに準拠している状態です。
おわりに
私自身もまだ完全に理解はできていませんが、自分がコードを書くとき
コードレビューをするときに、ただコードを書く、読むだけではなく
どのように書いているかを見るようになりました。
プログラミングを一通り経験した方でしたら、とても参考になり、
コードの見方やコードを書く時の意識も変わります。ぜひとも購入してみてください!