設計模式-裝飾模式(Decorator)-Java

設計模式-裝飾模式-Java


目錄




內容

  儘管目前房價依舊很高,但還是阻止不了大家對新房的渴望和買房的熱情。如果大家買的是毛坯房,無疑還有一項艱鉅的任務要面對,那就是裝修。對新房進行裝修並沒有改變房屋用於居住的本質,但它可以讓房子變得更漂亮、更溫馨、更實用、更能滿足居家的需求。在軟件設計中,我們也有一種類似新房裝修的技術可以對已有對象(新房)的功能進行擴展(裝修),以獲得更加符合用戶需求的對象,使得對象具有更加強大的功能。這種技術對應於一種被稱之爲裝飾模式的設計模式,本章將介紹用於擴展系統功能的裝飾模式。

1、示例案例-圖形界面構件庫的設計

  Sunny軟件公司基於面向對象技術開發了一套圖形界面構件庫VisualComponent,該構件庫提供了大量基本構件,如窗體、文本框、列表框等,由於在使用該構件庫時,用戶經常要求定製一些特效顯示效果,如帶滾動條的窗體、帶黑色邊框的文本框、既帶滾動條又帶黑色邊框的列表框等等,因此經常需要對該構件庫進行擴展以增強其功能,

  如何提高圖形界面構件庫性的可擴展性並降低其維護成本是Sunny公司開發人員必須面對的一個問題。

  Sunny軟件公司的開發人員針對上述要求,提出了一個基於繼承複用的初始設計方案,其基本結構如圖1-1所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-1FN7brUZ-1591711663928)(./images/gui.png)]
圖1-1 圖形界面構件庫初始設計方案

 &emsp圖1-1中,在抽象類Component中聲明瞭抽象方法display(),其子類Window、TextBox等實現了display()方法,可以顯示最簡單的控件,再通過它們的子類來對功能進行擴展,例如,在Window的子類ScrollBarWindow、BlackBorderWindow中對Window中的display()方法進行擴展,分別實現帶滾動條和帶黑色邊框的窗體。仔細分析該設計方案,我們不難發現存在如下幾個問題:

  • (1) 系統擴展麻煩,在某些編程語言中無法實現。如果用戶需要一個既帶滾動條又帶黑色邊框的窗體,在圖12-2中通過增加了一個新的類ScrollBarAndBlackBorderWindow來實現,該類既作爲ScrollBarWindow的子類,又作爲BlackBorderWindow的子類;但現在很多面向對象編程語言,如Java、C#等都不支持多重類繼承,因此在這些語言中無法通過繼承來實現對來自多個父類的方法的重用。此外,如果還需要擴展一項功能,例如增加一個透明窗體類TransparentWindow,它是Window類的子類,可以將一個窗體設置爲透明窗體,現在需要一個同時擁有三項功能(帶滾動條、帶黑色邊框、透明)的窗體,必須再增加一個類作爲三個窗體類的子類,這同樣在Java等語言中無法實現。系統在擴展時非常麻煩,有時候甚至無法實現。
  • (2)代碼重複。從圖12-2中我們可以看出,不只是窗體需要設置滾動條,文本框、列表框等都需要設置滾動條,因此在ScrollBarWindow、ScrollBarTextBox和ScrollBarListBox等類中都包含用於增加滾動條的方法setScrollBar(),該方法的具體實現過程基本相同,代碼重複,不利於對系統進行修改和維護。
  • (3) 系統龐大,類的數目非常多。如果增加新的控件或者新的擴展功能系統都需要增加大量的具體類,這將導致系統變得非常龐大。在圖12-2中,3種基本控件和2種擴展方式需要定義9個具體類;如果再增加一個基本控件還需要增加3個具體類;增加一種擴展方式則需要增加更多的類,如果存在3種擴展方式,對於每一個控件而言,需要增加7個具體類,因爲這3種擴展方式存在7種組合關係(大家自己分析爲什麼需要7個類?)。

