軟件設(shè)計(jì)原理
軟件設(shè)計(jì)一直是開發(fā)周期中最重要的階段。您花更多的時(shí)間設(shè)計(jì)彈性和靈活的體系結(jié)構(gòu),將來在發(fā)生更改時(shí)會(huì)節(jié)省更多的時(shí)間。需求總是在變化–如果不定期添加或維護(hù)任何功能,則軟件將成為傳統(tǒng)–并且這些變化的成本取決于系統(tǒng)的結(jié)構(gòu)和體系結(jié)構(gòu)。
軟件設(shè)計(jì)一直是開發(fā)周期中最重要的階段。您花更多的時(shí)間設(shè)計(jì)彈性和靈活的體系結(jié)構(gòu),將來在發(fā)生更改時(shí)會(huì)節(jié)省更多的時(shí)間。需求總是在變化–如果不定期添加或維護(hù)任何功能,則軟件將成為傳統(tǒng)–并且這些變化的成本取決于系統(tǒng)的結(jié)構(gòu)和體系結(jié)構(gòu)。在本文中,我們將討論有助于創(chuàng)建易于維護(hù)和擴(kuò)展的軟件的關(guān)鍵設(shè)計(jì)原則。
實(shí)際方案
假設(shè)老板要求您創(chuàng)建一個(gè)將Word文檔轉(zhuǎn)換為PDF的應(yīng)用程序。該任務(wù)看起來很簡單-您要做的就是查找一個(gè)可靠的庫,該庫將Word文檔轉(zhuǎn)換為PDF,并將其插入應(yīng)用程序中。經(jīng)過研究后,說您最終使用了 Aspose.words框架并創(chuàng)建了以下類:
/**
* A utility class which converts a word document to PDF
* @author Hussein
*
*/
public class PDFConverter {
/**
* This method accepts as input the document to be converted and
* returns the converted one.
* @param fileBytes
* @throws Exception
*/
public byte[] convertToPDF(byte[] fileBytes) throws Exception {
// We're sure that the input is always a WORD. So we just use
//aspose.words framework and do the conversion.
InputStream input = new ByteArrayInputStream(fileBytes);
com.aspose.words.Document wordDocument = new com.aspose.words.Document(input);
ByteArrayOutputStream pdfDocument = new ByteArrayOutputStream();
wordDocument.save(pdfDocument, SaveFormat.PDF);
return pdfDocument.toByteArray();
}
}
生活輕松,一切順利!
需求變更一如既往
幾個(gè)月后,一些客戶要求您也支持Excel文檔。因此,您進(jìn)行了一些研究,并決定使用Aspose.cells。然后,返回您的類,添加一個(gè)名為documentType的新字段,并按如下所示修改您的方法:
public class PDFConverter {
// we didn't mess with the existing functionality, by default
// the class will still convert WORD to PDF, unless the client sets
// this field to EXCEL.
public String documentType = "WORD";
/**
* This method accepts as input the document to be converted and
* returns the converted one.
* @param fileBytes
* @throws Exception
*/
public byte[] convertToPDF(byte[] fileBytes) throws Exception {
if (documentType.equalsIgnoreCase("WORD")) {
InputStream input = new ByteArrayInputStream(fileBytes);
com.aspose.words.Document wordDocument = new com.aspose.words.Document(input);
ByteArrayOutputStream pdfDocument = new ByteArrayOutputStream();
wordDocument.save(pdfDocument, SaveFormat.PDF);
return pdfDocument.toByteArray();
} else {
InputStream input = new ByteArrayInputStream(fileBytes);
Workbook workbook = new Workbook(input);
PdfSaveOptions saveOptions = new PdfSaveOptions();
saveOptions.setCompliance(PdfCompliance.PDF_A_1_B);
ByteArrayOutputStream pdfDocument = new ByteArrayOutputStream();
workbook.save(pdfDocument, saveOptions);
return pdfDocument.toByteArray();
}
}
}
該代碼將對新客戶端完全有效(并且仍將按預(yù)期對現(xiàn)有客戶端運(yùn)行),但是一些不良的設(shè)計(jì)異味開始出現(xiàn)在代碼中。這意味著我們沒有采用完美的方法,當(dāng)請求新的文檔類型時(shí),我們將無法輕松地修改類。
代碼重復(fù):如您所見,在if / else塊中正在重復(fù)類似的代碼,如果有一天我們設(shè)法支持不同的擴(kuò)展,那么我們將有很多重復(fù)。同樣,例如,如果稍后我們決定返回文件而不是byte [],那么我們必須在所有塊中進(jìn)行相同的更改。
剛性: 所有轉(zhuǎn)換算法都在同一方法內(nèi)耦合,因此,如果更改某些算法,則其他算法可能會(huì)受到影響。
固定性:上述方法直接取決于 documentType字段。一些客戶端會(huì)在調(diào)用convertToPDF()之前忘記設(shè)置該字段,因此他們將無法獲得預(yù)期的結(jié)果。另外,由于該方法依賴于字段,因此我們無法在其他任何項(xiàng)目中重用該方法。
高級(jí)模塊和框架之間的耦合: 如果我們以后出于某種目的決定用更可靠的框架替換Aspose框架,我們將最終修改整個(gè)PDFConverter 類-許多客戶端將受到影響。
用正確的方法做
通常,開發(fā)人員無法預(yù)測未來的變化,因此大多數(shù)開發(fā)人員將完全按照我們第一次實(shí)施的方式實(shí)施該應(yīng)用程序。但是,在進(jìn)行第一次更改之后,可以清楚地看到將來還會(huì)發(fā)生類似的更改。因此,優(yōu)秀的開發(fā)人員不會(huì)使用if / else塊對其進(jìn)行破解,而是以正確的方式進(jìn)行操作,以最大程度地減少未來更改的成本。因此,我們在暴露的工具(PDFConverter)和低級(jí)轉(zhuǎn)換算法之間創(chuàng)建一個(gè)抽象層,并將每種算法移到一個(gè)單獨(dú)的類中,如下所示:
/**
* This interface represents an abstract algorithm for converting
* any type of document to a PDF.
* @author Hussein
*
*/
public interface Converter {
public byte[] convertToPDF(byte[] fileBytes) throws Exception;
}
/**
* This class holds the algorithm for converting Excel
* documents to PDFs.
* @author Hussein
*
*/
public class ExcelPDFConverter implements Converter {
public byte[] convertToPDF(byte[] fileBytes) throws Exception {
InputStream input = new ByteArrayInputStream(fileBytes);
Workbook workbook = new Workbook(input);
PdfSaveOptions saveOptions = new PdfSaveOptions();
saveOptions.setCompliance(PdfCompliance.PDF_A_1_B);
ByteArrayOutputStream pdfDocument = new ByteArrayOutputStream();
workbook.save(pdfDocument, saveOptions);
return pdfDocument.toByteArray();
};
}
/**
* This class holds the algorithm for converting Word
* documents to PDFs.
* @author Hussein
*
*/
public class WordPDFConverter implements Converter {
@Override
public byte[] convertToPDF(byte[] fileBytes) throws Exception {
InputStream input = new ByteArrayInputStream(fileBytes);
com.aspose.words.Document wordDocument = new com.aspose.words.Document(input);
ByteArrayOutputStream pdfDocument = new ByteArrayOutputStream();
wordDocument.save(pdfDocument, SaveFormat.PDF);
return pdfDocument.toByteArray();
}
}
public class PDFConverter {
/**
* This method accepts the document to be converted as an input and
* returns the converted one.
* @param fileBytes
* @throws Exception
*/
public byte[] convertToPDF(Converter converter, byte[] fileBytes) throws Exception {
return converter.convertToPDF(fileBytes);
}
}
我們強(qiáng)制客戶端決定在調(diào)用convertToPDF()時(shí)使用哪種轉(zhuǎn)換算法。
這樣做的好處是什么?
關(guān)注點(diǎn)分離(高內(nèi)聚/低耦合):現(xiàn)在, PDFConverter類對應(yīng)用程序中使用的轉(zhuǎn)換算法一無所知。它的主要關(guān)注點(diǎn)是為客戶提供各種轉(zhuǎn)換功能,而不管轉(zhuǎn)換是如何進(jìn)行的?,F(xiàn)在我們可以替換低級(jí)轉(zhuǎn)換框架了,只要我們返回預(yù)期的結(jié)果,沒人會(huì)知道。
單一職責(zé):創(chuàng)建抽象層并將每個(gè)動(dòng)態(tài)行為移至一個(gè)單獨(dú)的類之后,我們實(shí)際上刪除了最初在最初設(shè)計(jì)中的convertToPDF()方法所承擔(dān)的多個(gè)職責(zé) ?,F(xiàn)在,它只需承擔(dān)一項(xiàng)職責(zé),即將客戶請求委托給抽象轉(zhuǎn)換層。而且,Converter接口的每個(gè)具體類 現(xiàn)在都具有與將某些文檔類型轉(zhuǎn)換為PDF有關(guān)的單一職責(zé)。結(jié)果,每個(gè)組件都有一個(gè)要修改的原因,因此沒有回歸。
打開/關(guān)閉應(yīng)用程序: 我們的應(yīng)用程序現(xiàn)已打開以進(jìn)行擴(kuò)展,并關(guān)閉以進(jìn)行修改。每當(dāng)我們想要添加對某些文檔類型的支持時(shí),我們只需從Converter接口創(chuàng)建一個(gè)新的具體類, 并且由于無需修改PDFConverter工具,新類型便會(huì)受到支持 ,因?yàn)槲覀兊墓ぞ攥F(xiàn)在依賴于抽象。
從本文中學(xué)到的設(shè)計(jì)原則
以下是構(gòu)建應(yīng)用程序體系結(jié)構(gòu)時(shí)應(yīng)遵循的一些最佳設(shè)計(jì)實(shí)踐。
將您的應(yīng)用程序劃分為幾個(gè)模塊,并在每個(gè)模塊的頂部添加一個(gè)抽象層。
在實(shí)現(xiàn)上偏愛抽象:始終確保依賴抽象層。這將使您的應(yīng)用程序打開以供將來擴(kuò)展。應(yīng)該將抽象應(yīng)用于應(yīng)用程序的動(dòng)態(tài)部分(最有可能定期更改),而不必應(yīng)用于每個(gè)部分,因?yàn)檫^度使用會(huì)使代碼復(fù)雜化。
識(shí)別應(yīng)用程序中各個(gè)方面,將其與保持不變的方面分開。
不要重復(fù)自己:始終將重復(fù)的功能放在某個(gè)實(shí)用程序類中,并使其可在整個(gè)應(yīng)用程序中訪問。這將使您的修改容易得多。
通過抽象層隱藏低級(jí)實(shí)現(xiàn):低級(jí)模塊很可能會(huì)定期更改,因此請將其與高級(jí)模塊分開。
每個(gè)類/方法/模塊都應(yīng)該有一個(gè)更改的原因,因此,為了使回歸最小化,請始終對每個(gè)類/方法/模塊負(fù)責(zé)。
關(guān)注點(diǎn)分離:每個(gè)模塊都知道另一個(gè)模塊的功能,但永遠(yuǎn)不應(yīng)該知道如何執(zhí)行。
