單一職責原則 (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("我是會飛的英雄");
}
}
而這個觀點可能看前面提到的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類別了,完成依賴反轉!