&esmp; 總之,圖12-2不是一個好的設計方案,怎麼辦?如何讓系統中的類可以進行擴展但是又不會導致類數目的急劇增加?不用着急,讓我們先來分析爲什麼這個設計方案會存在如此多的問題。根本原因在於複用機制的不合理,圖12-2採用了繼承複用,例如在ScrollBarWindow中需要複用Window類中定義的display()方法,同時又增加新的方法setScrollBar(),ScrollBarTextBox和ScrollBarListBox都必須做類似的處理,在複用父類的方法後再增加新的方法來擴展功能。根據“合成複用原則”,在實現功能複用時,我們要多用關聯,少用繼承,因此我們可以換個角度來考慮,將setScrollBar()方法抽取出來,封裝在一個獨立的類中,在這個類中定義一個Component類型的對象,通過調用Component的display()方法來顯示最基本的構件,同時再通過setScrollBar()方法對基本構件的功能進行增強。由於Window、ListBox和TextBox都是Component的子類,根據“里氏代換原則”,程序在運行時,我們只要向這個獨立的類中注入具體的Component子類的對象即可實現功能的擴展。這個獨立的類一般稱爲裝飾器(Decorator)或裝飾類,顧名思義,它的作用就是對原有對象進行裝飾,通過裝飾來擴展原有對象的功能。

  裝飾類的引入將大大簡化本系統的設計,它也是裝飾模式的核心,下面讓我們正式進入裝飾模式的學習。

2、裝飾模式概述

&esmp; 裝飾模式可以在不改變一個對象本身功能基礎上給對象增加額外的新行爲,在現實生活中,這種情況也到處存在,例如一張照片,我們可以不改變照片本身,給它增加一個相框,使得它具有防潮的功能,而且用戶可以根據需要給它增加不同類型的相框,甚至可以在一個小相框的外面在套一個大相框。

  裝飾模式是一種用於替代繼承的技術,它通過無須定義子類的方式來給對象動態增加職責,使用對象之間的關聯關係取代類之間的繼承關係。在裝飾模式中引入了裝飾類,在裝飾類中即可以調用待裝飾的原有類的方法,還可以增加新的方法,以擴充原有類的功能。

2.1、裝飾模式定義

  • 裝飾模式(Decorator Pattern):動態地給一個對象增加一些額外的職責,就增加對象功能來說,裝飾模式比生產子類實現更爲靈活。裝飾模式是一種對象結構型模式。

2.2、裝飾模式結構圖中角色

&esmp; 在裝飾模式中,爲了讓系統具有更好的靈活性和可擴展性,我們通常會定義一個抽象裝飾類,而將具體的裝飾類作爲它的子類,裝飾模式結構如圖2.2-1所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-4011Tdxy-1591711663929)(./images/model_decorator.png)]
圖2.2-1 裝飾模式結構圖

  在裝飾模式結構圖中包含如下幾個角色:

  • Component(抽象構件):它是具體構件和抽象裝飾類的共同父類,聲明瞭在具體構件中實現的業務方法,它的引入可以使客戶端以一致的方式未被裝飾對象以及裝飾之後的對象,實現客戶端的透明操作。
  • ConcreteComponent(具體構件):它是抽象構件類的子類,用於定義具體的構建對象,實現了在抽象構件中聲明的方法,裝飾器可以給它增加額外的職責(方法)。
  • Decorator(抽象裝飾類):它也是抽象構件類的子類,用於給具體構件增加職責,但是具體職責在其子類中實現。它維護一個指向抽象構件對象的引用,通過
  • ConcreteDecorator(具體裝飾類):它是抽象裝飾類的子類,負責向構件添加新的職責。每一個具體裝飾類都定義了一些新的行爲,它額可以調用在抽象裝飾類中定義的方法,並可以增加新的方法用以擴充對象的行爲。

  由於具體構件類和裝飾類都實現了相同的抽象構件接口,因此裝飾模式以對客戶透明的方法動態地給一個對象附加上更多的職責,換言之,客戶端並不會覺得對象在裝飾前和裝飾後又什麼不同。裝飾模式可以在不需要創造更多子類的情況下,將對象的功能加以擴展。

2.3、裝飾模式典型實現

  裝飾模式的核心在於抽象裝飾類的設計,其典型代碼如下所示:

