Archetypy Programowania

Party Pattern (Wzorzec podmiotu)

Based on "Enterprise Patterns and MDA" by Jim Arlow & Ila Neustadt

Problem

Musisz zamodelować Person i Organization w swoim systemie, ale:

  • Person i Organization mają wspólne zachowania (nazwa, dane kontaktowe, adresy)
  • Mają też unikalne atrybuty (Person ma datę urodzenia, Organization ma numer rejestrowy)
  • Chcesz uniknąć duplikowania kodu dla „Customer będący Person" vs „Customer będący Firmą"
  • Zapytania takie jak „znajdź wszystkie Party w Warszawie" powinny działać niezależnie od typu Party

Koncepcja

Party to abstrakcja reprezentująca dowolną encję, która może wchodzić w relacje, posiadać rzeczy lub uczestniczyć w transakcjach. Party jest albo Person (jednostka), albo Organization (firma, instytucja, grupa).

Pozwala to pisać kod operujący na „Party", który działa zarówno dla Person, jak i Organization.

Struktura

1

0..*

«abstract»

Party

+id: UUID

+name: String

+addresses: Address[]

+contacts: ContactMethod[]

+createdAt: Timestamp

+getName() : : String

+getPrimaryAddress() : : Address

Person

+firstName: String

+lastName: String

+birthDate: Date

+gender: Gender

+getName() : : String

Organization

+legalName: String

+tradingName: String

+registrationNumber: String

+taxId: String

+getName() : : String

Address

+street: String

+city: String

+postalCode: String

+country: String

+type: AddressType

+isPrimary: Boolean

Atrybuty

Party (abstrakcyjny)

Atrybut Typ Wymagane Opis
id UUID Tak Unikalny identyfikator
addresses Address[] Nie Kolekcja adresów
contacts Contact[] Nie Telefon, email itp.
createdAt Timestamp Tak Data utworzenia rekordu
updatedAt Timestamp Tak Czas ostatniej modyfikacji

Person

Atrybut Typ Wymagane Opis
firstName String Tak Imię
lastName String Tak Nazwisko
middleName String Nie Drugie imię (imiona)
birthDate Date Nie Data urodzenia
gender Enum Nie Male, Female, Other, Unknown

Organization

Atrybut Typ Wymagane Opis
legalName String Tak Oficjalna zarejestrowana nazwa
tradingName String Nie Nazwa handlowa
registrationNumber String Nie Numer rejestrowy firmy
taxId String Nie Numer identyfikacji podatkowej
foundedDate Date Nie Data założenia organizacji

Address

Atrybut Typ Wymagane Opis
street String Tak Adres ulicy z numerem
city String Tak Nazwa miasta
postalCode String Tak Kod pocztowy
country String Tak Kod kraju ISO (PL, US itp.)
type AddressType Tak Home, Work, Billing, Shipping
isPrimary Boolean Tak Czy to adres domyślny?

Zachowania

Entity Party (abstract):
    
    function getName(): String
        // Abstract - implemented by subclasses
    
    function getPrimaryAddress(): Address or None
        return addresses.find(a => a.isPrimary == true)
    
    function addAddress(address: Address):
        if address.isPrimary:
            // Ensure only one primary per type
            for existing in addresses:
                if existing.type == address.type:
                    existing.isPrimary = false
        addresses.add(address)
    
    function getAddressesByType(type: AddressType): Address[]
        return addresses.filter(a => a.type == type)


Entity Person extends Party:
    
    function getName(): String
        if middleName is not empty:
            return firstName + " " + middleName + " " + lastName
        return firstName + " " + lastName
    
    function getAge(): Integer or None
        if birthDate is None:
            return None
        return yearsBetween(birthDate, today())
    
    function isAdult(): Boolean
        age = getAge()
        return age is not None and age >= 18


Entity Organization extends Party:
    
    function getName(): String
        if tradingName is not empty:
            return tradingName
        return legalName
    
    function getOfficialName(): String
        return legalName

Przykłady

Person: Jan Kowalski

jan = new Person(
    firstName: "Jan",
    lastName: "Kowalski",
    middleName: "Adam",
    birthDate: 1985-03-15,
    gender: Gender.Male
)
jan.addAddress(new Address(
    street: "ul. Marszalkowska 10/5",
    city: "Warszawa",
    postalCode: "00-001",
    country: "PL",
    type: AddressType.Home,
    isPrimary: true
))

jan.getName()   // "Jan Adam Kowalski"
jan.getAge()    // 41
jan.isAdult()   // true

Organization: Acme Sp. z o.o.

acme = new Organization(
    legalName: "Acme Sp. z o.o.",
    tradingName: "Acme",
    registrationNumber: "0000123456",
    taxId: "5261234567"
)
acme.addAddress(new Address(
    street: "ul. Polna 22",
    city: "Krakow",
    postalCode: "30-001",
    country: "PL",
    type: AddressType.Work,
    isPrimary: true
))

