OO的五大原則:SOLID

單一職責原則 (Single Responsibility Principle, SRP)

A class should only have a single responsibility, that is, only changes to one part of the software’s specification should be able to affect the specification of the class.

一個類別只做一件事。讓一個類別只有一個職責,且該功能應由這個類完全封裝,兩件改變的事應分離成兩個類或兩個功能。正常情況下一個類別該做的事情,應該要能夠只用一句話描述出來。

舉例來說,我們的數據機功能如下,它的職責是連接管理(dial、handup)資料通訊(send、recv)

interface Modem {
	  void dial(String pNo);
	  void handup();
	  void send(char c);
	  char recv();
}

一旦和連接相關的功能要修改,或通訊相關的功能要修改,都必須讓無關的另一個功能重新編譯和部署,因此因該把這兩個職責抽離出來:

interface Connection {
	  void dial(String pNo);
	  void handup();
}

interface DataTransmission {
	  void send(char c);
	  char recv();
}

class Modem implements Connection, DataTransmission {
    // ...
}

開閉原則 (Open-Close principle, OCP)

Software entities … should be open for extension, but closed for modification.

OO的高層原則,Closed for Modification; Open for Extension(對修改關閉;對擴充開放),以Abstraction和Polymophism將靜態結構變成動態結構,維持設計的封閉性。不會牽一髮動全身,修改 A 的程式碼,B 也要跟著一起修改

假設原本有以下計算面積的程式碼:

import java.util.ArrayList;

class Square {
    private double width;
    Square(double width){
        this.width = width;
    }
    public double getWidth() { return width; }
}

class Circle {
    private double radius;
    Circle(double radius){
        this.radius = radius;
    }
    public double getRadius() { return radius; }
}

class Traingle {
    private double width;
    private double height;
    Traingle(double width, double height){
        this.width = width;
        this.height = height;
    }
    public double getWidth() { return width; }
    public double getHeight() { return height; }
}

public class AreaExample {

    public static void main(String[] args){
        ArrayList<Object> shapes = new ArrayList<>();
        shapes.add(new Square(10));
        shapes.add(new Circle(5));
        shapes.add(new Traingle(2,3));
        for(Object shape: shapes) {
            double area = 0;
            if(shape instanceof Square){
                area = Math.pow(((Square) shape).getWidth(), 2);
            } else if (shape instanceof Circle) {
                area = Math.pow(((Circle) shape).getRadius(), 2) * Math.PI;
            } else if (shape instanceof Traingle) {
                area = ((Traingle) shape).getWidth() * ((Traingle) shape).getHeight();
            }
            System.out.println("面積是:" + area);
        }
    }
}

如果今天要再新增一種形狀,還要修改計算area的方法,不僅失去靈活性,還難以擴充新功能。

因此,可以將實作不同的地方進行抽象,達成如下程式碼:

import java.util.ArrayList;

interface Shape {
    double getArea();
}

class Square implements Shape{
    private double width;
    Square(double width){
        this.width = width;
    }
    public double getArea() { return width * width; }
}

class Circle implements Shape{
    private double radius;
    Circle(double radius){
        this.radius = radius;
    }
    public double getArea() { return radius * radius * Math.PI; }
}

class Traingle implements Shape{
    private double width;
    private double height;
    Traingle(double width, double height){
        this.width = width;
        this.height = height;
    }
    public double getArea() { return width * height; }
}

public class AreaExample {

    public static void main(String[] args){
        ArrayList<Shape> shapes = new ArrayList<>();
        shapes.add(new Square(10));
        shapes.add(new Circle(5));
        shapes.add(new Traingle(2,3));
        for(Shape shape: shapes) {
            System.out.println("面積是:" + shape.getArea());
        }
    }
}

如此一來,新增新的Shape只要去實作getArea()方法就好,並不用修改其他所有的類別!


里氏替換原則 (Liskov substitution principle, LSP)

Objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program

所有子類別都可以代理父類別的工作,也就是說必須確保子類別在override父類別的方法的時候,必須要能相容父類別的行為,遵照父類別設計開發,讓已經寫好的方法能夠被重用。