class Decorator implements Component
{
	private Component component; //維持一個對抽象構件對象的引用
	public Decorator(Component component) //注入一個抽象構件類型的對象
	{
	this.component=component;
	}
	public void operation()
	{
	component.operation(); //調用原有業務方法
	}
}

  在抽象裝飾類Decorator中定義了一個Component類型的對象component,維持一個對抽象構件對象的引用,並可以通過構造方法或Setter方法將一個Component類型的對象注入進來,同時由於Decorator類實現了抽象構件Component接口,因此需要實現在其中聲明的業務方法operation(),需要注意的是在Decorator中並未真正實現operation()方法,而只是調用原有component對象的operation()方法,它沒有真正實施裝飾,而是提供一個統一的接口,將具體裝飾過程交給子類完成。

  在Decorator的子類即具體裝飾類中將繼承operation()方法並根據需要進行擴展,典型的具體裝飾類代碼如下:

class ConcreteDecorator extends Decorator
{
	public ConcreteDecorator(Component component)
	{
	super(component);
	}
	public void operation()
	{
	super.operation(); //調用原有業務方法
	addedBehavior(); //調用新增業務方法
	}
	//新增業務方法
	public void addedBehavior()
	{
	……
	}
}

 &emsp ;在具體裝飾類中可以調用到抽象裝飾類的operation()方法,同時可以定義新的業務方法,如addedBehavior()。

  由於在抽象裝飾類Decorator中注入的是Component類型的對象,因此我們可以將一個具體構件對象注入其中,再通過具體裝飾類來進行裝飾;此外,我們還可以將一個已經裝飾過的Decorator子類的對象再注入其中進行多次裝飾,從而對原有功能的多次擴展。

3、完整解決方案

  爲了讓系統具有更好的靈活性和可擴展性,克服繼承複用所帶來的問題,Sunny公司開發人員使用裝飾模式來重構圖形界面構件庫的設計,其中部分類的基本結構如圖3-1所示:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-ZgqKyZAc-1591711663930)(./images/gui_decorator.png)]
