1.1 開閉原則
定義:
一個軟件實體(類、模塊和函數)應該對擴展開放,對修改關閉。強調用抽象構建框架,用實現擴展細節。
舉例:
首先創建一個課程接口:
public interface ICourse {
Integer getId();
String getName();
Double getPrice();
}
在創建一個具體的實現類,比如叫Java架構課程類:
public class JavaCourse implements ICourse {
private Integer id;
private String name;
private Double price;
public JavaCourse(Integer id, String name, Double price) {
this.id = id;
this.name = name;
this.price = price;
}
@Override
public Integer getId() {
return this.id;
}
@Override
public String getName() {
return this.name;
}
@Override
public Double getPrice() {
return this.price;
}
}
這時突然提了一個需求,比如課程的價格有變動,需要對getPrice()方法進行修改,如果直接去改動這個方法,則其它調用這個方法的代碼可能存在風險。可以通過如下方式:
public class JavaDiscussCourse extends JavaCourse{
public JavaDiscussCourse(Integer id, String name, Double price) {
super(id, name, price);
}
public Double getOriginPrice() {
return super.getPrice();
}
public Double getPrice() {
return super.getPrice() * 0.61;
}
}
1.2 依賴倒置原則
定義:
設計代碼結構時,高層模塊不應該依賴底層模塊,抽象不應該依賴細節。
舉例:
Tom正在學兩門課程, 分別是Java和Python:
public class Tom {
public void studyJavaCourse() {
System.out.println("Tom在學習Java課程");
}
public void studyPythonCourse() {
System.out.println("Tom在學習Python課程");
}
public static void main(String[] args) {
Tom tom = new Tom();
tom.studyJavaCourse();
tom.studyPythonCourse();
}
}
此時,他突然還想學AI,於是準備在Tom類裏在家一個study方法,這樣的方式真的太差勁了,Tom和課程耦合度太高。
如下從Tom抽象出課程接口,然後Tom面向接口調用課程,具體的實現類由調用層自己指定。這樣不管增加多少課程,Tom都不需要管。
首先創建一個課程接口,裏面只有一個可以獲取自己的名稱方法:
public interface ICourse {
public String getName();
}
在分別創建三個課程實現類
public class JavaCourse implements ICourse {
private String name;
public JavaCourse() {
this.name = "Java";
}
@Override
public String getName() {
return name;
}
}
public class PythonCourse implements ICourse {
private String name;
public PythonCourse() {
this.name = "Python";
}
@Override
public String getName() {
return name;
}
}
public class AiCourse implements ICourse {
private String name;
public AiCourse() {
this.name = "Ai";
}
@Override
public String getName() {
return name;
}
}
最後在修改Tom類,使其面向課程接口調用:
public class Tom {
public void study(ICourse course) {
System.out.println("Tom正在學習" + course.getName());
}
public static void main(String[] args) {
Tom tom = new Tom();
tom.study(new JavaCourse());
tom.study(new PythonCourse());
tom.study(new AiCourse());
}
}
1.3 單一職責原則
定義:
不要存在多於一個導致類變更的原因。
舉例:
假如課程分成了直播課和錄播課,直播課不能快進,錄播課可以,很明顯這兩種課程功能職責已經不一樣。下面展示一端看似好像不復雜的代碼,但其實已經埋下了複雜的隱患:
public class Course {
public void study(String courseName) {
if("直播課".equals(courseName)) {
System.out.println(courseName + "不能快進");
} else {
System.out.println(courseName + "可以反覆觀看");
}
}
public static void main(String[] args) {
Course course = new Course();
course.study("直播課");
course.study("錄播課");
}
}
這個時候,如果需求變了,假如現在要對課程加密,直播課和錄播課的加密邏輯不一樣,那修改的邏輯就會相互影響,充滿了風險。所以我們需要對上面的代碼進行解耦,可以分別創建LiveCourse和ReplayCourse:
public class LiveCourse {
public void study(String courseName) {
System.out.println(courseName + "不能快進");
}
}
public class ReplayCourse {
public void study(String courseName) {
System.out.println(courseName + "可以反覆觀看");
}
}
public class Test {
public static void main(String[] args) {
LiveCourse liveCourse = new LiveCourse();
ReplayCourse replayCourse = new ReplayCourse();
liveCourse.study("直播課");
replayCourse.study("錄播課");
}
}
1.4 接口隔離原則
定義:用多個專門的接口,而不適用單一的總接口。
舉例:
先舉一個不合適的例子,假如不遵從接口隔離原則,則設計的接口一般比較臃腫,比如如下的IAnimal接口:
public interface IAnimal {
void eat();
void fly();
void swim();
}
此時如果有兩個接口的實現類,分別是Bird鳥類和Dog狗類,則將會出現和奇葩的問題。
public class Bird implements IAnimal {
@Override
public void eat() {
}
@Override
public void fly() {
}
@Override
public void swim() {
}
}
public class Dog implements IAnimal {
@Override
public void eat() {
}
@Override
public void fly() {
}
@Override
public void swim() {
}
}
什麼問題,還沒發現嗎?仔細一看發現鳥居然有一個swim(),狗居然有一個fly()方法,你說奇怪不奇怪。所以啊,接口是需要細分的,在這裏接口要針對不同的行爲來細分,比如分別設計出IEatAnimal、IFlyAnimal和ISwimAnimal接口,那鳥和狗肯定都需要實現公共接口IEatAnimal,不然不餓死了嘛。除了實現公共接口,他們還需實現自己特有行爲的接口,鳥實現IFlyAnimal接口,狗實現ISwimAnimal接口。
1.5 迪米特原則
定義:一個對象應該對其他對象保持最少的瞭解,儘量降低類與類之間的耦合度。
舉例:
假如現在有個老闆類Boss想要看看線上的課程數量,這個時候他去找到開發團隊領導TeamLeader去進行統計,TeamLeader需要將結果給Boss。
public class Course {
}
public class TeamLeader {
public void checkNumberOfCourses(List<Course> coursesList) {
System.out.println("目前已發佈的課程數量是:" + coursesList.size());
}
}
public class Boss {
public void checkNumberOfCourses(TeamLeader teamLeader) {
List<Course> coursesList = new ArrayList<>();
for(int i=0; i<20; i++) {
coursesList.add(new Course());
}
teamLeader.checkNumberOfCourses(coursesList);
}
}
如果是按上面的方法來完成課程數量統計,想必老闆Boss早把開發團隊領導TeamLeader炒掉了吧,爲啥?很明顯老闆招人肯定是給他幹活,他自己安排一件事,最希望的結果是你去幹,我啥都不管,最後給我一個結果就行,你們說是不是,一個簡單的事情都幹不好怎麼能不被炒,哈哈。看看下面的代碼,一個合格的TeamLeader總是能表現的優秀(老闆少操心):
public class TeamLeader {
public void checkNumberOfCourses() {
List<Course> coursesList = new ArrayList<>();
for(int i=0; i<20; i++) {
coursesList.add(new Course());
}
System.out.println("目前已發佈的課程數量是:" + coursesList.size());
}
}
public class Boss {
public void checkNumberOfCourses(TeamLeader teamLeader) {
teamLeader.checkNumberOfCourses();
}
}
1.6 裏式替換原則
定義:一個軟件實體如果適用於一個父類,那麼一定適用於子類。
隱含意思:子類可以擴展父類,但是不能改變父類原有功能。
(1)子類可以實現父類的抽象方法,但不能覆蓋父類的非抽象方法。
(2)子類可以添加新的方法。
(3)重載父類時,方法的入參要比父類方法的輸入參數更寬鬆。
(4)重載父類時,方法的出參要比父類方法的輸出參數更嚴格。
在1.1描述開閉原則時,增加了一個獲取父類屬性的方法,還重寫了父類的非抽象方法,很明顯違背了裏式替換原則。
舉例:
正方形是一種特殊的長方形。
首先創建一個父類Rectangle:
public class Rectangle {
private long height;
private long weight;
public long getHeight() {
return height;
}
public void setHeight(long height) {
this.height = height;
}
public long getWeight() {
return weight;
}
public void setWeight(long weight) {
this.weight = weight;
}
}
違背裏式替換原則創建一個正方形Square類:
public class Square extends Rectangle {
private long length;
public long getLength() {
return length;
}
public void setLength(long length) {
this.length = length;
}
public long getHeight() {
return getLength();
}
public void setHeight(long height) {
setLength(height);
}
public long getWeight() {
return getLength();
}
public void setWeight(long weight) {
setLength(weight);
}
}
最後在測試類中創建resize()方法,長方形的寬應該大於等於高,我們讓高一直增長,直到高和寬相等變成正方形:
public class Test {
public static void resize(Rectangle rectangle) {
while(rectangle.getWeight() >= rectangle.getHeight()) {
rectangle.setHeight(rectangle.getHeight() + 1);
System.out.println("width:" + rectangle.getWeight() + " ,height:" + rectangle.getHeight());
}
System.out.println("resize方法結束" + "\nwidth:" + rectangle.getWeight() + " ,height:" + rectangle.getHeight());
}
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.setHeight(10);
rectangle.setWeight(20);
resize(rectangle);
}
}
現在將Rectangle類替換成子類Square。
public class Test {
public static void resize(Rectangle rectangle) {
while(rectangle.getWeight() >= rectangle.getHeight()) {
rectangle.setHeight(rectangle.getHeight() + 1);
System.out.println("width:" + rectangle.getWeight() + " ,height:" + rectangle.getHeight());
}
System.out.println("resize方法結束" + "\nwidth:" + rectangle.getWeight() + " ,height:" + rectangle.getHeight());
}
public static void main(String[] args) {
Square square = new Square();
square.setHeight(10);
square.setWeight(20);
resize(square);
}
}
上面代碼運行時出現死循環,爲啥?這個應該不用我解釋了吧,因爲重寫了父類非抽象方法,使長方形的寬高始終相等,所以while就死了唄。
下面演示一個遵循裏式替換原則的案例。
只需稍稍改動一下正方形類,使其不覆蓋父類非抽象方法。
public class Square extends Rectangle {
private long length;
public long getLength() {
return length;
}
public void setLength(long length) {
this.length = length;
}
}
這個時候運行Test類,將Rectangle換成Square是不會報錯的。
1.7 合成複用原則
定義:儘量使用對象組合/聚合而不是繼承來達到軟件複用。
繼承叫白箱複用,因爲實現細節暴露給子類了。
組合/聚合叫黑箱複用,因爲無法獲取到組合對象的實現細節。
舉例:
下面舉一個合成複用原則的案例。
public abstract class DBConnection {
public abstract String getConnection();
}
public class MySQLConnection extends DBConnection {
@Override
public String getConnection() {
return "MySQL數據庫連接";
}
}
public class OracleConnection extends DBConnection {
@Override
public String getConnection() {
return "Oracle數據庫連接";
}
}
public class ProductDao {
private DBConnection dbConnection;
public void setDbConnection(DBConnection dbConnection) {
this.dbConnection = dbConnection;
}
public void addProduct() {
String conn = dbConnection.getConnection~~~~();
System.out.println("使用" + conn + "增加產品");
}
}
1.8 設計原則總結
理想與現實總是不能畫上等號,爲毛?因爲現實開發中要考慮人力、時間、成本、質量等種種因素,不能只追求完美。適當的場景遵循合適的設計原則纔是力求最好的標準,所以魚和熊掌不可兼得。