設計模式 | 外觀模式及典型應用

小旋鋒發表於2019-03-01

前言

本文的主要內容:

  • 介紹外觀模式
  • 示例
    • 自己泡茶
    • 到茶館喝茶
  • 外觀模式總結
  • 外觀模式的典型應用
    • spring JDBC 中的外觀模式
    • Mybatis中的外觀模式
    • Tomcat 中的外觀模式
    • SLF4J 中的外觀模式

外觀模式

外觀模式是一種使用頻率非常高的結構型設計模式,它通過引入一個外觀角色來簡化客戶端與子系統之間的互動,為複雜的子系統呼叫提供一個統一的入口,降低子系統與客戶端的耦合度,且客戶端呼叫非常方便。

外觀模式又稱為門面模式,它是一種物件結構型模式。外觀模式是迪米特法則的一種具體實現,通過引入一個新的外觀角色可以降低原有系統的複雜度,同時降低客戶類與子系統的耦合度。

外觀模式包含如下兩個角色:

Facade(外觀角色):在客戶端可以呼叫它的方法,在外觀角色中可以知道相關的(一個或者多個)子系統的功能和責任;在正常情況下,它將所有從客戶端發來的請求委派到相應的子系統去,傳遞給相應的子系統物件處理。

SubSystem(子系統角色):在軟體系統中可以有一個或者多個子系統角色,每一個子系統可以不是一個單獨的類,而是一個類的集合,它實現子系統的功能;每一個子系統都可以被客戶端直接呼叫,或者被外觀角色呼叫,它處理由外觀類傳過來的請求;子系統並不知道外觀的存在,對於子系統而言,外觀角色僅僅是另外一個客戶端而已。

外觀模式的目的不是給予子系統新增新的功能介面,而是為了讓外部減少與子系統內多個模組的互動,鬆散耦合,從而讓外部能夠更簡單地使用子系統。

外觀模式的本質是:封裝互動,簡化呼叫

示例

泡茶需要水 Water

public class Water {
    private int temperature;    // 溫度
    private int capacity;       // 容量
    public Water() {
        this.temperature = 0;
        this.capacity = 10;
    }
    // 省略...
}    
複製程式碼

泡茶需要茶葉 TeaLeaf

public class TeaLeaf {
    private String teaName;
    // 省略...
}    
複製程式碼

燒水需要用水壺燒,將水加熱

public class KettleService {
    public void waterBurning(String who, Water water, int burnTime) {
        // 燒水,計算最終溫度
        int finalTermperature = Math.min(100, water.getTemperature() + burnTime * 20);
        water.setTemperature(finalTermperature);
        System.out.println(who + " 使用水壺燒水,最終水溫為 " + finalTermperature);
    }
}
複製程式碼

泡茶,將燒好的水與茶葉進行沖泡,最終得到一杯茶水

public class TeasetService {
    public Teawater makeTeaWater(String who, Water water, TeaLeaf teaLeaf) {
        String teawater = "一杯容量為 " + water.getCapacity() + ", 溫度為 " + water.getTemperature() + " 的" + teaLeaf.getTeaName() + "茶水";
        System.out.println(who + " 泡了" + teawater);
        return new Teawater(teawater);
    }
}
複製程式碼

人喝茶水

public class Man {
    private String name;
    public Man(String name) {
        this.name = name;
    }
    public void drink(Teawater teawater) {
        System.out.println(name + " 喝了" + teawater.getTeaWater());
    }
}
複製程式碼

自己泡茶喝

張三、李四各自泡茶喝,各自都需要準備茶具、茶葉、水,各自還要完成燒水、泡茶等操作

public class Main {
    public static void main(String[] args) {
        Man zhangsan = new Man("張三");
        KettleService kettleService1 = new KettleService();
        TeasetService teasetService1 = new TeasetService();
        Water water1 = new Water();
        TeaLeaf teaLeaf1 = new TeaLeaf("西湖龍井");
        kettleService1.waterBurning(zhangsan.getName(), water1, 4);
        Teawater teawater1 = teasetService1.makeTeaWater(zhangsan.getName(), water1, teaLeaf1);
        zhangsan.drink(teawater1);
        System.out.println();

        Man lisi = new Man("李四");
        KettleService kettleService2 = new KettleService();
        TeasetService teasetService2 = new TeasetService();
        Water water2 = new Water(10, 15);
        TeaLeaf teaLeaf2 = new TeaLeaf("碧螺春");
        kettleService2.waterBurning(lisi.getName(), water2, 4);
        Teawater teawater2 = teasetService2.makeTeaWater(lisi.getName(), water2, teaLeaf2);
        lisi.drink(teawater2);
    }
}
複製程式碼