圖3-1 圖形界面構件庫結構圖

  在圖3-1中,Component充當抽象構件類,其子類Window、TextBox、ListBox充當具體構件類,Component類的另一個子類ComponentDecorator充當抽象裝飾類,ComponentDecorator的子類ScrollBarDecorator和BlackBorderDecorator充當具體裝飾類。完整代碼如下所示:

  • Component類代碼3-1:抽象界面構件類-抽象構件類

      package decorator;
    
      //抽象界面構件類:抽象構件類,爲了突出與模式相關的核心代碼,對原有控件代碼進行了大量簡化
      public abstract class Component {
      	public abstract void display();
      }
    
  • Window類代碼3-2:窗體類-具體構件類

      package decorator;
    
      //窗體類:具體構件類
      public class Window extends Component{
    
      	@Override
      	public void display() {
      		System.out.println("顯示窗體");
      	}
      }
    
  • TextBox類代碼3-3:文本框類-具體構件類

      package decorator;
    
      //文本框類:具體構件類
      public class TextBox extends Component{
    
      	@Override
      	public void display() {
      		System.out.println("顯示文本框");
      	}
      }
    
  • ListBox類代碼3-4:列表框類-具體構件類

      package decorator;
    
      //列表框類:具體構件類
      public class ListBox extends Component {
    
      	@Override
      	public void display() {
      		System.out.println("顯示列表框");
      	}
      }
    
  • ComponentDecorator類代碼3-5:構件裝飾類-抽象裝飾類

      package decorator;
    
      //構件裝飾類:抽象裝飾類
      public class ComponentDecorator extends Component{
      	private Component component; //維持對抽象構件類型對象的引用
    
      	//注入抽象構件類型的對象
      	public ComponentDecorator(Component component) {
      		this.component = component;
      	}
    
      	@Override
      	public void display() {
      		component.display();
      	}
      }
    
  • ScrollBarDecorator類代碼3-6:滾動條裝飾類-具體裝飾類

      package decorator;
    
      //滾動條裝飾類:具體裝飾類
      public class ScrollBarDecorator extends ComponentDecorator {
      	public ScrollBarDecorator(Component component) {
      		super(component);
      	}
    
      	@Override
      	public void display() {
      		this.setScrollBar();
      		super.display();
      	}
    
      	public void setScrollBar() {
      		System.out.println("爲組件增加滾動條!");
      	}
      }
    
  • BlackBorderDecorator類代碼3-7:黑色邊框裝飾類-具體裝飾類

      package decorator;
    
      //黑色邊框裝飾類:具體裝飾類
      public class BlackBorderDecorator extends ComponentDecorator {
      	public BlackBorderDecorator(Component component) {
      		super(component);
      	}
    
      	@Override
      	public void display() {
      		this.setBlackBorder();
      		super.display();
      	}
    
      	public void setBlackBorder() {
      		System.out.println("爲組件增加黑色邊框!");
      	}
      }
    
  • Utils類代碼3-8:工具類-獲取具體組件對象和具體裝飾器對象

      package decorator;
    
      import java.util.Properties;
    
      public class Utils {
      	public static Object getObject(String args) {
      		try {
      			Properties prop = new Properties();
      			prop.load(Utils.class.getClassLoader().getResourceAsStream("gui.properties"));
      			String className = prop.getProperty(args);
      			return (Class.forName(className).newInstance());
      		}catch (Exception e) {
      			e.printStackTrace();
      			return null;
      		}
      	}
    
      	public static Component getComponentDecorator(Component component) {
      		try {
      			Component com = component;
      			Properties prop = new Properties();
      			prop.load(Utils.class.getClassLoader().getResourceAsStream("gui.properties"));
      			String[] className = prop.getProperty("decorator").split(",");
      			Class<?> cls = null;
      			for(String name: className) {
      				com = (Component) Class.forName(name).getConstructor(Component.class).newInstance(com);
      			}
      			return com;
      		}catch (Exception e) {
      			e.printStackTrace();
      			return null;
      		}
      	}
      }
    
  • gui.properties 配置文件3-1:配置具體組件類名稱和具體裝飾器類名稱

      component=decorator.Window
      decorator=decorator.BlackBorderDecorator
    
  • Client類代碼3-9:測試類

      package decorator;
    
      public class Client {
      	public static void main(String[] args) {
      		Component component,componentSB; //使用抽象構件定義
      		component = (Component) Utils.getObject("component"); //定義具體構件
      		componentSB = Utils.getComponentDecorator(component); //定義裝飾後的構件
      		componentSB.display();
      	}
      }
    
  • 測試結果:

      爲組件增加黑色邊框!
      顯示窗體
    

  在客戶端代碼中,我們先定義了一個Window類型的具體構件對象component,然後將component作爲構造函數的參數注入到具體裝飾類ScrollBarDecorator中,得到一個裝飾之後對象componentSB,再調用componentSB的display()方法後將得到一個有滾動條的窗體。如果我們希望得到一個既有滾動條又有黑色邊框的窗體,不需要對原有類庫進行任何修改,只需將配置文件gui.properties改爲如下所示:

component=decorator.Window
decorator=decorator.BlackBorderDecorator,decorator.ScrollBarDecorator

測試結果:
爲組件增加滾動條!
爲組件增加黑色邊框!
顯示窗體

  如果需要在原有系統中增加一個新的具體構件類或者新的具體裝飾類,無須修改現有類庫代碼,只需將它們分別作爲抽象構件類或者抽象裝飾類的子類即可。與繼承結構相比,使用裝飾模式之後將大大減少了子類的個數,讓系統擴展起來更加方便,而且更容易維護,是取代繼承複用的有效方式之一。

4、透明裝飾模式與半透明裝飾模式

  裝飾模式雖好,但存在一個問題。如果客戶端希望單獨調用具體裝飾類新增的方法,而不想通過抽象構件中聲明的方法來調用新增方法時將遇到一些麻煩,我們通過一個實例來對這種情況加以說明: 在Sunny軟件公司開發的Sunny OA系統中,採購單(PurchaseRequest)和請假條(LeaveRequest)等文件(Document)對象都具有顯示功能,現在要爲其增加審批、刪除等功能,使用裝飾模式進行設計。

  我們使用裝飾模式可以得到如圖4-1所示結構圖:
