Pytania odnośnie oznaczeń na diagramach UML - Wzorzec Budowniczy

0

TypeScript + PlantUML - W Internecie znajduję różne oznaczenia, więc może jest to kwestia konwencji - chciałbym jednak się poradzić, w jaki sposób powinienem poprawnie oznaczyć relacje na diagramie Budowniczego pomiędzy:
a). interfejsem Builder i klasą Director - czy ciągła strzałka z niezamalowanym rombem mówi "klasa zawiera egzemplarz interfejsu"?
b). klasą Crew i klasą House - czy ciągłą strzałka mówi "klasa zawiera egzemplarz innej klasy"?
builder
c). w sumie... Director bez Buildera nic nie zbuduje, więc może romb powinien być zamalowany?

2

a) To jest "agregacja" czyli zawieranie, ale obiekty maja osobną "tożsamość", tzn ten builder może spokojnie istnieć bez twojego directora i można go używać samodzielnie
b) Na diagramie masz asocjacje, która normalnie sugeruje że obiekty danych klas ze sobą współpracują i nie ma tu mowy o żadnym "zawieraniu"
c) Ale to zamalowanie znaczyłoby coś zupełnie innego - znaczyłoby że nie twój Builder nie ma sensu bez Directora, co nie jest prawdą.

Ogólnie to jakiś srogi WTF ten cały diagram i generalnie w 99% przypadków wcale się takiego cyrku nie robi. W praktyce builder wygląda mniej wiecej tak:

class A{
    private final int x;
    public A(int x){
        this.x=x;
    }

    public static Builder builder(){
        return new Builder();
    }

    public static class Builder{
        int x;
        public Builder withX(int value){
            this.x = value;
            return this;
        }
        public A build(){
            return new A(x);
        }
    }
}

(normalnie oczywiście pól jest więcej i wiele z nich jest opcjonalna, plus build może robic jakieś walidacje parametrów i ewentualnie budować obiekty pod-klas)

Od biedy można tych builderów mieć niby więcej, ale to już wtedy jakis mix buildera z factory.

1

Jeśli chodzi o buildera można ograniczyć zakres jego dostępności (z głowy na podst. jakiejś książki o FP, chyba Functional Programming in Java):

class A{
    private final int x;
    
    private A(int x){
        this.x=x;
    }

    public int getX() { return x; }
    
    public static A build(Function<Builder, Builder> builderFiller){
        Builder builder = new Builder();
        builder = builderFiller.apply(builder);
        return builder.build();
    }

    public static class Builder{
        private int x;

        private Builder() {}

        public Builder withX(int value){
            this.x = value;
            return this;
        }

        private A build(){
            return new A(x);
        }
    }
}

Użycie:

A a = A.build(builder -> builder.withX(11));

https://ideone.com/Gufnwr

1

Pewnie dostanę brązową łopatę za odgrzebanie tego wątku...
Nie wiem co przedstawia diagram z pierwszego postu, ale nie jest to nic wspólnego ze znanymi mi wzorcami Builder.

Nie lubię tego wzorca, bo w 99% używa się go do przykrycia faktu że jakaś klasa ma 500 zależności i analiza statyczna ma SO po zaczytaniu samej sygnatury konstruktora. Pozostały 1% to użycie zgodnie z przeznaczeniem, ale wynikające z ubóstwa języka.

Sens użycia tego wzorca, to tworzenie obiektów (captain obvious) w sposób względnie wygodny dla programisty (bo język nie pozwala lepiej) i bezpieczny, czyli z wymuszeniem i sprawdzeniem, że wszystko co miało zostać przekazane, zostało przekazane. Głównym powodem użycia Buildera jest brak w języku parametrów opcjonalnych / domyślnych.

Gdybym NAPRAWDĘ MUSIAŁ zrobić jakąś skomplikowaną w inicjalizacji bibliotekę, to zakładając niezmienność stanów powstałby taki potworek:

package org.example.builder;

import java.time.Clock;
import java.time.Instant;
import java.util.Optional;

public class Person {
    private final String firstname;
    private final String lastname;
    private final Instant birthDate;

    private Person(String firstname, String lastname, Instant birthDate) {
        this.firstname = firstname;
        this.lastname = lastname;
        this.birthDate = birthDate;
    }

    public String getFirstname() {
        return firstname;
    }

    public String getLastname() {
        return lastname;
    }

    public Instant getBirthDate() {
        return birthDate;
    }

    public static class Builder{
        private final String fistName;
        private final String lastName;
        private final Instant birthDate;

        private Builder(String firstName, String lastName, Instant birthDate) {
            if(firstName == null || lastName == null || birthDate == null){
                throw new RuntimeException("Parameter is null");
            }
            
            this.fistName = firstName;
            this.lastName = lastName;
            this.birthDate = birthDate;
        }

        public Builder(String firstName, String lastName) {
            //just born...
            this(firstName, lastName, Clock.systemUTC().instant());
        }

        public Builder withBirthDdate(Instant birthDate){
            return new Builder(this.fistName, this.lastName, birthDate);
        }

        public Person build(){
            return new Person(this.fistName, this.lastName, this.birthDate);
        }
    }
}

Przykłady użycia:

Person person = new Person.Builder(first, last).build();

Person person = new Person.Builder(first, last)
    .withBirthDdate(birthDate)
    .build();

Dla kontrastu implementacja tego co wyżej w czymś odrobinę bardziej normalnym

class KotlinPerson(val firstName:String, val lastName:String, val birthDate: Instant = Clock.systemUTC().instant())

1 użytkowników online, w tym zalogowanych: 0, gości: 1