最近依然在應對面試,有人問到設計模式的題目,之前自己看過,但是都忘記了,現在整理一下。
1.單例模式
有時,允許自由創建某個類的實例沒有意義,反而可能會導致系統性能下降。例如:數據庫引擎訪問點、Hibernate的SessionFactory都只需要一個實例即可,此時可以使用單例模式。
如果一個類始終只能創建一個實例,則稱這個類爲單例類,這種模式爲單例模式。
Spring中框架中可以直接在配置時通過制定scope=”singleton”實現單例模式。
Java代碼可以自己實現單例模式,如下:
class Singleton{
//類變量緩存曾經創建的實例
private static Singleton instance;
//構造方法私有
private Singleton(){}
//靜態方法,返回類的實例
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
思路:把構造方法設爲私有,這樣在類外就無法調用構造器生成對象的實例。然後在類中聲明一個類變量用來緩存生成的實例。這樣的話,還有個問題是無法生成這個對象的實例,因此需要寫一個方法生成那個單例的對象實例。做法:聲明一個靜態方法,判斷類變量對否爲null,如果是null說明沒有生成實例,就需要調用私有的構造方法生產一個實例;如果有了的話,就不需要如何操作。最後返回這個實例。
2.簡單工廠模式
開發過程中經常遇到A實例需要調用B實例。這個時候大多數做法是直接new一個B出來。這樣的一個缺點就是代碼耦合了,因爲A中調用了B類的類名(硬編碼耦合)。後果就是如果我們後來重構了,需要用C來替換B,這樣的後果就是每個B都需要修改爲C。工作量很大。
解決方法:因爲我們只是調用了B中的方法,不需要關心B的創建、實現過程,因此可以使用一個接口:IB,讓B實現接口IB。這樣A依賴的就不是具體的類了,而是具體的接口。然後創建一個工程類:IBFactory,該工廠負責產生IB的實例。A只需要調用IBFactory裏面的方法就可以拿到IB的實例了。
將來如果出現重構:C代替B的情況,只需要讓C也實現IB接口,然後修改IBFactory裏面代碼,讓工廠產生C實例就可以了。
代碼:
IB接口。
public interface IB {
public void show();
}
B類
public class B implements IB {
@Override
public void show() {
System.out.println("this is B!!");
}
}
C類
public class C implements IB {
@Override
public void show() {
System.out.println("this is C!!");
}
}
IBFactory。工廠類,返回實例。
public class IBFactory {
public IB getIB() {
//如果是return new B();則返回B的實例
//重構只需要修改這裏。
return new C();
}
}
A類。
public class A {
private IB ib;
public A(IB ib){
this.ib = ib;
}
public void show(){
ib.show();
}
public static void main(String[] args) {
IBFactory ibFactory = new IBFactory();
A a = new A(ibFactory.getIB());
a.show();
}
}
3.工廠方法和抽象工廠
(1)工廠方法:
在上面2中,系統通過工廠方法生產了對象的實例,在工廠類中決定生產那個對象的實例。但是這樣也不太完美。修改生產的產品是,需要去工廠裏面直接修改代碼,這樣不太好。希望在調用工廠的時候(即在A中),直接可以判斷需要生產什麼產品。換言之,重構時,只需要修改A中代碼就可以了。
解決辦法,可以設計一個工廠的接口,程序爲不同的產品設計不同的工廠。
代碼:
Factory類。生產不同工廠的工廠類。
public interface Factory {
IB getIB();
}
BFactory.java。生產B產品的工廠。
public class BFactory implements Factory {
@Override
public IB getIB() {
return new B();
}
}
CFactory.java。生產C產品的工廠。
public class CFactory implements Factory {
@Override
public IB getIB() {
return new C();
}
}
IB.java,B.java,C.java與前面簡單工廠類一樣,不在贅述。
A.java代碼。
public class A {
private IB ib;
public A(IB ib){
this.ib = ib;
}
public void show(){
ib.show();
}
public static void main(String[] args) {
//使用CFactory子類創建Factory
Factory factory = new CFactory();
A a = new A(factory.getIB());
a.show();
}
}
(2)抽象工廠
對於上面的工廠方法,依然存在一種耦合:客戶端代碼與不同的工廠類耦合。解決這個辦法可以再增加一個工廠類,這個類來製造不同的工廠:
代碼:
添加類FactoryFactory.java
public class FactoryFactory {
public static Factory getFactory(String type){
if(type.equalsIgnoreCase("b"))
return new BFactory();
else
return new CFactory();
}
}
A.java修改代碼:
public class A {
private IB ib;
public A(IB ib){
this.ib = ib;
}
public void show(){
ib.show();
}
public static void main(String[] args) {
//通過傳參來確定生產的工廠
Factory factory = FactoryFactory.getFactory("B");
A a = new A(factory.getIB());
a.show();
}
}
“抽象工廠”與“簡單工廠”區別:簡單工廠直接生產對被調用對象;抽象工廠生產工廠對象。
Spring框架的IoC容器可以認爲是抽象工廠。可以管理Bean實例,也可以管理工廠實例。
4.代理模式
當客戶端代碼需要調用某個對象的時候,客戶端實際上不關心是否準確得到了這個對象,只要一個能提供該功能的對象即可,此時可以返回對象的代理。
這種設計模式下,系統會爲某個對象生成一個代理,由這個代理控制源對象的引用。這種情況下,客戶端代碼僅僅持有一個被代理對象的接口,變爲面向接口編程。
其實這裏可以看看動態代理、靜態代理的知識。
代碼:
BigImage.java。模擬大的圖片。
public class BigImage implements Image {
public BigImage(){
try {
//暫停3秒,模擬系統開銷
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
@Override
public void show() {
System.out.println("image show...");
}
}
ProxyImage.java,圖片的代理類。
public class ProxyImage implements Image {
private Image image;
public ProxyImage(Image image){
this.image = image;
}
@Override
public void show() {
if(image == null){
image = new BigImage();
}
image.show();
}
}
測試類。
public class ProxyTest {
public static void main(String[] args) {
long startTime = System.currentTimeMillis();
Image image = new ProxyImage(null);
System.out.println("加載圖片耗時:"+(System.currentTimeMillis()-startTime));
//調用show方法時,纔會真正執行加載
long startTime2 = System.currentTimeMillis();
image.show();
System.out.println("加載圖片真正耗時:"+(System.currentTimeMillis()-startTime2));
}
}
結果:
Hibernate框架使用了代理模式,比如它的延遲加載。加入A、B實體存在關聯關係,在加載A實體時,B應該被加載。採取延遲加載,系統會生產一個B的代理,只有A真正需要訪問B中變量的時候,纔會真正加載B實體。從而節約開銷。
5.命令模式
很多時候,我們需要一個方法完成一個功能,而且,這個功能大多數步驟我們已知並確定,但是有少量具體的步驟,需要在執行這個方法的時候,才能確定。這個時候可以使用命令模式。比如:我們有一個方法遍歷數組的每一個元素,但是對元素的具體操作(迭代輸出、累加)需要在調用該方法的時候才能確定,則需要在調用該方法時指定具體的處理行爲。
代碼:
ProcessArray.java,裏面有一個each方法,用於處理數組。具體操作不知道,所以需要傳入一個Command參數。
public class ProcessArray {
public void each(int[] target, Command cmd) {
cmd.process(target);
}
}
Command.java,裏面的process方法定義了對於數組的處理行爲。這是一個接口
public interface Command {
void process(int[] target);
}
CommandTest.java。測試命令方法。
public class CommandTest {
public static void main(String[] args) {
ProcessArray processArray = new ProcessArray();
int[] target = {5, 2, 8, 4};
processArray.each(target, new Command() {
@Override
public void process(int[] target) {
for (int temp : target) {
System.out.println("迭代輸出:" + temp);
}
}
});
}
}
如果需要對數組進行其他的操作,只需要修改傳入的匿名類的實例。不同的Command實例封裝了不同的操作。
6.策略模式
如果現在有這樣一個需求,有一件商品,不同的用戶(普通會員、VIP)所享受到的折扣不一樣。需要針對不同的打折需求計算不同的價格。以往的做法就是switch()語句解決。但是一個弊端就是,如果某天我們增加了一個新的打折情況,則需要在switch中添加新的case,並添加新的語句,然後針對新的打折。現在使用策略模式,我們只需要新建一個類,實現統一的接口,描述這種打折的具體操作就可以了,不需要修改之前的代碼。
代碼:
首先我們寫打折接口。DiscountStrategy.java。
public interface DiscountStrategy {
double getDiscount(double originPrice);
}
其次兩個打折具體類,實現這個接口。
普通用戶打折。CommonDiscount .java。
public class CommonDiscount implements DiscountStrategy {
@Override
public double getDiscount(double originPrice) {
System.out.println("普通用戶打折...");
return originPrice * 0.7;
}
}
VIP用戶的打折。VIPDiscount .java。
public class VIPDiscount implements DiscountStrategy {
@Override
public double getDiscount(double originPrice) {
System.out.println("VIP 打五折...");
return originPrice * 0.5;
}
}
選擇策略算法、返回價格的類。DiscountContext.java。
public class DiscountContext {
private DiscountStrategy strategy;
public DiscountContext(DiscountStrategy strategy){
this.strategy = strategy;
}
public double getDiscount(double price){
return strategy.getDiscount(price);
}
//改變策略
public void changeStrategy(DiscountStrategy strategy){
this.strategy = strategy;
}
}
測試
public class Test {
public static void main(String[] args) {
//null代表普通用戶
DiscountContext discountContext = new DiscountContext(new CommonDiscount());
double price = 998.99;
System.out.println("普通用戶價格:" + discountContext.getDiscount(price));
discountContext.changeStrategy(new VIPDiscount());
System.out.println("VIP用戶價格:" + discountContext.getDiscount(price));
}
}
之後的代碼修改中,如果新增加一種打折方式,只需要寫一個實現了打折接口的類,然後在測試類中添加
discountContext.changeStrategy(new /*打折類*/);
就可以了。
Hibernate中數據庫方言Dialect就是使用的這種模式。
7.門面模式
很多情況下,我們進行一個操作的步驟是固定的。比如餐館點餐吃飯,首先會有點餐,其次廚師做飯,最後服務員上菜…這些步驟都是一定的了。但是每次這樣一個過程都需要調用三個類(訂餐、做飯、上菜),比較麻煩,我們可以寫一個類(門面),類中一次調用這三個類。在以後顧客點餐的時候,直接調用這個門面。
代碼:
PayMent.java。訂餐。
public class Payment {
public String pay(String food) {
System.out.println("點餐:" + food + "一份!");
return food;
}
}
Cook.java。廚師類。
public class Cook {
public String cook(String food) {
System.out.println("廚師正在烹調:" + food);
return food;
}
}
Serve.java。服務類。
public class Serve {
public void serve(String food) {
System.out.println("服務員上菜:" + food);
}
}
Facade.java。門面類。
public class Facade {
Payment payment;
Cook cook;
Serve serve;
public Facade() {
this.payment = new Payment();
this.cook = new Cook();
this.serve = new Serve();
}
public void serveFood(String food){
payment.pay(food);
cook.cook(food);
serve.serve(food);
}
}
測試
public class Test {
public static void main(String[] args) {
Facade facade = new Facade();
facade.serveFood("牛肉麪");
}
}
我們在門面裏面封裝了要執行的步驟,在程序中可以直接調用門面完成一系列操作。
Hibernate框架中,hibernateTemplate就是用了這種模式。例如save操作,封裝了SessionFactory、session等門面。
8.橋接模式
開發中,遇到兩個維度的結構模式,僅僅使用繼承無法實現。比如麪條,可能有材料維度(牛肉、羊肉、豬肉),也有口味維度(清淡、微辣)等等。這個時候可以這樣解決。
設置一個接口,專門記錄口味的。不同的口味,對應一個實現類。
設置一個抽象類,該類有一個變量記錄麪條的口味。然後對於具體的材料(豬肉、牛肉等)繼承這個抽象類。
這樣對於添加口味維度或者材料維度,都只需要添加一個類就可以。
代碼:
口味維度的兩個類。
public class PepperyStyle implements Peppery {
@Override
public String style() {
return "辣口味";
}
}
public class PlainStyle implements Peppery {
@Override
public String style() {
return "清淡口味";
}
}
麪條抽象類。
public abstract class AbstractNoodle {
Peppery style;
public AbstractNoodle(Peppery style){
this.style = style;
}
public abstract void eat();
}
牛肉麪以及豬肉面類。
public class BeefNoodle extends AbstractNoodle {
public BeefNoodle(Peppery style) {
super(style);
}
@Override
public void eat() {
System.out.println("牛肉麪,口味:" + super.style.style());
}
}
public class ProkyNoodle extends AbstractNoodle {
public ProkyNoodle(Peppery style) {
super(style);
}
@Override
public void eat() {
System.out.println("豬肉面,口味:" + super.style.style());
}
}
測試類。
public class Test {
public static void main(String[] args) {
AbstractNoodle noodle1 = new BeefNoodle(new PepperyStyle());
noodle1.eat();
AbstractNoodle noodle2 = new ProkyNoodle(new PlainStyle());
noodle2.eat();
}
}
9.觀察者模式
比較常見的一種模式。
定義了對象間一對多的依賴關係:一個或者多個觀察者對象觀察一個主題對象。當主題對象發生變化時,系統通知所有的觀察此對象的觀察者。
一般包括四個角色:
被觀察者的基類對象:持有多個觀察者的引用。
觀察者接口:所有觀察者必須實現的一個接口。
被觀察者實現類:繼承被觀察者的基類對象。
觀察者實現類:實現觀察者接口。