輸出為

張三 使用水壺燒水,最終水溫為 80
張三 泡了一杯容量為 10, 溫度為 80 的西湖龍井茶水
張三 喝了一杯容量為 10, 溫度為 80 的西湖龍井茶水

李四 使用水壺燒水,最終水溫為 90
李四 泡了一杯容量為 15, 溫度為 90 的碧螺春茶水
李四 喝了一杯容量為 15, 溫度為 90 的碧螺春茶水
複製程式碼

自己泡茶喝模式圖

自己泡茶喝模式圖

到茶館喝茶

茶館,茶館有不同的套餐

public class TeaHouseFacade {
    private String name;
    private TeasetService teasetService;
    private KettleService kettleService;

    public TeaHouseFacade(String name) {
        this.name = name;
        this.teasetService = new TeasetService();
        this.kettleService = new KettleService();
    }

    public Teawater makeTea(int teaNumber) {
        switch (teaNumber) {
            case 1:
                Water water1 = new Water();
                TeaLeaf teaLeaf1 = new TeaLeaf("西湖龍井");
                kettleService.waterBurning(this.name, water1, 4);
                Teawater teawater1 = teasetService.makeTeaWater(this.name, water1, teaLeaf1);
                return teawater1;
            case 2:
                Water water2 = new Water(10, 15);
                TeaLeaf teaLeaf2 = new TeaLeaf("碧螺春");
                kettleService.waterBurning(this.name, water2, 4);
                Teawater teawater2 = teasetService.makeTeaWater(this.name, water2, teaLeaf2);
                return teawater2;
            default:
                Water water3 = new Water();
                TeaLeaf teaLeaf3 = new TeaLeaf("招牌烏龍");
                kettleService.waterBurning(this.name, water3, 5);
                Teawater teawater3 = teasetService.makeTeaWater(this.name, water3, teaLeaf3);
                return teawater3;
        }
    }
}
複製程式碼

張三和李四點茶,只需要告訴茶館套餐編號即可,水、茶葉由茶館準備,燒水泡茶的操作由茶館統一完成

public class Test {
    public static void main(String[] args) {
        TeaHouseFacade teaHouseFacade = new TeaHouseFacade("老舍茶館");

        Man zhangsan = new Man("張三");
        Teawater teawater = teaHouseFacade.makeTea(1);
        zhangsan.drink(teawater);
        System.out.println();

        Man lisi = new Man("李四");
        Teawater teawater1 = teaHouseFacade.makeTea(2);
        lisi.drink(teawater1);
    }
}
複製程式碼

輸出為

老舍茶館 使用水壺燒水,最終水溫為 80
老舍茶館 泡了一杯容量為 10, 溫度為 80 的西湖龍井茶水
張三 喝了一杯容量為 10, 溫度為 80 的西湖龍井茶水

老舍茶館 使用水壺燒水,最終水溫為 90
老舍茶館 泡了一杯容量為 15, 溫度為 90 的碧螺春茶水
李四 喝了一杯容量為 15, 溫度為 90 的碧螺春茶水
複製程式碼

到茶館喝茶模式圖

到茶館喝茶模式圖

外觀模式總結

外觀模式的主要優點如下:

  • 它對客戶端遮蔽了子系統元件,減少了客戶端所需處理的物件數目,並使得子系統使用起來更加容易。通過引入外觀模式,客戶端程式碼將變得很簡單,與之關聯的物件也很少。
  • 它實現了子系統與客戶端之間的鬆耦合關係,這使得子系統的變化不會影響到呼叫它的客戶端,只需要調整外觀類即可。
  • 一個子系統的修改對其他子系統沒有任何影響,而且子系統內部變化也不會影響到外觀物件。