acme.getName()          // "Acme" (tradingName)
acme.getOfficialName()  // "Acme Sp. z o.o."

Niezmienniki

  1. Party musi mieć nazwę — Person wymaga firstName + lastName; Organization wymaga legalName
  2. Tylko jeden adres główny na typ — Nie można mieć dwóch głównych adresów rozliczeniowych
  3. Wiek Person nie może być ujemny — birthDate musi być w przeszłości
  4. Numer rejestrowy Organization jest unikalny — Żadne dwie Organization nie mogą mieć tego samego registrationNumber (w obrębie jednej jurysdykcji)

Relacje

Od Do Liczność Opis
Party Address 1 do wielu Party ma wiele adresów
Party ContactMethod 1 do wielu Party ma wiele kontaktów
Party PartyRole 1 do wielu Party pełni wiele ról
Organization Person wiele do wielu Poprzez relację Employment

Obsługa błędów

Operacja Warunek wstępny Błąd
new Person(...) firstName i lastName wymagane ValidationError: Person requires firstName and lastName
new Organization(...) legalName wymagane ValidationError: Organization requires legalName
addAddress(isPrimary=true) — Brak błędu; istniejący główny adres tego samego typu zostaje zdegradowany
Person.getAge() birthDate jest ustawione Zwraca None jeśli birthDate jest null (brak błędu)

Schemat SQL

sql-- Using single-table inheritance with discriminator column
CREATE TABLE party (
    id              UUID PRIMARY KEY,
    party_type      VARCHAR(20) NOT NULL,  -- 'Person' or 'Organization'
    
    -- Person fields
    first_name      VARCHAR(100),
    last_name       VARCHAR(100),
    middle_name     VARCHAR(100),
    birth_date      DATE,
    gender          VARCHAR(10),
    
    -- Organization fields
    legal_name      VARCHAR(255),
    trading_name    VARCHAR(255),
    registration_number VARCHAR(50),
    tax_id          VARCHAR(50),
    founded_date    DATE,
    
    -- Common fields
    created_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    
    -- Constraints
    CONSTRAINT chk_party_type CHECK (party_type IN ('Person', 'Organization')),
    CONSTRAINT chk_person_name CHECK (
        party_type != 'Person' OR (first_name IS NOT NULL AND last_name IS NOT NULL)
    ),
    CONSTRAINT chk_org_name CHECK (
        party_type != 'Organization' OR legal_name IS NOT NULL
    )
);

CREATE TABLE address (
    id              UUID PRIMARY KEY,
    party_id        UUID NOT NULL REFERENCES party(id) ON DELETE CASCADE,
    street          VARCHAR(255) NOT NULL,
    city            VARCHAR(100) NOT NULL,
    postal_code     VARCHAR(20) NOT NULL,
    country         CHAR(2) NOT NULL,  -- ISO 3166-1 alpha-2
    address_type    VARCHAR(20) NOT NULL,  -- 'Home', 'Work', 'Billing', 'Shipping'
    is_primary      BOOLEAN NOT NULL DEFAULT FALSE,
    created_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Index for common queries
CREATE INDEX idx_party_type ON party(party_type);
CREATE INDEX idx_address_party ON address(party_id);
CREATE INDEX idx_address_type ON address(party_id, address_type);

Zagadnienia projektowe

Dziedziczenie jednej tabeli vs łączenie tabel

Podejście Zalety Wady
Jedna tabela Proste zapytania, szybkie Wiele kolumn NULL, marnowanie miejsca
Łączenie tabel Znormalizowane, indeksy specyficzne dla typu Złożone zapytania, więcej JOINów
Tabela konkretna Brak NULLi, izolowane typy Nie można łatwo odpytywać wszystkich Party

Zalecenie: Jedna tabela dla Party (niewiele kolumn specyficznych dla typu), łączenie tabel dla złożonych hierarchii.

Kiedy stosować wzorzec Party

✅ Stosuj gdy:

  • Zarówno Person, jak i Organization mogą być Customer/Supplier/Employee
  • Potrzebujesz ujednoliconych zapytań niezależnie od typu Party
  • Współdzielone zachowania (adresy, kontakty, role)

❌ Nie stosuj gdy:

  • Masz do czynienia tylko z jednym typem (tylko Person LUB tylko Organization)
  • Typy mają drastycznie różne atrybuty bez części wspólnej
  • Prosty CRUD bez logiki biznesowej
  • Party Role — Jaką rolę pełni Party? (Customer, Supplier, Employee)
  • Party Relationship — Jak Party są powiązane? (Employment, Ownership)
  • Party Identifier — Zewnętrzne identyfikatory (Paszport, NIP, Numer Customer)