Java編程中必知必會(huì)的五條SOLID原則
在面向?qū)ο缶幊蹋∣OP)領(lǐng)域,SOLID原則是類設(shè)計(jì)的指導(dǎo)準(zhǔn)則。這五個(gè)原則形成了一套規(guī)則和最佳實(shí)踐,開發(fā)人員在設(shè)計(jì)類結(jié)構(gòu)時(shí)應(yīng)遵循這些原則。通過理解和應(yīng)用這些原則,我們可以發(fā)揮出設(shè)計(jì)模式的潛力,創(chuàng)建強(qiáng)大的軟件架構(gòu)。
在本文中,我們介紹SOLID原則的核心內(nèi)容,并幫助您理解這些原則如何應(yīng)用到項(xiàng)目中。
核心目標(biāo):編寫可理解、無回歸和可測試的代碼
SOLID原則的核心目標(biāo)是構(gòu)建可理解、無回歸、可讀和可測試的代碼。這些代碼作為一個(gè)畫布,多個(gè)開發(fā)人員可以輕松地進(jìn)行協(xié)作,促進(jìn)生產(chǎn)力和創(chuàng)新的環(huán)境。
現(xiàn)在,我們逐個(gè)深入討論SOLID原則,揭示它們?cè)谒茉祛愒O(shè)計(jì)中的重要性。
- S — 單一責(zé)任原則
- O — 開放封閉原則
- L — 里氏替換原則
- I — 接口隔離原則
- D — 依賴反轉(zhuǎn)原則
1. 單一責(zé)任原則
SOLID的第一個(gè)支柱,單一責(zé)任原則(SRP),強(qiáng)調(diào)一個(gè)類應(yīng)該只有一個(gè)變化的原因。遵循這個(gè)原則,我們確保每個(gè)類只負(fù)責(zé)一個(gè)任務(wù),可使將來的維護(hù)和修改更容易。
/*手機(jī)類,具有屬性和構(gòu)造函數(shù)*/
Class MobilePhone{
String brandName;
Float price;
Date manufactureDate;
public MobilePhone(String brandName,Float price,Date manufactureDate){
this.brandName=brandName;
this.price=price;
this.manufactureDate=manufactureDate;
}
};
/*Invoice類擁有一個(gè)手機(jī)(mobile phone)和該手機(jī)的數(shù)量(quantity)*/
Class Invoice{
private MobilePhone mPhone;
int quantity;
public Invoice(MobilePhone mPhone,quantity){
this.mPhone=mPhone;
this.quantity=quantity;
}
public float calculateTotalPrice(){
return mPhone.price*this.quantity;//返回發(fā)票的總金額
}
public void printInvoice(){
//打印發(fā)票的邏輯
}
public void sendNotification(){
//發(fā)送通知的邏輯
}
}
以上代碼,與計(jì)算、打印或通知邏輯相關(guān)的任何更改都需要修改Invoice類。因此,發(fā)票類缺乏明確的關(guān)注點(diǎn)分離,對(duì)一個(gè)方面的修改會(huì)影響到其他功能。為了遵循SRP,關(guān)鍵是將Invoice類重構(gòu)為更小、更專注的類, 每個(gè)類都獨(dú)立處理特定的職責(zé),比如計(jì)算、打印或通知邏輯。
根據(jù)職責(zé)將代碼分離成獨(dú)立的類是遵循單一責(zé)任原則(SRP)并促進(jìn)可維護(hù)和靈活的代碼庫的正確方法。
在這種設(shè)計(jì)中,我們應(yīng)該有:
- Invoice類只包含計(jì)算邏輯。
- InvoicePrint類只包含打印邏輯。
- InvoiceNotify類只包含發(fā)送通知的邏輯。
/*手機(jī)類,具有屬性和構(gòu)造函數(shù)*/
Class MobilePhone{
String brandName;
Float price;
Date manufactureDate;
public MobilePhone(String brandName,Float price,Date manufactureDate){
this.brandName=brandName;
this.price=price;
this.manufactureDate=manufactureDate;
}
};
/*發(fā)票類,擁有手機(jī)和數(shù)量屬性*/
Class Invoice{
private MobilePhone mPhone;
int quantity;
public Invoice(MobilePhone mPhone,quantity){
this.mPhone=mPhone;
this.quantity=quantity;
}
public float calculateTotalPrice(){
return mPhone.price*this.quantity;//返回發(fā)票的總金額
}
}
Class InvoicePrint{
private Invoice invoice;
public InvoicePrint(Invoice invoice){
this.invoice=invoice
}
public void printInvoice(){
//打印發(fā)票的邏輯
}
}/*如果打印邏輯發(fā)生變化,只有InvoicePrint類會(huì)發(fā)生變化。*/
Class InvoiceNotify{
private Invoice invoice;
public InvoiceNotify(Invoice invoice){
this.invoice=invoice
public void sendNotification(){
//發(fā)送通知給用戶的邏輯
}
}/*如果通知邏輯發(fā)生變化,只有InvoiceNotify類會(huì)發(fā)生變化。*/
2. 開放-封閉原則
第二個(gè)原則是開放-封閉原則(OCP),它鼓勵(lì)軟件實(shí)體對(duì)擴(kuò)展開放,但對(duì)修改封閉。換句話說,一旦一個(gè)類被建立,它應(yīng)該能夠輕松地進(jìn)行擴(kuò)展,而不需要修改其現(xiàn)有的代碼。這促進(jìn)了代碼的重用和穩(wěn)定性。
讓我們以上面使用的InvoiceNotify類為例,InvoiceNotify類經(jīng)過測試,并且當(dāng)前在客戶端中實(shí)際使用,通過電子郵件發(fā)送發(fā)票通知。
現(xiàn)在有一個(gè)客戶需求,他們需要通過推送通知發(fā)送通知。
Class InvoiceNotify{
private Invoice invoice;
public InvoiceNotify(Invoice invoice){
this.invoice=invoice
public void sendNotification(){
//發(fā)送通知給用戶的邏輯
}
public void sendPushNotification(){
//發(fā)送推送通知給用戶的邏輯
}
}
以上代碼,通過在現(xiàn)有類中添加一個(gè)新方法,我們違反了開放/封閉原則
與其在現(xiàn)有類中添加一個(gè)新方法,我們應(yīng)該設(shè)計(jì)一個(gè)接口并在各個(gè)類中實(shí)現(xiàn)。
Interface InvoiceNotification{
public void sendNotification();
}
Class EmailNotification implements InvoiceNotification{
private Invoice invoice;
public EmailNotification(Invoice invoice){
this.invoice=invoice;
}
@Override
public void sendNotification(){
//通過電子郵件發(fā)送通知的邏輯
}
}
Class PushNotification implements InvoiceNotification{
private Invoice invoice;
public PushNotification(Invoice invoice){
this.invoice=invoice;
}
@Override
public void sendNotification(){
//發(fā)送推送通知的邏輯
}
}
如果進(jìn)一步增強(qiáng)需求,需要通過短信發(fā)送通知,無需修改現(xiàn)有類。相反,我們可以創(chuàng)建一個(gè)名為TextNotification的新類,它實(shí)現(xiàn)了InvoiceNotification接口并重寫了sendNotification()方法。這樣,我們就能夠順利地集成新功能,而不會(huì)破壞現(xiàn)有的代碼庫。
3. 里氏替換原則
里氏替換原則(LSP)定義了基類和派生類之間的契約。它規(guī)定派生類應(yīng)該能夠替代其基類,而不會(huì)影響程序的正確性。實(shí)質(zhì)上,遵循這個(gè)原則可以確保繼承被謹(jǐn)慎地使用,并保持類層次結(jié)構(gòu)的完整性。
例如:在數(shù)學(xué)中,正方形可以被歸類為矩形的一種特殊形式。它們之間的“是一個(gè)”關(guān)系可能會(huì)導(dǎo)致我們考慮在代碼中使用繼承來建模這種關(guān)系。然而,將正方形實(shí)現(xiàn)為矩形的派生類可能會(huì)導(dǎo)致意外的行為。
在數(shù)學(xué)中,正方形確實(shí)是矩形的一種特殊形式,正如“是一個(gè)”關(guān)系所暗示的那樣。這往往會(huì)引誘我們?cè)诖a中使用繼承來建模這種關(guān)系。然而,將正方形實(shí)現(xiàn)為矩形的派生類可能會(huì)導(dǎo)致意想不到和違反直覺的行為。
我們用一個(gè)簡單的Java代碼示例來說明這個(gè)問題:
class Rectangle {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
public int calculateArea() {
return width * height;
}
}
class Square extends Rectangle {
@Override
public void setWidth(int width) {
this.width = width;
this.height = width; // 正方形的邊長始終相等,所以兩個(gè)維度都設(shè)置為相同的值。
}
@Override
public void setHeight(int height) {
this.height = height;
this.width = height; // 正方形的邊長始終相等,所以兩個(gè)維度都設(shè)置為相同的值。
}
}
public class Main {
public static void main(String[] args) {
Rectangle rectangle = new Square();
rectangle.setWidth(5);
rectangle.setHeight(3);
System.out.println("Area: " + rectangle.calculateArea());
}
}
在這個(gè)例子中,我們有一個(gè)基類Rectangle,其中包含setWidth和setHeight方法,分別用于設(shè)置矩形的寬度和高度。Square類繼承Rectangle類,并重寫這些方法,以確保兩個(gè)維度保持相等,以保持正方形的特性。
在主方法中,我們創(chuàng)建一個(gè)Rectangle引用,指向一個(gè)Square對(duì)象。當(dāng)我們嘗試為寬度和高度設(shè)置不同的值(分別為5和3)時(shí),我們得到了一個(gè)邊長為3的正方形,而不是實(shí)際寬度為5、高度為3的矩形。因此,計(jì)算得到的面積(9)與我們期望從寬度為5、高度為3的矩形得到的面積不符。
這個(gè)場景展示了里氏替換原則被違反的情況,通過Rectangle引用使用Square對(duì)象導(dǎo)致了意外的行為。
為了解決Square繼承Rectangle的問題,我們需要重新評(píng)估繼承關(guān)系和類設(shè)計(jì)。一種方法是在這種情況下避免使用繼承,而是專注于公共接口或組合。我們用Java代碼來說明解決方案:
interface Shape {
int calculateArea();
}
class Rectangle implements Shape {
protected int width;
protected int height;
public void setWidth(int width) {
this.width = width;
}
public void setHeight(int height) {
this.height = height;
}
@Override
public int calculateArea() {
return width * height;
}
}
class Square implements Shape {
protected int side;
public void setSide(int side) {
this.side = side;
}
@Override
public int calculateArea() {
return side * side;
}
}
public class Main {
public static void main(String[] args) {
Shape rectangle = new Rectangle();
rectangle.setWidth(5);
rectangle.setHeight(3);
System.out.println("矩形面積: " + rectangle.calculateArea());
Shape square = new Square();
square.setSide(5);
System.out.println("正方形面積: " + square.calculateArea());
}
}
在這個(gè)解決方案中,我們引入了一個(gè)名為Shape的公共接口,定義了calculateArea()方法。現(xiàn)在,Rectangle和Square都實(shí)現(xiàn)了這個(gè)接口。Rectangle類保留了setWidth和setHeight方法,而Square類有一個(gè)setSide方法。每個(gè)類根據(jù)自己特定的屬性計(jì)算面積。
現(xiàn)在,在main方法中,我們?yōu)镽ectangle和Square對(duì)象分別創(chuàng)建了不同的Shape引用。我們可以適當(dāng)設(shè)置尺寸而不會(huì)遇到任何問題。
通過使用組合和共同接口,我們確保每個(gè)形狀都能獨(dú)立運(yùn)作,并且按預(yù)期運(yùn)行,而不違反里氏替換原則。這種設(shè)計(jì)使我們能夠優(yōu)雅地處理不同的形狀,促進(jìn)了更清晰和可維護(hù)的代碼庫。
4. 接口隔離原則
接口隔離原則(ISP)建議客戶端不應(yīng)被強(qiáng)迫依賴于它們不使用的接口。與其擁有龐大而笨重的接口,更好的做法是創(chuàng)建小而專注的接口,以滿足客戶端的特定需求。
讓我們通過一個(gè)簡單的Java代碼示例來說明ISP:
假設(shè)我們有一個(gè)名為Printer的接口,提供打印功能:
interface DocumentProcessor {
void print();
void fax();
}
class LaserPrinter implements DocumentProcessor {
@Override
public void print() {
System.out.println("Printing with a laser printer.");
}
@Override
public void fax() {
System.out.println("Sending a fax with a laser printer.");
}
}
class FaxMachine implements DocumentProcessor {
@Override
public void print() {
// 傳真機(jī)無法打印,所以將這個(gè)方法保持為空。
}
@Override
public void fax() {
System.out.println("Sending a fax with a fax machine.");
}
}
這個(gè)設(shè)計(jì)的問題在于FaxMachine類對(duì)于print()方法沒有有意義的實(shí)現(xiàn),因?yàn)閭髡鏅C(jī)無法打印文檔。盡管如此,F(xiàn)axMachine類仍然被強(qiáng)制實(shí)現(xiàn)print()方法,這是因?yàn)镈ocumentProcessor接口的設(shè)計(jì)。
這種對(duì)接口隔離原則的違反顯而易見,因?yàn)镕axMachine類現(xiàn)在需要實(shí)現(xiàn)它不需要或使用的方法。
5. 依賴反轉(zhuǎn)原則
SOLID原則的最后一塊拼圖是依賴反轉(zhuǎn)原則(Dependency Inversion Principle,DIP)。該原則主張高層模塊不應(yīng)依賴于低層模塊,而應(yīng)依賴于抽象。通過遵循這一原則,我們實(shí)現(xiàn)了解耦,從而增強(qiáng)了靈活性、可維護(hù)性和測試的便捷性。
讓我們通過一個(gè)小的Java代碼示例來說明違反依賴反轉(zhuǎn)原則的情況:
假設(shè)我們有一個(gè)ReportGenerator類,它直接依賴于一個(gè)DatabaseConnection類:
class DatabaseConnection {
public void connect() {
System.out.println("Connected to the database.");
}
public void executeQuery(String query) {
System.out.println("Executing query: " + query);
}
public void close() {
System.out.println("Connection closed.");
}
}
class ReportGenerator {
private DatabaseConnection databaseConnection;
public ReportGenerator() {
this.databaseConnection = new DatabaseConnection();
}
public void generateReport() {
databaseConnection.connect();
databaseConnection.executeQuery("SELECT * FROM data_table");
databaseConnection.close();
System.out.println("Report generated successfully.");
}
}
在這段代碼中,ReportGenerator類在其構(gòu)造函數(shù)中直接創(chuàng)建了一個(gè)DatabaseConnection實(shí)例。結(jié)果,ReportGenerator與DatabaseConnection緊密耦合。對(duì)DatabaseConnection類的任何更改都可能會(huì)影響到ReportGenerator。
為了解決這個(gè)問題,我們需要應(yīng)用依賴反轉(zhuǎn)原則,引入一個(gè)兩個(gè)類都依賴的接口:
interface Connection {
void connect();
void executeQuery(String query);
void close();
}
class DatabaseConnection implements Connection {
@Override
public void connect() {
System.out.println("Connected to the database.");
}
@Override
public void executeQuery(String query) {
System.out.println("Executing query: " + query);
}
@Override
public void close() {
System.out.println("Connection closed.");
}
}
class ReportGenerator {
private Connection connection;
public ReportGenerator(Connection connection) {
this.connection = connection;
}
public void generateReport() {
connection.connect();
connection.executeQuery("SELECT * FROM data_table");
connection.close();
System.out.println("Report generated successfully.");
}
}
public class Main {
public static void main(String[] args) {
Connection databaseConnection = new DatabaseConnection();
ReportGenerator reportGenerator = new ReportGenerator(databaseConnection);
reportGenerator.generateReport();
}
}
通過遵循依賴反轉(zhuǎn)原則,我們通過Connection接口解耦了ReportGenerator和DatabaseConnection類。這種方法允許我們?cè)诓恍薷腞eportGenerator的情況下輕松切換和擴(kuò)展Connection接口的實(shí)現(xiàn)。現(xiàn)在的代碼符合原則,更易于維護(hù)和靈活。
結(jié)論
SOLID原則是面向?qū)ο箢愒O(shè)計(jì)的基石,對(duì)于每個(gè)尋求創(chuàng)建高效、可維護(hù)和協(xié)作的軟件的開發(fā)人員來說至關(guān)重要。當(dāng)你踏上編碼之旅時(shí),請(qǐng)記住SOLID運(yùn)用原則!