舉例來說,原本有計算正方形和矩形的程式:

class Rectangle{
    private int width;
    private int height;
    Rectangle(){}
    public void setWidth(int width){ this.width = width; }
    public void setHeight(int height){ this.height = height; }
    public int area(){
        return width * height;
    }
}

class Square extends Rectangle{
    Square(){}
    public void setWidth(int width){ 
        this.width = width;
        this.height= width; 
    }
    public void setHeight(int height){ 
        this.width = height;
        this.height= height; 
    }
}

public class ShapeLSP {
    public static void main(String[] args) {
        Rectangle rectangle = new Rectangle());
        rectangle.setWidth(5);
        rectangle.setHeight(10);
        Rectangle square = new Square();
        square.setWidth(5);
        square.setHeight(10);

        System.out.println(rectangle.area());
        System.out.println(square.area());
    }
}

我們在語義上會說正方形是長方形的一種,但是正方形和長方形不同之處在於,正方形的長和寬是相等的。因此,我們計算的時候,雖然執行了一樣的行為(且這個行為是父類別定義好的),卻得到不同的結果(上述程式碼rectangle面積為50;square面積為100)。這就違反了LSP。

因此我們應該對它進行以下修改:

interface Shape{
    public int area();
}

class Square implements Shape{
    private int edge;
    Square(){}
    public void setEdge(){ this.edge = edge; }
    public int area(){
        return edge * edge;
    }
}

class Rectangle implements Shape{
    private int width;
    private int height;
    Rectangle(){}
    public void setWidth(int width){ this.width = width; }
    public void setHeight(int height){ this.height = height; }
    public int area(){
        return width * height;
    }
}

public class ShapeLSP {
    public static void main(String[] args) {
        Shape rectangle = new Rectangle());
        rectangle.setWidth(5);
        rectangle.setHeight(10);
        Shape square = new Square();
        square.setEdge(5);

        System.out.println(rectangle.area());
        System.out.println(square.area());
    }
}

如此一來,我們期望的面積計算,就能因為不用的Shape而有所不同,而且不會因為override造成父、子類別的行為期望不一致!


介面隔離原則 (Interface segregation principle, ISP)

Many client-specific interfaces are better than one general-purpose interface.

一個interface相當於一種角色,而此角色由誰來演出,就相當於它的concrete class。因此一個interface應該當簡單的代表一個角色。也就是說,應該把interface依各個方法拆出來,不應強迫依賴於不使用的interface

舉例來說,如果今天有個角色是超級英雄,而我們在超級英雄這個介面寫了各種超級英雄的行為:

interface SuperHero {
    void fightVillains();
    void growLarge();
    void shrink();
    void fly();
}

但是不是每個超級英雄都會用到所有的行為,這時候就很可能被迫依賴於這些行為而進行空實作,如:

class FlyingSuperHero implements SuperHero {
    void fightVillains() {
        System.out.println("打擊壞人!");
    }
    void growLarge(){}
    void shrink(){}
    void fly() {
        System.out.println("我是會飛的英雄");
    }
}

如果要改善這種情況,重新設計成符合ISP的介面,它應該會長這樣:

// 介面隔離
interface SuperHero {
    void fightVillains();
}
interface GrowingLargeSkill {
    void grwoLarge();
}
interface ShinkingSkill {
    void shrink();
}
interface FlyingSkill {
    void fly();
}

// 實作需要的部分
class FlyingSuperHero implements SuperHero, FlyingSkill {
    void fightVillains() {
        System.out.println("打擊壞人!");
    }
    void fly() {
        System.out.println("我是會飛的英雄");
    }
}
符合ISP

而這個觀點可能看前面提到的SRP有點像,但是ISP和SRP最主要的差別是:

  • SRP:一個模組應該只承擔一個單一的責任。
  • ISP:不應該被迫實作太多不需要的方法。

依賴反轉原則 (Dependency inversion principle, DIP)