外觀模式的主要缺點如下:

  • 不能很好地限制客戶端直接使用子系統類,如果對客戶端訪問子系統類做太多的限制則減少了可變性和靈活性。
  • 如果設計不當,增加新的子系統可能需要修改外觀類的原始碼,違背了開閉原則。

適用場景:

  • 當要為訪問一系列複雜的子系統提供一個簡單入口時可以使用外觀模式。
  • 客戶端程式與多個子系統之間存在很大的依賴性。引入外觀類可以將子系統與客戶端解耦,從而提高子系統的獨立性和可移植性。
  • 在層次化結構中,可以使用外觀模式定義系統中每一層的入口,層與層之間不直接產生聯絡,而通過外觀類建立聯絡,降低層之間的耦合度。

原始碼分析外觀模式的典型應用

spring jdbc中的外觀模式

檢視 org.springframework.jdbc.support.JdbcUtils

public abstract class JdbcUtils {
    public static void closeConnection(Connection con) {
		if (con != null) {
			try {
				con.close();
			}
			catch (SQLException ex) {
				logger.debug("Could not close JDBC Connection", ex);
			}
			catch (Throwable ex) {
				// We don't trust the JDBC driver: It might throw RuntimeException or Error.
				logger.debug("Unexpected exception on closing JDBC Connection", ex);
			}
		}
	}
	
	public static Object getResultSetValue(ResultSet rs, int index, Class<?> requiredType) throws SQLException {
		if (requiredType == null) {
			return getResultSetValue(rs, index);
		}

		Object value = null;
		boolean wasNullCheck = false;

		// Explicitly extract typed value, as far as possible.
		if (String.class.equals(requiredType)) {
			value = rs.getString(index);
		}
		else if (boolean.class.equals(requiredType) || Boolean.class.equals(requiredType)) {
			value = rs.getBoolean(index);
			wasNullCheck = true;
		}
		else if (byte.class.equals(requiredType) || Byte.class.equals(requiredType)) {
			value = rs.getByte(index);
			wasNullCheck = true;
		}
		else if (short.class.equals(requiredType) || Short.class.equals(requiredType)) {
			value = rs.getShort(index);
			wasNullCheck = true;
		}
		else if (int.class.equals(requiredType) || Integer.class.equals(requiredType)) {
			value = rs.getInt(index);
			wasNullCheck = true;
		}
		else if (long.class.equals(requiredType) || Long.class.equals(requiredType)) {
			value = rs.getLong(index);
			wasNullCheck = true;
		}
		else if (float.class.equals(requiredType) || Float.class.equals(requiredType)) {
			value = rs.getFloat(index);
			wasNullCheck = true;
		}
		else if (double.class.equals(requiredType) || Double.class.equals(requiredType) ||
				Number.class.equals(requiredType)) {
			value = rs.getDouble(index);
			wasNullCheck = true;
		}
		else if (byte[].class.equals(requiredType)) {
			value = rs.getBytes(index);
		}
		else if (java.sql.Date.class.equals(requiredType)) {
			value = rs.getDate(index);
		}
		else if (java.sql.Time.class.equals(requiredType)) {
			value = rs.getTime(index);
		}
		else if (java.sql.Timestamp.class.equals(requiredType) || java.util.Date.class.equals(requiredType)) {
			value = rs.getTimestamp(index);
		}
		else if (BigDecimal.class.equals(requiredType)) {
			value = rs.getBigDecimal(index);
		}
		else if (Blob.class.equals(requiredType)) {
			value = rs.getBlob(index);
		}
		else if (Clob.class.equals(requiredType)) {
			value = rs.getClob(index);
		}
		else {
			// Some unknown type desired -> rely on getObject.
			value = getResultSetValue(rs, index);
		}
		
		if (wasNullCheck && value != null && rs.wasNull()) {
			value = null;
		}
		return value;
	}
    // ...省略...
}    
複製程式碼

該工具類主要是對原生的 jdbc 進行了封裝

Mybatis中的外觀模式

檢視 org.apache.ibatis.session.Configuration 類中以 new 開頭的方法

