OO的五大原則:SOLID
單一職責原則 (Single Responsibility Principle, SRP)
一個類別只做一件事。讓一個類別只有一個職責,且該功能應由這個類完全封裝,兩件改變的事應分離成兩個類或兩個功能。正常情況下一個類別該做的事情,應該要能夠只用一句話描述出來。
舉例來說,我們的數據機功能如下,它的職責是連接管理(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)
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 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 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)
所有子類別都可以代理父類別的工作,也就是說必須確保子類別在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)
一個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("我是會飛的英雄"); }}
而這個觀點可能看前面提到的SRP有點像,但是ISP和SRP最主要的差別是:
- SRP:一個模組應該只承擔一個單一的責任。ISP:不應該被迫實作太多不需要的方法。
依賴反轉原則 (Dependency inversion principle, DIP)
One should “depend upon abstractions, [not] concretions.”
DIP簡單說就是以下原則:
- 高階模組不應該依賴於低階模組,兩者都該依賴抽象。抽象不應該依賴於具體實作方式。具體實作方式則應該依賴抽象。能夠動態替換原本依賴的模組,降低耦合,做到隔離修改及單元測試。
高階依賴低階模組
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」
- Programmer原本直接耦合於Computer,但使用IoC Container (Company),Programmer就直接對Company,至於Programmer要用什麼Computer,由Company決定也就是說,原本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類別了,完成依賴反轉!
文章分享
如果這篇文章對你有幫助,歡迎分享給更多人!
部分內容可能已過時