当前位置:网站首页>2-軟件設計原則
2-軟件設計原則
2022-04-23 05:40:00 【@阿清】
在軟件開發中,為了提高軟件系統的可維護性和可複用性,增加軟件的可擴展性和靈活性,程序員要盡量根據6條原則來開發程序,從而提高軟件開發效率、節約軟件開發成本和維護成本。
1.1 開閉原則
對擴展開放,對修改關閉。在程序需要進行拓展的時候,不能去修改原有的代碼,實現一個熱插拔的效果。簡言之,是為了使程序的擴展性好,易於維護和昇級。
想要達到這樣的效果,我們需要使用接口和抽象類。
因為抽象靈活性好,適應性廣,只要抽象的合理,可以基本保持軟件架構的穩定。而軟件中易變的細節可以從抽象派生來的實現類來進行擴展,當軟件需要發生變化時,只需要根據需求重新派生一個實現類來擴展就可以了。
下面以 搜狗輸入法
的皮膚為例介紹開閉原則的應用。
【例】搜狗輸入法
的皮膚設計。
分析:搜狗輸入法
的皮膚是輸入法背景圖片、窗口顏色和聲音等元素的組合。用戶可以根據自己的喜愛更換自己的輸入法的皮膚,也可以從網上下載新的皮膚。這些皮膚有共同的特點,可以為其定義一個抽象類(AbstractSkin),而每個具體的皮膚(DefaultSpecificSkin和HeimaSpecificSkin)是其子類。用戶窗體可以根據需要選擇或者增加新的主題,而不需要修改原代碼,所以它是滿足開閉原則的。
// 抽象皮膚類
public abstract class AbstractSkin {
//顯示的方法
public abstract void display();
}
//默認皮膚類
public class DefaultSkin extends AbstractSkin {
public void display() {
System.out.println("默認皮膚");
}
}
// 黑馬程序員皮膚
public class HeimaSkin extends AbstractSkin {
public void display() {
System.out.println("黑馬皮膚");
}
}
//搜狗輸入法
public class SougouInput {
private AbstractSkin skin;
public void setSkin(AbstractSkin skin) {
this.skin = skin;
}
public void display() {
skin.display();
}
}
//
public class Client {
public static void main(String[] args) {
//1,創建搜狗輸入法對象
SougouInput input = new SougouInput();
//2,創建皮膚對象
//DefaultSkin skin = new DefaultSkin();
HeimaSkin skin = new HeimaSkin();
//3,將皮膚設置到輸入法中
input.setSkin(skin);
//4,顯示皮膚
input.display();
}
}
1.2 裏氏代換原則
裏氏代換原則是面向對象設計的基本原則之一。
裏氏代換原則:任何基類可以出現的地方,子類一定可以出現。通俗理解:子類可以擴展父類的功能,但不能改變父類原有的功能。換句話說,子類繼承父類時,除添加新的方法完成新增功能外,盡量不要重寫父類的方法。
如果通過重寫父類的方法來完成新的功能,這樣寫起來雖然簡單,但是整個繼承體系的可複用性會比較差,特別是運用多態比較頻繁時,程序運行出錯的概率會非常大。
下面看一個裏氏替換原則中經典的一個例子
【例】正方形不是長方形。
在數學領域裏,正方形毫無疑問是長方形,它是一個長寬相等的長方形。所以,我們開發的一個與幾何圖形相關的軟件系統,就可以順理成章的讓正方形繼承自長方形。
代碼如下:
長方形類(Rectangle):
public class Rectangle {
private double length;
private double width;
public double getLength() {
return length;
}
public void setLength(double length) {
this.length = length;
}
public double getWidth() {
return width;
}
public void setWidth(double width) {
this.width = width;
}
}
正方形(Square):
由於正方形的長和寬相同,所以在方法setLength和setWidth中,對長度和寬度都需要賦相同值。
public class Square extends Rectangle {
public void setWidth(double width) {
super.setLength(width);
super.setWidth(width);
}
public void setLength(double length) {
super.setLength(length);
super.setWidth(length);
}
}
類RectangleDemo是我們的軟件系統中的一個組件,它有一個resize方法依賴基類Rectangle,resize方法是RectandleDemo類中的一個方法,用來實現寬度逐漸增長的效果。
public class RectangleDemo {
public static void resize(Rectangle rectangle) {
while (rectangle.getWidth() <= rectangle.getLength()) {
rectangle.setWidth(rectangle.getWidth() + 1);
}
}
//打印長方形的長和寬
public static void printLengthAndWidth(Rectangle rectangle) {
System.out.println(rectangle.getLength());
System.out.println(rectangle.getWidth());
}
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.setLength(20);
rectangle.setWidth(10);
resize(rectangle);
printLengthAndWidth(rectangle);
System.out.println("============");
Rectangle rectangle1 = new Square();
rectangle1.setLength(10);
resize(rectangle1);
printLengthAndWidth(rectangle1);
}
}
分析:我們運行一下這段代碼就會發現,假如我們把一個普通長方形作為參數傳入resize方法,就會看到長方形寬度逐漸增長的效果,當寬度大於長度,代碼就會停止,這種行為的結果符合我們的預期;假如我們再把一個正方形作為參數傳入resize方法後,就會看到正方形的寬度和長度都在不斷增長,代碼會一直運行下去,直至系統產生溢出錯誤。所以,普通的長方形是適合這段代碼的,正方形不適合。
結論:在resize方法中,Rectangle類型的參數是不能被Square類型的參數所代替,如果進行了替換就得不到預期結果。因此,Square類和Rectangle類之間的繼承關系違反了裏氏代換原則,它們之間的繼承關系不成立,正方形不是長方形。
如何改進呢?此時我們需要重新設計他們之間的關系。抽象出來一個四邊形接口(Quadrilateral),讓Rectangle類和Square類實現Quadrilateral接口
public interface Quadrilateral {
//獲取長
double getLength();
//獲取寬
double getWidth();
}
public class Rectangle implements Quadrilateral {
private double length;
private double width;
public void setLength(double length) {
this.length = length;
}
public void setWidth(double width) {
this.width = width;
}
public double getLength() {
return length;
}
public double getWidth() {
return width;
}
}
public class Square implements Quadrilateral {
private double side;
public double getSide() {
return side;
}
public void setSide(double side) {
this.side = side;
}
public double getLength() {
return side;
}
public double getWidth() {
return side;
}
}
public class RectangleDemo {
public static void main(String[] args) {
//創建長方形對象
Rectangle r = new Rectangle();
r.setLength(20);
r.setWidth(10);
//調用方法進行擴寬操作
resize(r);
printLengthAndWidth(r);
}
//擴寬的方法
public static void resize(Rectangle rectangle) {
//判斷寬如果比長小,進行擴寬的操作
while(rectangle.getWidth() <= rectangle.getLength()) {
rectangle.setWidth(rectangle.getWidth() + 1);
}
}
//打印長和寬
public static void printLengthAndWidth(Quadrilateral quadrilateral) {
System.out.println(quadrilateral.getLength());
System.out.println(quadrilateral.getWidth());
}
}
1.3 依賴倒轉原則
高層模塊不應該依賴低層模塊,兩者都應該依賴其抽象;抽象不應該依賴細節,細節應該依賴抽象。簡單的說就是要求對抽象進行編程,不要對實現進行編程,這樣就降低了客戶與實現模塊間的耦合。
下面看一個例子來理解依賴倒轉原則
【例】組裝電腦
現要組裝一臺電腦,需要配件cpu,硬盤,內存條。只有這些配置都有了,計算機才能正常的運行。選擇cpu有很多選擇,如Intel,AMD等,硬盤可以選擇希捷,西數等,內存條可以選擇金士頓,海盜船等。
類圖如下:
代碼如下:
希捷硬盤類(XiJieHardDisk):
public class XiJieHardDisk implements HardDisk {
public void save(String data) {
System.out.println("使用希捷硬盤存儲數據" + data);
}
public String get() {
System.out.println("使用希捷希捷硬盤取數據");
return "數據";
}
}
Intel處理器(IntelCpu):
public class IntelCpu implements Cpu {
public void run() {
System.out.println("使用Intel處理器");
}
}
金士頓內存條(KingstonMemory):
public class KingstonMemory implements Memory {
public void save() {
System.out.println("使用金士頓作為內存條");
}
}
電腦(Computer):
public class Computer {
private XiJieHardDisk hardDisk;
private IntelCpu cpu;
private KingstonMemory memory;
public IntelCpu getCpu() {
return cpu;
}
public void setCpu(IntelCpu cpu) {
this.cpu = cpu;
}
public KingstonMemory getMemory() {
return memory;
}
public void setMemory(KingstonMemory memory) {
this.memory = memory;
}
public XiJieHardDisk getHardDisk() {
return hardDisk;
}
public void setHardDisk(XiJieHardDisk hardDisk) {
this.hardDisk = hardDisk;
}
public void run() {
System.out.println("計算機工作");
cpu.run();
memory.save();
String data = hardDisk.get();
System.out.println("從硬盤中獲取的數據為:" + data);
}
}
測試類(TestComputer):
測試類用來組裝電腦。
public class TestComputer {
public static void main(String[] args) {
Computer computer = new Computer();
computer.setHardDisk(new XiJieHardDisk());
computer.setCpu(new IntelCpu());
computer.setMemory(new KingstonMemory());
computer.run();
}
}
上面代碼可以看到已經組裝了一臺電腦,但是似乎組裝的電腦的cpu只能是Intel的,內存條只能是金士頓的,硬盤只能是希捷的,這對用戶肯定是不友好的,用戶有了機箱肯定是想按照自己的喜好,選擇自己喜歡的配件。
根據依賴倒轉原則進行改進:
代碼我們只需要修改Computer類,讓Computer類依賴抽象(各個配件的接口),而不是依賴於各個組件具體的實現類。
類圖如下:
電腦(Computer):**
public class Computer {
private HardDisk hardDisk;
private Cpu cpu;
private Memory memory;
public HardDisk getHardDisk() {
return hardDisk;
}
public void setHardDisk(HardDisk hardDisk) {
this.hardDisk = hardDisk;
}
public Cpu getCpu() {
return cpu;
}
public void setCpu(Cpu cpu) {
this.cpu = cpu;
}
public Memory getMemory() {
return memory;
}
public void setMemory(Memory memory) {
this.memory = memory;
}
public void run() {
System.out.println("計算機工作");
}
}
面向對象的開發很好的解决了這個問題,一般情况下抽象的變化概率很小,讓用戶程序依賴於抽象,實現的細節也依賴於抽象。即使實現細節不斷變動,只要抽象不變,客戶程序就不需要變化。這大大降低了客戶程序與實現細節的耦合度。
1.4 接口隔離原則
客戶端不應該被迫依賴於它不使用的方法;一個類對另一個類的依賴應該建立在最小的接口上。
下面看一個例子來理解接口隔離原則
【例】安全門案例
我們需要創建一個黑馬
品牌的安全門,該安全門具有防火、防水、防盜的功能。可以將防火,防水,防盜功能提取成一個接口,形成一套規範。類圖如下:
上面的設計我們發現了它存在的問題,黑馬品牌的安全門具有防盜,防水,防火的功能。現在如果我們還需要再創建一個傳智品牌的安全門,而該安全門只具有防盜、防水功能呢?很顯然如果實現SafetyDoor接口就違背了接口隔離原則,那麼我們如何進行修改呢?看如下類圖:
代碼如下:
AntiTheft(接口):
public interface AntiTheft {
void antiTheft();
}
Fireproof(接口):
public interface Fireproof {
void fireproof();
}
Waterproof(接口):
public interface Waterproof {
void waterproof();
}
HeiMaSafetyDoor(類):
public class HeiMaSafetyDoor implements AntiTheft,Fireproof,Waterproof {
public void antiTheft() {
System.out.println("防盜");
}
public void fireproof() {
System.out.println("防火");
}
public void waterproof() {
System.out.println("防水");
}
}
ItcastSafetyDoor(類):
public class ItcastSafetyDoor implements AntiTheft,Fireproof {
public void antiTheft() {
System.out.println("防盜");
}
public void fireproof() {
System.out.println("防火");
}
}
Client(類)
public class Client {
public static void main(String[] args) {
//創建黑馬安全門對象
HeimaSafetyDoor door = new HeimaSafetyDoor();
//調用功能
door.antiTheft();
door.fireProof();
door.waterProof();
System.out.println("============");
//創建傳智安全門對象
ItcastSafetyDoor door1 = new ItcastSafetyDoor();
//調用功能
door1.antiTheft();
door1.fireproof();
}
}
1.5 迪米特法則
迪米特法則又叫最少知識原則。
只和你的直接朋友交談,不跟“陌生人”說話(Talk only to your immediate friends and not to strangers)。
其含義是:如果兩個軟件實體無須直接通信,那麼就不應當發生直接的相互調用,可以通過第三方轉發該調用。其目的是降低類之間的耦合度,提高模塊的相對獨立性。
迪米特法則中的“朋友”是指:當前對象本身、當前對象的成員對象、當前對象所創建的對象、當前對象的方法參數等,這些對象同當前對象存在關聯、聚合或組合關系,可以直接訪問這些對象的方法。
下面看一個例子來理解迪米特法則
【例】明星與經紀人的關系實例
明星由於全身心投入藝術,所以許多日常事務由經紀人負責處理,如和粉絲的見面會,和媒體公司的業務洽淡等。這裏的經紀人是明星的朋友,而粉絲和媒體公司是陌生人,所以適合使用迪米特法則。
類圖如下:
代碼如下:
明星類(Star)
public class Star {
private String name;
public Star(String name) {
this.name=name;
}
public String getName() {
return name;
}
}
粉絲類(Fans)
public class Fans {
private String name;
public Fans(String name) {
this.name=name;
}
public String getName() {
return name;
}
}
媒體公司類(Company)
public class Company {
private String name;
public Company(String name) {
this.name=name;
}
public String getName() {
return name;
}
}
經紀人類(Agent)
public class Agent {
private Star star;
private Fans fans;
private Company company;
public void setStar(Star star) {
this.star = star;
}
public void setFans(Fans fans) {
this.fans = fans;
}
public void setCompany(Company company) {
this.company = company;
}
//和粉絲見面的方法
public void meeting() {
System.out.println(fans.getName() + "與明星" + star.getName() + "見面了。");
}
//和媒體公司洽談的方法
public void business() {
System.out.println(company.getName() + "與明星" + star.getName() + "洽淡業務。");
}
}
Client(類):
public class Client {
public static void main(String[] args) {
//創建經紀人類
Agent agent = new Agent();
//創建明星對象
Star star = new Star("林青霞");
agent.setStar(star);
//創建粉絲對象
Fans fans = new Fans("李四");
agent.setFans(fans);
//創建媒體公司對象
Company company = new Company("黑馬媒體公司");
agent.setCompany(company);
agent.meeting();//和粉絲見面
agent.business();//和媒體公司洽談業務
}
}
1.6 合成複用原則
合成複用原則是指:盡量先使用組合或者聚合等關聯關系來實現,其次才考慮使用繼承關系來實現。
通常類的複用分為繼承複用和合成複用兩種。
繼承複用雖然有簡單和易實現的優點,但它也存在以下缺點:
- **繼承複用破壞了類的封裝性。**因為繼承會將父類的實現細節暴露給子類,父類對子類是透明的,所以這種複用又稱為“白箱”複用。
- 子類與父類的耦合度高。父類的實現的任何改變都會導致子類的實現發生變化,這不利於類的擴展與維護。
- 它限制了複用的靈活性。從父類繼承而來的實現是靜態的,在編譯時已經定義,所以在運行時不可能發生變化。
采用組合或聚合複用時,可以將已有對象納入新對象中,使之成為新對象的一部分,新對象可以調用已有對象的功能,它有以下優點:
- 它維持了類的封裝性。因為成分對象的內部細節是新對象看不見的,所以這種複用又稱為“黑箱”複用。
- 對象間的耦合度低。可以在類的成員比特置聲明抽象。
- **複用的靈活性高。**這種複用可以在運行時動態進行,新對象可以動態地引用與成分對象類型相同的對象。
下面看一個例子來理解合成複用原則
【例】汽車分類管理程序
汽車按“動力源”劃分可分為汽油汽車、電動汽車等;按“顏色”劃分可分為白色汽車、黑色汽車和紅色汽車等。如果同時考慮這兩種分類,其組合就很多。類圖如下:
從上面類圖我們可以看到使用繼承複用產生了很多子類,如果現在又有新的動力源或者新的顏色的話,就需要再定義新的類。我們試著將繼承複用改為聚合複用看一下。
再次證明實例:
- 繼承複用
- 合成複用
版权声明
本文为[@阿清]所创,转载请带上原文链接,感谢
https://yzsam.com/2022/04/202204230539031531.html
边栏推荐
- 提升Facebook触及率和互动率攻略 | 智能客服帮您抓住用户的心
- Find the number of "blocks" in the matrix (BFS)
- SQL语句简单优化
- Typescript interface & type rough understanding
- Frequently asked interview questions - 2 (computer network)
- Utf8 to STD: string and STD: string to utf8
- Breadth first search topics (BFS)
- refused connection
- Pilotage growth · ingenuity empowerment -- yonmaster developer training and pilotage plan is fully launched
- Establish excel bookkeeping book through setting context menu
猜你喜欢
Interview Basics
橙单微服务之批量导入
C语言——恶搞关机小程序
Hongji micro classroom | cyclone RPA's "flexible digital employee" actuator
[no title] Click the classification jump page to display the details
Three methods of list rendering
Create a tabbar component under the components folder, which is public
what is wifi6?
Arithmetic and logical operations
C# ,类库
随机推荐
Markdown syntax support test
Differences between auto and decltype inference methods (learning notes)
Qwebsocket communication
XXL job pit guide XXL RPC remoting error (connect timed out)
shell指令学习1
C语言——恶搞关机小程序
提升独立站转化率攻略 | 挽回弃购用户
Hongji micro classroom | cyclone RPA's "flexible digital employee" actuator
The QT debug version runs normally and the release version runs crash
Edit, cancel, pull up menu
Simple and basic use of switch and if
X86 assembly syntax: at & T and Intel
Understand the relationship between promise async await
Find the number of "blocks" in the matrix (BFS)
what is wifi6?
Why can't V-IF and V-for be used together
OSI层常用协议
Utf8 to STD: string and STD: string to utf8
热键,界面可视化配置(界面交互)
C# ,类库