Wzorzec projektowy Kompozyt - okładka

Wzorzec projektowy Kompozyt

Opublikowano Kategorie Czysty kodCzas czytania 5min

Wzorzec projektowy Kompozyt (Composite) to jeden ze strukturalnych wzorców projektowych opisanych przez Gang of Four. W tym artykule poznasz zasadę działania tego wzorca oraz jego przykładowe zastosowania. Ten wpis jest kolejnym wpisem z serii o wzorcach projektowych. Jeśli chcesz poznać inne wzorce projektowe lub dowiedzieć się czym są wzorce projektowe, to koniecznie sprawdź mój wpis o wzorcach projektowych. Przykłady kodu zostały przygotowane w TypeScript.

Opis wzorca Kompozyt

Kompozyt jest wzorcem, który znajdzie swoje zastosowanie w aplikacjach, gdzie wykorzystywane są struktury drzewiaste. Celem Kompozytu jest uporządkowanie struktur hierarchicznych (drzewiastych) w kodzie. Drugi cel to zapewnienie jednolitego interfejsu, który będzie implementowany przez wszystkie elementy składające się na tę strukturę. Banda Czworga cel Kompozytu definiuje następująco:

Compose objects into tree structures to represent part-whole hierarchies. Composite lets clients treat individual objects and compositions of objects uniformly.

Elementy składowe Kompozytu

W zrozumieniu co składa się na Kompozyt, pomoże poniższy diagram klas.

Wzorzec projektowy Kompozyt - diagram klas UML

Na Kompozyt składają się:

  • Leaf — liść, czyli komponent, który nie ma swoich potomków;
  • Composite — komponent, który może posiadać swoich potomków;
  • Component — interfejs wymuszający implementację wspólnej metody lub wielu metod dla wszystkich klas. W zależności od aplikacji, zamiast metody operation w interfejsie będzie znajdować się inna metoda lub metody implementowane zarówno przez liście, jak i przez komponenty mogące mieć swoich potomków;
  • niektóre źródła podają czwarty element wzorca, którym jest Client, czyli obiekt, który manipuluje obiektami występującymi w strukturze z wykorzystaniem metod zdefiniowanych w interfejsie Component. Dla uproszczenia jednak moim zdaniem można go pominąć. Kod piszemy po to, by ktoś/coś go wykorzystywała, więc moim zdaniem fakt istnienia klienta jest oczywisty.

Przykłady wykorzystania Kompozytu

Pierwszym przykładem wykorzystania Kompozytu, jaki przyszedł mi do głowy, jest aplikacja generująca hierarchiczne dokumenty wykorzystujące zagnieżdżone struktury. Może to być np. aplikacja w JavaScript generująca dokumenty HTML. Struktura elementów HTML mogłaby być reprezentowana w kodzie przez następującą strukturę.

Struktura drzewiasta HTML

Liśćmi w przedstawionym przykładzie jest Text, czyli klasa reprezentująca tekst, który nie zawiera już kolejnych elementów potomnych. Natomiast wszystkie pozostałe klasy to elementy HTML, które mogą posiadać potomków. Przeanalizuj na spokojnie poniższy przykład.


interface IElement {
  id: string;
  render(): string;
}

abstract class HtmlElement implements IElement {
  public readonly id: string;
  private readonly _children: Map<string, IElement> = new Map();

  public constructor( private readonly _tag = '' ) {
    this.id = crypto.randomUUID();
  }

  public addChild( element: IElement ) {
    this._children.set( element.id, element );
  }

  public removeChild( id: string ) {
    this._children.delete( id );
  }

  public render(): string {
    const childrenContent: string[] = [];

    for ( const child of this._children.values() ) {
      childrenContent.push( child.render() );
    }

    return `<${ this._tag }>${ childrenContent.join( '' ) }</${ this._tag }>`;
  }
}

class Html extends HtmlElement {
  public constructor() {
    super('html');
  }
}

class Body extends HtmlElement {
  public constructor() {
    super('body');
  }
}

class Paragraph extends HtmlElement {
  public constructor() {
    super('p');
  }
}

class TextContent implements IElement {
    public readonly id: string;

    public constructor( private readonly _text: string ) {
      this.id = crypto.randomUUID();
    }

    public render(): string {
      return `${ this._text }`;
  }
}

const html = new Html();
const body = new Body();
const paragraph1 = new Paragraph();
const paragraph2 = new Paragraph();
const text1 = new TextContent( 'Hello,' );
const text2 = new TextContent( 'World!' );

html.addChild( body );
body.addChild( paragraph1 );
paragraph1.addChild( text1 )
paragraph1.addChild( paragraph2 );
paragraph2.addChild( text2 );

console.log( html.render() ); // <html><body><p>Hello,<p>World!</p></p></body></html>
console.log( body.render() ); // <body><p>Hello,<p>World!</p></p></body>

Implementacja metody render w klasie HtmlElement rekurencyjnie renderuje wszystkie elementy potomne aż do napotkania przypadku podstawowego, czyli sytuacji, gdy element metoda render elementu potomnego zwraca string. Możliwe jest również renderowanie części drzewa poprzez wywołanie metody render dla dowolnego elementu drzewa. Klient nie musi wtedy wiedzieć, że w tym przypadku obiekt body ma w sobie kolejne dzieci do wyrenderowania. Dla czytelności zdecydowałem się też na skorzystanie z dziedziczenia i stworzenie odpowiednich klas dla każdego elementu HTML, choć z technicznego punktu widzenia nie jest to konieczne.

Przykład został mocno uproszczony na potrzeby artykułu. W praktyce struktura ta byłaby znacznie bardziej rozbudowana. Dla elementów HTML powinna być możliwość np. definiowania atrybutów. Samych elementów również powinno być znacznie więcej.

W podobny sposób można by zaimplementować np. strukturę plików i katalogów, gdzie liściem jest pojedynczy plik.

Struktura plików

Podsumowanie

Kompozyt ma zastosowanie w ściśle określonym przypadku, czyli w aplikacjach wykorzystujących struktury drzewiaste. Jeśli miałbyś/miałabyś zapamiętać jedno z tego artykułu, to niech to będzie właśnie sytuacja, kiedy ten wzorzec może się przydać i jakie problemy rozwiązuje. Aby przypomnieć sobie szczegóły, zawsze możesz tu wrócić. Bardzo jestem ciekaw czy Ty miałeś/aś styczność z Kompozytem w praktyce. Zachęcam do opisania swoich doświadczeń w komentarzu pod postem 🙂

Książkę Design Patterns: Elements of Reusable Object-Oriented Software możesz kupić klikając w TEN link. Kupując z tego linku, wspierasz rozwój bloga.

Źródła i materiały dodatkowe

Dominik Szczepaniak

Zawodowo Senior Software Engineer w CKSource. Prywatnie bloger, fan włoskiej kuchni, miłośnik jazdy na rowerze i treningu siłowego.

Inne wpisy, które mogą Cię zainteresować

Zapisz się na mailing i odbierz e-booka

Zapisując się na mój mailing, otrzymasz darmowy egzemplarz e-booka 106 Pytań Rekrutacyjnych Junior JavaScript Developer! Będziesz też otrzymywać wartościowe treści i powiadomienia o nowych wpisach na skrzynkę e-mail.

Subscribe
Powiadom o
guest

0 komentarzy
Inline Feedbacks
View all comments