One should “depend upon abstractions, [not] concretions.”

DIP簡單說就是以下原則:

  1. 高階模組不應該依賴於低階模組,兩者都該依賴抽象。
  2. 抽象不應該依賴於具體實作方式。具體實作方式則應該依賴抽象。
  3. 能夠動態替換原本依賴的模組,降低耦合,做到隔離修改及單元測試。

高階依賴低階模組

class Programmer {
    Computer computer = new Computer();

    void forFun(){
        computer.watchVideo();
    }
}

class Computer {
    void watchVideo() { System.out.println("播放影片"); }
    void playGame() { System.out.println("玩遊戲"); }
    void runProgram() { System.out.println("執行程式"); }
}

這時候,Programmer依賴於Computer,也就是說Programmer必須知道電腦有什麼方法可以用,才能做事情,一旦Computer變了,Programmer也必須跟著變!

但是現在電腦是Programmer自己創建的,假如電腦出了問題,那Programmer就不能做事情了,所以我們可以改寫這個方法,讓它變成依賴注入。

Dependency Injection 依賴注入

class Programmer {
    Computer computer;
    Programmer(Computer computer) {
        this.computer = computer;
    }
    void setComputer(Computer computer) {
        this.computer = computer;
    }
    void forFun(){
        computer.watchVideo();
    }
}

class Computer {
    void watchVideo() { System.out.println("播放影片"); }
    void playGame() { System.out.println("玩遊戲"); }
    void runProgram() { System.out.println("執行程式"); }
}

public class Company {
    Computer computer;
    Programmer programmer;
    public Programmer getProgrammer(){
        computer = new Computer();
        programmer = new Programmer(computer);
        return programmer
    }
}

public class Client {
    public static void main(String[] args){
        Company company = new Company();
        Programmer programmer = company.getProgrammer();
    }
}

這樣比起把電腦在Programmer裡面寫死,有更大的彈性,讓使用Programmer的人負責建立這些物件。

IoC 控制反轉

把原本A對B控制權移交給第三方容器降低A對B物件的耦合性,讓雙方都倚賴第三方容器。這也是經典的好萊塢法則:「don’t call us, we’ll call you」

  1. Programmer原本直接耦合於Computer,但使用IoC Container (Company),Programmer就直接對Company,至於Programmer要用什麼Computer,由Company決定
  2. 也就是說,原本Programmer直接控制於Computer,但透過一個IoC容器我們控制權反轉給了容器

依賴反轉

現在Programmer還是直接依賴於Computer,但假設今天他要換用筆電做事情,那我們除了要新增一個Laptop類別之外,還要修改Programmer和相關的Class,這時候我們應該讓Programmer依賴於interface,而不是Concrete Class本身:

interface ElectronicDevice {
    void watchVideo();
    void playGame();
    void runProgram(); 
}

class Computer implements ElectronicDevice {
    void watchVideo() { System.out.println("我用電腦播放影片"); }
    void playGame() { System.out.println("我用電腦玩遊戲"); }
    void runProgram() { System.out.println("我用電腦執行程式"); }
}

class Laptop implements ElectronicDevice {
    void watchVideo() { System.out.println("我用筆電播放影片"); }
    void playGame() { System.out.println("我用筆電玩遊戲"); }
    void runProgram() { System.out.println("我用筆電執行程式"); }
}

class Programmer {
    ElectronicDevice device;
    Programmer(ElectronicDevice device) {
        this.device = device;
    }
    void setDevice(ElectronicDevice device) {
        this.device = device;
    }
    void forFun(){
        device.watchVideo();
    }
}

public class Company {
    ElectronicDevice device;
    Programmer programmer;
    public Programmer getProgrammer(){
        device = new Laptop();
        programmer = new Programmer(device);
        return programmer
    }
}

public class Client {
    public static void main(String[] args){
        Company company = new Company();
        Programmer programmer = company.getProgrammer();
    }
}

這樣就算之後還要新增其他裝置,也不會影響到原本的Programmer類別了,完成依賴反轉!