public class Configuration {
	public Executor newExecutor(Transaction transaction, ExecutorType executorType) {
	    executorType = executorType == null ? defaultExecutorType : executorType;
	    executorType = executorType == null ? ExecutorType.SIMPLE : executorType;
	    Executor executor;
	    if (ExecutorType.BATCH == executorType) {
	      executor = new BatchExecutor(this, transaction);
	    } else if (ExecutorType.REUSE == executorType) {
	      executor = new ReuseExecutor(this, transaction);
	    } else {
	      executor = new SimpleExecutor(this, transaction);
	    }
	    if (cacheEnabled) {
	      executor = new CachingExecutor(executor);
	    }
	    executor = (Executor) interceptorChain.pluginAll(executor);
	    return executor;
	}
	
	public ResultSetHandler newResultSetHandler(Executor executor, MappedStatement mappedStatement, RowBounds rowBounds, ParameterHandler parameterHandler,
	      ResultHandler resultHandler, BoundSql boundSql) {
	    ResultSetHandler resultSetHandler = new DefaultResultSetHandler(executor, mappedStatement, parameterHandler, resultHandler, boundSql, rowBounds);
	    resultSetHandler = (ResultSetHandler) interceptorChain.pluginAll(resultSetHandler);
	    return resultSetHandler;
	}
	// ...省略...
}
複製程式碼

該類主要對一些建立物件的操作進行封裝

Tomcat 中的外觀模式

Tomcat 原始碼中大量使用了很多外觀模式

Tomcat中的外觀模式

org.apache.catalina.connector.Requestorg.apache.catalina.connector.RequestFacade 這兩個類都實現了 HttpServletRequest 介面

Request 中呼叫 getRequest() 實際獲取的是 RequestFacade 的物件

protected RequestFacade facade = null;

public HttpServletRequest getRequest() {
    if (facade == null) {
        facade = new RequestFacade(this);
    }
    return facade;
}
複製程式碼

RequestFacade 中再對認為是子系統的操作進行封裝

public class RequestFacade implements HttpServletRequest {
    /**
     * The wrapped request.
     */
    protected Request request = null;
    
    @Override
    public Object getAttribute(String name) {
        if (request == null) {
            throw new IllegalStateException(sm.getString("requestFacade.nullRequest"));
        }
        return request.getAttribute(name);
    }
    // ...省略...
}    
複製程式碼

SLF4J 中的外觀模式

SLF4J 是簡單的日誌外觀模式框架,抽象了各種日誌框架例如 LogbackLog4jCommons-loggingJDK 自帶的 logging 實現介面。它使得使用者可以在部署時使用自己想要的日誌框架。

SLF4J 沒有替代任何日誌框架,它僅僅是標準日誌框架的外觀模式。如果在類路徑下除了 SLF4J 再沒有任何日誌框架,那麼預設狀態是在控制檯輸出日誌。

日誌處理框架 Logback 是 Log4j 的改進版本,原生支援SLF4J(因為是同一作者開發的),因此 Logback+SLF4J 的組合是日誌框架的最佳選擇,比 SLF4J+其它日誌框架 的組合要快一些。而且Logback的配置可以是XML或Groovy程式碼。

SLF4J 的 helloworld 如下:

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class HelloWorld {
  public static void main(String[] args) {
    Logger logger = LoggerFactory.getLogger(HelloWorld.class);
    logger.info("Hello World");
  }
}
複製程式碼

下圖為 SLF4J 與日誌處理框架的繫結呼叫關係

SLF4J與日誌處理框架的繫結呼叫關係

應用層呼叫 slf4j-api.jarslf4j-api.jar 再根據所繫結的日誌處理框架呼叫不同的 jar 包進行處理

參考:
劉偉:設計模式Java版
慕課網java設計模式精講 Debug 方式+記憶體分析
Java日誌框架:slf4j作用及其實現原理

推薦閱讀

設計模式 | 簡單工廠模式及典型應用
設計模式 | 工廠方法模式及典型應用
設計模式 | 抽象工廠模式及典型應用
設計模式 | 建造者模式及典型應用
設計模式 | 原型模式及典型應用

點選[閱讀原文]可訪問我的個人部落格:laijianfeng.org

關注【小旋鋒】微信公眾號

相關文章