首先,澄清兩個概念。用戶程序——服務使用者,服務程序——服務提供者。
用戶程序怎麼使用服務程序提供的服務呢?最直接的方法如下:在JAVA中的用戶類中直接創建一個服務類,然後調用服務類提供的方法;在C中用戶模塊直接調用服務模塊的函數。
舉個例子,Client需要使用Impl提供的print功能來輸出一個字符串。
[JAVA CODE]
xxx/Impl.java:
package xxx;
public class Impl {
public void print(Strings) {
System.out.println(“NowIn Impl. The input s==“ + s);
}
}
yyy/Client.java:
package yyy;
import xxx.Impl;
public class Client {
public static voidmain(String[] args) {
Impl impl = new Impl();
impl.print(“簡單的測試程序!”);
}
}
[C CODE]
xxx/inc/impl.h
#ifndef_IMPL_H_
#define_IMPL_H_
#include<stdio.h>
extern voidprint(char *str);
#endif /* _IMPL_H_ */
xxx/src/impl.c
#include “impl.h”
void print(char*str)
{
printf(“Now In impl. The input str==%s”, str);
return;
}
yyy/src/client.c
#include “impl.h”
int main(argc,char **argv)
{
print(“簡單的測試程序!”);
return 0;
}
上述代碼有什麼問題麼?對於簡單的應用,或者很穩定的應用來說,是沒有什麼問題的。但是,請考慮,假如有個用戶程序用了一段時間,發現當前服務程序提供的服務不好用,他想換一種,怎麼辦?
方案一:直接修改當前服務實現的代碼,對於只有一個用戶程序的情況是沒有問題,但是假如還有其他用戶程序在使用,且不想改變現有實現呢?
方案二:重新添加一個服務來滿足需求,該用戶程序使用新的實現。此時,用戶程序就需要修改。
以上兩個方案都不符合”開閉原則”。那麼,還有沒有其他方案呢?能否在擴展當前功能的情況下不修改原有代碼呢?
造成以上問題的原因就在於用戶程序和服務程序之間產生了嚴重耦合,用戶程序知道了服務程序的實現(方法\函數)。
那麼,怎麼能夠解除耦合呢?
面向接口編程!而不是面向實現編程。用戶程序完全不需要知道服務程序是怎麼實現的,只需要和服務程序之間建立一份“契約”,明確服務程序需要提供的功能即可。此後,不管服務程序使用方法/函數名爲print還是print_str,用戶程序都不需要關心。
[JAVA CODE]
xxx/Api.java:
package xxx;
/**
* 某個接口(通用的、抽象的、非具體的功能)
*/
public interface Api {
/**
*
* 某個具體的功能方法的定義
* 功能是把傳入的字符串打印出來
* @ param s 任意想要打印輸出的字符串
*/
public void print(Strings);
}
xxx/Impl.java:
package xxx;
/**
* 對接口的實現
*/
public class Impl implements Api {
public void print(Strings) {
System.out.println(“NowIn Impl. The input s==“ + s);
}
}
yyy/Client.java:
package yyy;
import xxx.Api;
import xxx.Impl;
/**
* 客戶端: 測試使用API接口
*/
public class Client {
public static voidmain(String[] args) {
Api api = new Impl();
api.print(“簡單的 接口-實現 測試程序!”);
}
}
[C CODE]
xxx/inc/api.h
#ifndef _API_H_
#define _API_H_
typedef void(*PRINT_FUNC)(char *str);
typedef struct tagApi_S
{
PRINT_FUNC printFunc;
}Api_S,*API_S_Handle;
#endif /* _API_H_ */
xxx/inc/impl.h
#ifndef _IMPL_H_
#define_IMPL_H_
#include"api.h”
#include<stdio.h>
externAPI_S_Handle impl_get_opt(void)
#endif /* _IMPL_H_ */
xxx/src/impl.c
#include"impl.h”
void print(char*str)
{
printf("Now In impl. The inputstr==%s", str);
return;
}
Api_S g_api =
{
.printFunc = print;
};
API_S_Handleimpl_get_opt(void)
{
return &(g_api);
}
yyy/src/client.c
#include"api.h"
#include"impl.h"
int main(argc,char **argv)
{
API_S_Handle handle = impl_get_opt();
/* 省略錯誤處理 */
handle->printFunc("簡單的 接口-實現 測試程序!”);
return 0;
}
好了,這次是面向接口編程了。但是,問題解決了麼?沒有!用戶程序還是要從一定程度上知道服務程序的實現,即JAVA代碼中的Api api = newImpl();和C代碼中的API_S_Handle handle =impl_get_opt();還是和具體實現產生了耦合。當要使用AnotherImpl來替換Impl時,還是要修改現有代碼。
那麼,能不產生耦合麼?怎麼讓用戶程序在只知道接口而不知道實現的情況下工作?這就是簡單工廠要解決的問題。
先看代碼。
[JAVA CODE]
xxx/Api.java:
package xxx;
/**
* 某個接口(通用的、抽象的、非具體的功能)
*/
public interface Api {
/**
*
* 某個具體的功能方法的定義
* 功能是把傳入的字符串打印出來
* @ param s 任意想要打印輸出的字符串
*/
public void print(Strings);
}
xxx/Impl.java:
package xxx;
/**
* 對接口的實現
*/
public class Impl implements Api {
public void print(Strings) {
System.out.println(“NowIn Impl. The input s==“ + s);
}
}
xxx/ Factory.properties:
ImplClass = xxx.Impl
xxx/ Factory.java:
package xxx;
import java.util.Properties;
import java.io.InputStream;
import java.io.IOException;
import java.lang.Class;
import java.lang.InstantiationException;
import java.lang.IllegalAccessException;
import java.lang.ClassNotFoundException;
/**
* 工廠類,用來創建Api對象
*/
public class Factory{
/**
* 具體創建Api對象的方法,根據配置文件的參數來創建接口
* @return 創建好的Api對象
*/
public static ApicreateApi() {
// 直接讀取配置文件來獲取要創建實例的類
Properties p =new Properties();
InputStream in =null;
try {
in =Factory.class.getResourceAsStream("Factory.properties");
p.load(in);
} catch (IOExceptione) {
System.out.println("裝載工廠配置文件出錯了,具體的堆棧信息如下:");
e.printStackTrace();
} finally {
try {
in.close();
}
catch (IOException e) {
e.printStackTrace();
}
}
// 用反射去創建,暫且忽略異常處理
Api api = null;
try {
api =(Api)Class.forName(p.getProperty("ImplClass")).newInstance();
}
catch(InstantiationException e) {
e.printStackTrace();
}
catch (IllegalAccessExceptione) {
e.printStackTrace();
}
catch(ClassNotFoundException e) {
e.printStackTrace();
}
return api;
}
}
yyy/Client.java:
package yyy;
import xxx.Api;
import xxx.Factory;
/**
* 客戶端: 測試使用API接口
*/
public classClient {
public static void main(String[] args) {
Api api = Factory.createApi();
api.print("簡單的 接口-實現 測試程序!");
}
}
[C CODE]
xxx/inc/api.h
#ifndef _API_H_
#define _API_H_
typedef void(*PRINT_FUNC)(char *str);
typedef struct tagApi_S
{
PRINT_FUNC printFunc;
}Api_S,*API_S_Handle;
#endif /* _API_H_ */
xxx/inc/impl.h
#ifndef_IMPL_H_
#define_IMPL_H_
#include"api.h”
#include<stdio.h>
externAPI_S_Handle impl_get_opt(void)
#endif /* _IMPL_H_ */
xxx/src/impl.c
#include"impl.h”
void print(char*str)
{
printf("Now In impl. The inputstr==%s", str);
return;
}
Api_S g_api =
{
.printFunc = print;
};
API_S_Handleimpl_get_opt(void)
{
return &(g_api);
}
xxx/src/factory.h
externAPI_S_Handle create_api(void);
xxx/src/factory.c
#include"api.h"
#include"factory.h"
#include"impl.h"
API_S_Handlecreate_api(void)
{
/* 暫且忽略選擇實現的機制 */
API_S_Handle handle;
/* 假設選擇了Impl實現 */
handle = impl_get_opt();
return handle;
}
yyy/src/client.c
#include"api.h"
#include"factory.h"
int main(argc,char **argv)
{
API_S_Handle handle = create_api();
/* 省略錯誤處理 */
handle->printFunc("簡單的 接口-實現 測試程序!”);
return 0;
}
現在,用戶程序使用簡單工廠來實例化接口,不用關注具體的實現了。在簡單工廠中可以選擇具體的實現,在這裏不深入討論選擇實現的機制,有一種更好的選擇方式就是使用容器,將選擇延遲到容器中,感興趣的同學們可以查閱相關資料(關鍵詞包括“依賴倒置”、“控制反轉”、“Spring容器”等)。
使用簡單工廠,有什麼好處呢?下面以JAVA代碼爲例,演示一下在另一個用戶程序中要將服務程序修改爲由AnotherImpl來提供實現的步驟。
[JAVA CODE]
首先,新增AnotherImpl。
xxx/ AnotherImpl.java:
package xxx;
/**
* 對接口的實現
*/
public class AnotherImpl implements Api {
public void print(Strings) {
System.out.println("NowIn AnotherImpl. The input s==" + s);
}
}
其次,修改配置文件。
xxx/ Factory.properties:
# ImplClass = xxx.Impl
ImplClass = xxx.AnotherImpl
再次,沒有啦!
最後,測試,OK,滿足需求!
通過以上示例,相信各位同學已經理解了簡單工廠的主要作用:封裝隔離和選擇實現。封裝隔離的目的是讓用戶程序不必關注服務程序的實現,這也是面向接口編程的意義所在;選擇實現的目的是爲了滿足不同用戶程序的不同需求,體現向擴展開放、向修改封閉的原則。