[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-nhpcDKPQ-1591711663932)(./images/OA.png)]
圖4-1 文件對象功能增加實例結構圖

  在圖4-1中,Document充當抽象構件類,PurchaseRequest和LeaveRequest充當具體構件類,Decorator充當抽象裝飾類,Approver和Deleter充當具體裝飾類。其中Decorator類和Approver類的示例代碼如下所示:

//抽象裝飾類
class Decorator implements Document
{
	private Document document;
	public Decorator(Document document)
	{
	this. document = document;
	}
	public void display()
	{
	document.display();
	}
}
//具體裝飾類
class Approver extends Decorator
	{
	public Approver(Document document)
	{
	super(document);
	System.out.println("增加審批功能!");
	}
	public void approve()
	{
	System.out.println("審批文件!");
	}
}

  大家注意,Approver類繼承了抽象裝飾類Decorator的display()方法,同時新增了業務方法approve(),但這兩個方法是獨立的,沒有任何調用關係。如果客戶端需要分別調用這兩個方法,代碼片段如下所示:

Document doc; //使用抽象構件類型定義
doc = new PurchaseRequest();
Approver newDoc; //使用具體裝飾類型定義
newDoc = new Approver(doc);
newDoc.display();//調用原有業務方法
newDoc.approve();//調用新增業務方法

  如果newDoc也使用Document類型來定義,將導致客戶端無法調用新增業務方法approve(),因爲在抽象構件類Document中沒有對approve()方法的聲明。也就是說,在客戶端無法統一對待裝飾之前的具體構件對象和裝飾之後的構件對象。

  在實際使用過程中,由於新增行爲可能需要單獨調用,因此這種形式的裝飾模式也經常出現,這種裝飾模式被稱爲半透明(Semi-transparent)裝飾模式,而標準的裝飾模式是透明(Transparent)裝飾模式。下面我們對這兩種裝飾模式進行較爲詳細的介紹:

4.1、透明裝飾模式

  在透明裝飾模式中,要求客戶端完全針對抽象編程,裝飾模式的透明性要求客戶端程序不應該將對象聲明爲具體構件類型或具體裝飾類型,而應該全部聲明爲抽象構件類型。對於客戶端而言,具體構件對象和具體裝飾對象沒有任何區別。也就是應該使用如下代碼:

Component c, c1; //使用抽象構件類型定義對象
c = new ConcreteComponent();
c1 = new ConcreteDecorator (c);
而不應該使用如下代碼:
ConcreteComponent c; //使用具體構件類型定義對象
c = new ConcreteComponent();
或
ConcreteDecorator c1; //使用具體裝飾類型定義對象
c1 = new ConcreteDecorator(c);

  在圖形界面構件庫的設計方案中使用的就是透明裝飾模式,在客戶端中存在如下代碼片段:

……
Component component,componentSB,componentBB; //全部使用抽象構件定義
component = new Window();
componentSB = new ScrollBarDecorator(component);
componentBB = new BlackBorderDecorator(componentSB);
componentBB.display();
……

  使用抽象構件類型Component定義全部具體構件對象和具體裝飾對象,客戶端可以一致地使用這些對象,因此符合透明裝飾模式的要求。
  透明裝飾模式可以讓客戶端透明地使用裝飾之前的對象和裝飾之後的對象,無須關心它們的區別,此外,還可以對一個已裝飾過的對象進行多次裝飾,得到更爲複雜、功能更爲強大的對象。在實現透明裝飾模式時,要求具體裝飾類的operation()方法覆蓋抽象裝飾類的operation()方法,除了調用原有對象的operation()外還需要調用新增的addedBehavior()方法來增加新行爲,

4.2、半透明裝飾模式

  透明裝飾模式的設計難度較大,而且有時我們需要單獨調用新增的業務方法。爲了能夠調用到新增方法,我們不得不用具體裝飾類型來定義裝飾之後的對象,而具體構件類型還是可以使用抽象構件類型來定義,這種裝飾模式即爲半透明裝飾模式,也就是說,對於客戶端而言,具體構件類型無須關心,是透明的;但是具體裝飾類型必須指定,這是不透明的。如本節前面所提到的文件對象功能增加實例,爲了能夠調用到在Approver中新增方法approve(),客戶端代碼片段如下所示:

