面向對象設計原則面試
概述spring
對於面向對象軟件系統的設計而言,在支持可維護性的同時,提升系統的可複用性是一個相當重要的問題,如何同時提升一個軟件系統的可維護性和可複用性是面向對象設計須要解決的核心問題之一。在面向對象設計中,可維護性的複用是以設計原則爲基礎的。每個原則都蘊含一些面向對象設計的思想,能夠從不一樣的角度提高一個軟件結構的設計水平。 面向對象設計原則爲支持可維護性複用而誕生,這些原則蘊含在不少設計模式中,它們是從許多設計方案中總結出的指導性原則。面向對象設計原則也是咱們用於評價一個設計模式的使用效果的重要指標之一,在設計模式的學習中,你們常常會看到諸如「XXX模式符合XXX原則」、「XXX模式違反了XXX原則」這樣的語句。編程
最多見的7種面向對象設計原則以下表所示:後端
1.單一職責原則設計模式
單一職責定義bash
一個類只負責一個功能領域中的相應職責,或者能夠定義爲:就一個類而言,應該只有一個引發它變化的緣由微信
從定義中不難思考,一個類的所作的事情越多,也就越難以複用,由於一旦作的事情多了,職責的耦合度就變高了因此咱們根據這個原則應該將不一樣職責封裝在不一樣類中,不一樣的變化封裝在不一樣類中。從咱們日常的開發中不難發現,若是一個類或者方法接口等等只作一件事,那麼可讀性很高,而且複用性也很高,而且一旦需求變化,也容易維護,假如你一個類糅雜多個職責,那麼很難維護。多線程
單一職責舉例分析架構
從實際業務來剝離一個例子:如今有這麼一種狀況,某租車平臺我的模塊類涉及多個方法,有以下登陸、註冊、支付寶押金支付、微信押金支付、支付寶套餐支付、微信套餐支付、整個結構以下:併發
/**
* 我的模塊
*/
@Controller
public class userController{
/**
* 登陸
*/
public void login(){
}
/**
* 註冊
*/
public void register(){
}
/**
* 押金支付(阿里)
*/
public void payAliDeposit(){
}
/**
* 押金支付(微信)
*/
public void payWXDeposit(){
}
/**
* 套餐支付(阿里)
*/
public void payAliPackage(){
}
/**
* 套餐支付(微信)
*/
public void payWXPackage(){
}
}複製代碼
咱們能夠看到不少功能都糅雜在一塊兒,一個類作了那麼多事情,很臃腫,別提維護,就連找代碼都很困難,因此咱們能夠對這個UserController進行拆解,與此同時咱們應該分包,好比這個應該在xxx.xxx.userMoudule下面,可能支付相關的有公共的方法,登陸抑或也有公共的方法,那邊抽成公共服務去調用。
public class LoginController(){}
public class registerController(){}
public class depositPayController(){
// 支付寶支付
// 微信支付
}
public class packagePayController(){
// 支付寶支付
// 微信支付
}複製代碼
整個方案實現的目的就是爲了解決高耦合,代碼複用率低下的問題。單一職責理解起來不難,可是實際操做須要根據具體業務的糅雜度來切割,實際上很難運用。
2.開閉原則
開閉原則簡介
開閉原則是面向對象的可複用設計的第一塊基石,它是最重要的面向對象設計原則,定義以下:
一個軟件實體應當對擴展開放,對修改關閉。即軟件實體應儘可能在不修改原有代碼的狀況下進行擴展。
軟件實體包括如下幾個部分:
注意:開閉原則是指對擴展開放,對修改關閉,並非說不作任何的修改。
開閉原則的優點
如何使用開閉原則
案例
某公司開發的租車系統有一個押金支付功能,支付方式有支付寶、阿里支付,後期可能還有銀聯支付、易支付等等,原始的設計方案以下:
// 客戶端調用-押金支付選擇支付手段
public class DepositPay {
void pay(String type){
if(type.equals("ali")){
AliPay aliPay = new AliPay();
aliPay.pay();
}else if(type.equals("wx")){
WXPay wxPay = new WXPay();
wxPay.pay();
}
}
}
// 支付寶支付
public class AliPay {
public void pay() {
System.out.println("正在使用支付寶支付");
}
}
// 微信支付
public class WXPay{
public void pay() {
System.out.println("正在使用微信支付");
}
}複製代碼
在以上代碼中,若是須要增長銀聯支付,如YLPay,那麼就必需要修改DepositPay中的pay方法的源代碼,增長新的判斷邏輯,違反了開閉原則(對修改關閉,對擴展開放,注意這邊的銀聯支付至關於擴展,因此它沒有違反規則),因此如今必須重構此代碼,讓其遵循開閉原則,作法以下:
重構後的圖以下所示:
在上圖中咱們引入了接口Pay,定義了pay方法,而且DepositPay是針對接口編程,經過setPayMode()由客戶端來實例化具體的支付方式,在DepositPay的pay()方法中調用payMode對象來支付。若是須要增長新的支付方式,好比銀聯支付,只須要讓它也實現Pay接口,在配置文件中配置銀聯支付便可,依賴注入是實現此開閉原則的一種手段,在這裏不贅述,源碼以下:
public interface Pay {
// 支付
void pay();
}
public class AliPay implements Pay {
@Override
public void pay() {
System.out.println("正在使用支付寶支付");
}
}
public class WXPay implements Pay{
@Override
public void pay() {
System.out.println("正在使用微信支付");
}
}
// 客戶端調用-押金支付選擇支付手段
public class DepositPay {
// 支付方式 (這邊能夠經過依賴注入的方式來注入)
// 支付方式能夠寫在配置文件中
// 如今無論你選用何種方式,我都不須要更改
@Autowired
Pay payMode;
void pay(Pay payMode){
payMode.pay();
}
}複製代碼
由於配置文件能夠直接編輯,且不須要編譯,因此通常不認爲更改配置文件是更改源碼。若是一個系統能作到只須要修改配置文件,無需修改源碼,那麼複合開閉原則。
3.里氏代換原則
里氏替換原則簡介
Barbara Liskov提出:
標準定義:若是對每個類型爲S的對象o1,都有類型爲T的對象o2,使得以T定義的全部程序P在全部的對象o1代換o2時,程序P的行爲沒有變化,那麼類型S是類型T的子類型。
上面的定義可能比較難以理解,簡單理解就是全部引用基類(父類的)地方均可以用子類來替換,且程序不會有任何的異常。可是反過來就不行,全部使用子類的地方則不必定能用基類來替代,很簡單的例子狗是動物,不能說動物是狗,由於可能還有貓。。。。
里氏替換原則是實現開閉原則的重要方式之一,因爲使用基類的全部地方均可以用子類來替換,所以在程序中儘可能使用基類來定義對象,在運行時肯定其子類類型。
里氏替換原則約束
因此咱們在運用里氏替換原則的時候,儘可能把父類設計爲抽象類或者接口,讓子類繼承父類或者實現接口並實如今父類中聲明的方法,運行時,子類實例替換父類實例,咱們能夠很方便地擴展系統的功能,同時無須修改原有子類的代碼,增長新的功能能夠經過增長一個新的子類來實現。里氏代換原則是開閉原則的具體實現手段之一。
里氏替換原則實戰
某租車系統客戶分爲普通用戶(customer)和VIP客戶(VIPCustomer),系統須要提供一個根據郵箱重置密碼的功能。原始設計圖:
在編寫重置密碼的時候發現,業務邏輯是同樣的,存在着大量的重複代碼,並且還可能增長新的用戶類型,爲了減小代碼重複性,使用里氏替換原則進行重構:
圖上重置密碼交由ResetPassword類去處理,只須要傳入Customer類便可,無論任何類型的Customer類,只要繼承自Customer,均可以使用里氏替換原則進行替換,假若有新的類型,咱們只須要在配置文件中注入新的類型便可。代碼以下(簡單意會一下):
// 抽象基類
public abstract class Customer {
}
public class CommonCustomer extends Customer{
}
public class VIPCustomer extends Customer{
}
// 重置密碼邏輯在這裏實現,只須要傳入對應的類型便可
public class ResetPassword {
void resetPassword(Customer customer){
}
}複製代碼
里氏替換原則是實現開閉原則不可或缺的手段之一,在本例中,經過傳遞參數使用基類對象,針對抽象編程,從而知足開閉原則。
4.依賴倒轉原則
依賴倒轉原則簡介
依賴倒轉原則(Dependency Inversion Principle, DIP):抽象不該該依賴於細節,細節應當依賴於抽象。換言之,要針對接口編程,而不是針對實現編程。
能夠通俗的定義爲兩種:
要求咱們在設計程序的時候儘可能使用層次高的抽象層類,即便用接口和抽象類進行變量的聲明、參數類型聲明、方法返回類型聲明以及數據類型轉換等等,同時要注意一個具體類應該只實現抽象類或者接口中存在的方法,不要給出多餘的方法,這樣抽象類將沒法調用子類增長的方法.咱們能夠經過配置文件來寫入具體類,這樣一旦程序行爲改變,可直接改變配置文件,而不須要更改程序,從新編譯,經過依賴倒轉原則來知足開閉原則。
在實現依賴倒轉原則時,咱們須要針對抽象層編程,而將具體類的對象經過依賴注入(DependencyInjection, DI)的方式注入到其餘對象中,依賴注入是指當一個對象要與其餘對象發生依賴關係時,經過抽象來注入所依賴的對象。經常使用的注入方式有三種,分別是:構造注入,設值注入(Setter注入)和接口注入
依賴倒轉原則實例
這部分能夠參照上面開閉原則案例,能夠從那例子中看出,開閉原則,依賴倒轉原則,里氏替換原則同時出現了,能夠說`開閉原則是咱們要實現的目標,而里氏替換原則是實現手段之一,而同時里氏替換原則又是依賴倒轉原則實現的基礎,由於加入沒有這個理論,依賴倒轉原則是不成立的,沒法針對抽象編程,要注意這3個原則基本都是同時出現的。Java後端學習交流圈:834962734 面向2-6年Java開發人員,進羣可免費獲取一份Java架構進階技術精品視頻。(高併發+Spring源碼+JVM原理解析+分佈式架構+微服務架構+多線程併發原理+BATJ面試寶典)以及架構思惟導圖。
5.接口隔離原則
接口隔離原則簡介
接口隔離原則的兩個定義:
1:使用多個專門的接口,而不使用單一的總接口,即客戶端不該該依賴那些它不須要的接口
2:類間的依賴關係應該創建在最小的接口上
接口的含義:
根據接口隔離原則,咱們可明白,每一個接口都應只承擔一種相對獨立的角色,不幹不應乾的事情.
實例演示
場景:模擬動物平時的動做,固然也包括人,最初的設計就是一個總接口IAnimal,裏面定義動物會有的一些動做。
代碼以下:
public interface IAnimal{
/**
* 吃飯
*/
void eat();
/**
* 工做
*/
void work();
/**
* 飛行
*/
void fly();
}
public class Tony implements IAnimal{
@Override
public void eat() {
System.out.println("tony吃");
}
@Override
public void work() {
System.out.println("tony工做");
}
@Override
public void fly() {
System.out.println("tony不會飛");
}
}
public class Bird implements IAnimal{
@Override
public void eat() {
System.out.println("鳥吃");
}
@Override
public void work() {
System.out.println("鳥工做");
}
@Override
public void fly() {
System.out.println("鳥飛");
}
}複製代碼
根據上面的寫法發現Tony須要實現飛的接口,這很明顯不只僅是多餘,並且不合理,所以須要經過接口隔離原則進行重構:
/**
* 抽象動物的行爲
*/
public interface IAnimal {
/**
* 吃飯
*/
void eat();
/**
* 睡覺
*/
void sleep();
}
/**
* 高級動物人 的行爲
*/
public interface IAdvancedAnimalBehavior {
/**
* 打牌
*/
void playCard();
/**
* 騎車
*/
void byBike();
}
/**
* 低級動物的行爲
*/
public interface IJuniorAnimalBehavior {
/**
* fly
*/
void fly();
}
/**
* 實現高級動物人的共通方法
*/
public class AbstractAdvancedAnimal implements IAnimal {
@Override
public void eat() {
System.out.println("人吃");
}
@Override
public void sleep() {
System.out.println("人睡");
}
}
/**
* 實現低級動物人的共通方法
*/
public class AbstractJuniorAnimal implements IAnimal {
@Override
public void eat() {
System.out.println("動物吃");
}
@Override
public void sleep() {
System.out.println("動物睡");
}
}
// tony
public class Tony extends AbstractAdvancedAnimal implements IAdvancedAnimalBehavior {
@Override
public void playCard() {
System.out.println("tony打牌");
}
@Override
public void byBike() {
System.out.println("tony騎車");
}
}
// 鳥
public class Bird extends AbstractJuniorAnimal implements IJuniorAnimalBehavior{
@Override
public void fly() {
System.out.println("鳥飛");
}
}複製代碼
重構以後,首先定義了一個總的動物接口的大類,而後分別使用了兩個抽象類(一個是高級動物,一個是低級動物)分別去實現這些公共的方法,實現中能夠拋出異常,代表繼承此抽象類的類能夠選擇性的重寫,可不重寫。以後再定義了兩個行爲接口代表高級動物和低級動物所特有的,這樣使得接口之間徹底隔離,動物接口再也不糅雜各類各樣的角色,固然接口的大小尺度仍是要靠經驗來調整,不能過小,會形成接口氾濫,也不能太大,會背離接口隔離原則。
6.合成複用原則
合成複用原則簡介
合成複用原則(Composite Reuse Principle, CRP):儘可能使用對象組合,而不是繼承來達到複用的目的。
經過合成複用原則來使一些已有的對象使之成爲對象的一部分,通常經過組合/聚合關係來實現,而儘可能不要使用繼承。由於組合和聚合能夠下降類之間的耦合度,而繼承會讓系統更加複雜,最重要的一點會破壞系統的封裝性,由於繼承會把基類的實現細節暴露給子類,同時若是基類變化,子類也必須跟着改變,並且耦合度會很高。