……
Document doc; //使用抽象構件類型定義
doc = new PurchaseRequest();
Approver newDoc; //使用具體裝飾類型定義
newDoc = new Approver(doc);
……

  半透明裝飾模式可以給系統帶來更多的靈活性,設計相對簡單,使用起來也非常方便;但是其最大的缺點在於不能實現對同一個對象的多次裝飾,而且客戶端需要有區別地對待裝飾之前的對象和裝飾之後的對象。在實現半透明的裝飾模式時,我們只需在具體裝飾類中增加一個獨立的addedBehavior()方法來封裝相應的業務處理,由於客戶端使用具體裝飾類型來定義裝飾後的對象,因此可以單獨調用addedBehavior()方法來擴展系統功能。

5、裝飾模式注意事項

  在使用裝飾模式時,通常我們需要注意以下幾個問題:

  • (1) 儘量保持裝飾類的接口與被裝飾類的接口相同,這樣,對於客戶端而言,無論是裝飾之前的對象還是裝飾之後的對象都可以一致對待。這也就是說,在可能的情況下,我們應該儘量使用透明裝飾模式。
  • (2) 儘量保持具體構件類ConcreteComponent是一個“輕”類,也就是說不要把太多的行爲放在具體構件類中,我們可以通過裝飾類對其進行擴展。
  • (3) 如果只有一個具體構件類,那麼抽象裝飾類可以作爲該具體構件類的直接子類。如圖5-1所示:
    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-aZ5IuzQG-1591711663934)(./images/oneComponent.png)]
    圖5-1 沒有抽象構件類的裝飾模式

6、總結

  裝飾模式降低了系統的耦合度,可以動態增加或刪除對象的職責,並使得需要裝飾的具體構件類和具體裝飾類可以獨立變化,以便增加新的具體構件類和具體裝飾類。在軟件開發中,裝飾模式應用較爲廣泛,例如在JavaIO中的輸入流和輸出流的設計、javax.swing包中一些圖形界面構件功能的增強等地方都運用了裝飾模式。

5.1、優缺點

  • 主要優點
    • (1) 對於擴展一個對象的功能,裝飾模式比繼承更加靈活性,不會導致類的個數急劇增加。
    • (2) 可以通過一種動態的方式來擴展一個對象的功能,通過配置文件可以在運行時選擇不同的具體裝飾類,從而實現不同的行爲。
    • (3)) 可以對一個對象進行多次裝飾,通過使用不同的具體裝飾類以及這些裝飾類的排列組合,可以創造出很多不同行爲的組合,得到功能更爲強大的對象。
    • (4) 具體構件類與具體裝飾類可以獨立變化,用戶可以根據需要增加新的具體構件類和具體裝飾類,原有類庫代碼無須改變,符合“開閉原則”。
  • 主要缺點:
    • (1)使用裝飾模式進行系統設計時將產生很多小對象,這些對象的區別在於它們之間相互連接的方式有所不同,而不是它們的類或者屬性值有所不同,大量小對象的產生勢必會佔用更多的系統資源,在一定程序上影響程序的性能。
    • (2)) 裝飾模式提供了一種比繼承更加靈活機動的解決方案,但同時也意味着比繼承更加易於出錯,排錯也很困難,對於多次裝飾的對象,調試時尋找錯誤可能需要逐級排查,較爲繁瑣。

5.2、適用場景

  在以下情況下可以考慮使用裝飾模式:

  • (1) 在不影響其他對象的情況下,以動態、透明的方式給單個對象添加職
  • (2) 當不能採用繼承的方式對系統進行擴展或者採用繼承不利於系統擴展和維護時可以使用裝飾模式。不能採用繼承的情況主要有兩類:第一類是系統中存在大量獨立的擴展,爲支持每一種擴展或者擴展之間的組合將產生大量的子類,使得子類數目呈爆炸性增長;第二類是因爲類已定義爲不能被繼承(如Java語言中的final類)。

後記

  參考文獻:Java設計模式(劉偉).pdf。持續更新,歡迎交流,本人QQ:806797785

前端項目源代碼地址:https://gitee.com/gaogzhen/vue-leyou
後端JAVA源代碼地址:https://gitee.com/gaogzhen/JAVA
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章