Archetypy Programowania

Party Relationship Pattern (Wzorzec relacji między podmiotami)

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

Problem

Party nie istnieją w izolacji — łączą je relacje:

  • Person pracuje dla Organization (Employment)
  • Organization posiada inną Organization (spółka zależna)
  • Person jest w związku małżeńskim z inną Person
  • Organization dostarcza do innej Organization

Te relacje:

  • Mają własne atrybuty (stanowisko w Employment, procent Ownership)
  • Są ograniczone czasowo (zatrudniony od 2020 do 2023)
  • Można je nawigować w obu kierunkach („kto pracuje dla X?" i „gdzie pracuje Y?")

Koncepcja

Party Relationship modeluje połączenia między dwoma Party. Każda relacja ma typ, który definiuje naturę połączenia i role, jakie pełni każdy Party (np. „pracodawca" i „Employee" w relacji Employment).

Sama relacja jest encją, która może mieć atrybuty, cykl życia i zachowania.

Struktura

fromParty

toParty

1

1

0..*

0..*

Party

+id: UUID

+name: String

«abstract»

PartyRelationship

+id: UUID

+fromParty: Party

+toParty: Party

+validFrom: Date

+validTo: Date

+isActive() : : Boolean

Employment

+jobTitle: String

+department: String

+employmentType: EmploymentType

Ownership

+ownershipPercentage: Decimal

+votingRights: Boolean

Marriage

+marriageDate: Date

+divorceDate: Date

CustomerRelationship

+accountManager: Employee

+contractValue: Money

Typy relacji

Typ Party źródłowy Party docelowy Przykład
Employment Person Organization „Jan pracuje dla Acme Corp"
Ownership Party Organization „Holding posiada 60% Spółki Zależnej"
Marriage Person Person „Anna jest w związku małżeńskim z Piotrem"
CustomerRelationship Organization Organization „Acme jest Customer Supplier Co"
ParentChild Person Person „Maria jest rodzicem Kasi"
Partnership Organization Organization „Firma A jest partnerem Firmy B"

Atrybuty

PartyRelationship (klasa abstrakcyjna)

Atrybut Typ Wymagane Opis
id UUID Tak Unikalny identyfikator
fromParty Party Tak Pierwszy Party w relacji
toParty Party Tak Drugi Party w relacji
validFrom Date Tak Kiedy relacja się rozpoczęła
validTo Date Nie Kiedy relacja się zakończyła
notes String Nie Dodatkowe informacje

Employment

Atrybut Typ Wymagane Opis
jobTitle String Tak Stanowisko
department String Nie Nazwa działu
employmentType EmploymentType Tak FullTime, PartTime, Contract
workLocation String Nie Location biura
reportsTo Employment Nie Rekord zatrudnienia przełożonego

Ownership

Atrybut Typ Wymagane Opis
ownershipPercentage Decimal Tak Procent Ownership (0-100)
votingRights Boolean Tak Czy posiada prawo głosu?
shareClass String Nie Klasa akcji (A, B, itd.)

CustomerRelationship

Atrybut Typ Wymagane Opis
accountManager Employee Nie Przypisany opiekun klienta
contractValue Money Nie Roczna wartość kontraktu
tier Enum Nie Strategic, Key, Standard
nda Boolean Nie Czy podpisano NDA?

Zachowania

Entity PartyRelationship (abstract):
    
    function isActive(): Boolean
        today = currentDate()
        return validFrom <= today and (validTo is None or validTo >= today)
    
    function terminate(endDate: Date, reason: String):
        require endDate >= validFrom
        require validTo is None  // Not already terminated
        
        validTo = endDate
        terminationReason = reason
        
        emit RelationshipTerminated(this, endDate, reason)
    
    function overlaps(other: PartyRelationship): Boolean
        // Check if two relationships overlap in time
        if validTo is None and other.validTo is None:
            return true  // Both ongoing
        if validTo is None:
            return other.validTo >= validFrom
        if other.validTo is None:
            return validTo >= other.validFrom
        return not (validTo < other.validFrom or other.validTo < validFrom)
    
    function getOtherParty(party: Party): Party
        if fromParty == party:
            return toParty
        if toParty == party:
            return fromParty
        raise Error("Party not in relationship")


Entity Employment extends PartyRelationship:
    
    // fromParty = Employee (Person)
    // toParty = Employer (Organization)
    
    function getEmployee(): Person
        return fromParty as Person
    
    function getEmployer(): Organization
        return toParty as Organization
    
    function promote(newTitle: String, newDepartment: String):
        require isActive()
        
        previousTitle = jobTitle
        jobTitle = newTitle
        department = newDepartment
        
        emit EmployeePromoted(this, previousTitle, newTitle)
    
    function transfer(newDepartment: String, newLocation: String):
        require isActive()
        
        department = newDepartment
        workLocation = newLocation
        
        emit EmployeeTransferred(this, newDepartment, newLocation)


Entity Ownership extends PartyRelationship:
    
    // fromParty = Owner
    // toParty = Owned Organization
    
    function isMajorityOwner(): Boolean
        return ownershipPercentage > 50.0
    
    function adjustOwnership(newPercentage: Decimal):
        require newPercentage >= 0 and newPercentage <= 100
        require isActive()
        
        previousPercentage = ownershipPercentage
        ownershipPercentage = newPercentage
        
        emit OwnershipChanged(this, previousPercentage, newPercentage)


// Query helpers on Party
Entity Party:
    
    function getRelationshipsFrom(): PartyRelationship[]
        return findAll PartyRelationship where fromParty == this
    
    function getRelationshipsTo(): PartyRelationship[]
        return findAll PartyRelationship where toParty == this
    
    function getAllRelationships(): PartyRelationship[]
        return getRelationshipsFrom() + getRelationshipsTo()
    
    function getActiveEmployments(): Employment[]
        return getRelationshipsFrom()
            .filter(r => r is Employment and r.isActive())
    
    function getEmployees(): Person[]  // For Organization
        return findAll Employment 
            where toParty == this and isActive()
            .map(e => e.fromParty)

Przykłady

Employment: Jan pracuje dla Acme

jan = findParty("Jan Kowalski")     // Person
acme = findParty("Acme Sp. z o.o.") // Organization

employment = new Employment(
    fromParty: jan,
    toParty: acme,
    validFrom: 2022-06-01,
    jobTitle: "Senior Developer",
    department: "Engineering",
    employmentType: EmploymentType.FullTime
)

employment.getEmployee()  // jan
employment.getEmployer()  // acme
employment.isActive()     // true

// Jan gets promoted
employment.promote("Tech Lead", "Engineering")
// emits EmployeePromoted(employment, "Senior Developer", "Tech Lead")

Ownership: Holding posiada Spółkę Zależną

holding = findParty("Holding S.A.")
subsidiary = findParty("Acme Sp. z o.o.")

ownership = new Ownership(
    fromParty: holding,
    toParty: subsidiary,
    validFrom: 2018-01-01,
    ownershipPercentage: 75.0,
    votingRights: true,
    shareClass: "A"
)

ownership.isMajorityOwner()  // true (75% > 50%)

// Ownership diluted after new investment round
ownership.adjustOwnership(51.0)
// emits OwnershipChanged(ownership, 75.0, 51.0)

Niezmienniki

  1. Różne Party — fromParty i toParty muszą być różne (brak relacji z samym sobą)
  2. Prawidłowy zakres dat — validFrom musi być wcześniejsze lub równe validTo
  3. Ograniczenia typu Party — Employment wymaga relacji Person → Organization
  4. Suma Ownership — Łączna Ownership Organization nie może przekraczać 100%
  5. Brak duplikatów aktywnych relacji — Ten sam typ między tymi samymi Party nie powinien się nakładać

Obsługa błędów

Operacja Naruszony warunek wstępny Błąd
terminate(endDate, reason) endDate < validFrom PreconditionError: end date before start date
terminate(endDate, reason) Już zakończona PreconditionError: relationship already terminated
getOtherParty(party) Party nie jest w relacji Error: Party not in relationship
adjustOwnership(pct) pct < 0 or pct > 100 PreconditionError: percentage must be 0-100
adjustOwnership(pct) Relacja nieaktywna PreconditionError: cannot adjust terminated ownership
promote(title, dept) Employment nieaktywne PreconditionError: cannot promote in terminated employment
Utworzenie z tymi samymi Party fromParty == toParty InvariantError: self-relationships not allowed

Kierunkowość

Relacje mogą być:

Kierunkowość Przykład Nawigacja
Skierowana Employment Person → Organization (asymetryczna)
Nieskierowana Marriage Person ↔ Person (symetryczna)
Hierarchiczna ParentChild Rodzic → Dziecko (asymetryczna)

W przypadku relacji nieskierowanych zapytanie od dowolnego Party powinno zwrócić relację.

Schemat SQL

sql-- Base relationship table
CREATE TABLE party_relationship (
    id                  UUID PRIMARY KEY,
    relationship_type   VARCHAR(30) NOT NULL,
    from_party_id       UUID NOT NULL REFERENCES party(id),
    to_party_id         UUID NOT NULL REFERENCES party(id),
    valid_from          DATE NOT NULL,
    valid_to            DATE,
    notes               TEXT,
    created_at          TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at          TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    
    CONSTRAINT chk_different_parties CHECK (from_party_id != to_party_id),
    CONSTRAINT chk_valid_dates CHECK (valid_to IS NULL OR valid_to >= valid_from)
);

-- Employment-specific
CREATE TABLE employment (
    id              UUID PRIMARY KEY REFERENCES party_relationship(id) ON DELETE CASCADE,
    job_title       VARCHAR(100) NOT NULL,
    department      VARCHAR(100),
    employment_type VARCHAR(20) NOT NULL,  -- 'FullTime', 'PartTime', 'Contract'
    work_location   VARCHAR(100),
    reports_to_id   UUID REFERENCES employment(id)
);

-- Ownership-specific
CREATE TABLE ownership (
    id                      UUID PRIMARY KEY REFERENCES party_relationship(id) ON DELETE CASCADE,
    ownership_percentage    DECIMAL(5,2) NOT NULL,
    voting_rights           BOOLEAN NOT NULL DEFAULT TRUE,
    share_class             VARCHAR(10),
    
    CONSTRAINT chk_percentage CHECK (ownership_percentage >= 0 AND ownership_percentage <= 100)
);

-- Customer relationship-specific
CREATE TABLE customer_relationship (
    id                  UUID PRIMARY KEY REFERENCES party_relationship(id) ON DELETE CASCADE,
    account_manager_id  UUID REFERENCES party_role(id),  -- Employee role
    contract_value_cents INTEGER,
    contract_currency   CHAR(3),
    tier                VARCHAR(20),  -- 'Strategic', 'Key', 'Standard'
    nda_signed          BOOLEAN DEFAULT FALSE
);

-- Indexes for navigation
CREATE INDEX idx_relationship_from ON party_relationship(from_party_id);
CREATE INDEX idx_relationship_to ON party_relationship(to_party_id);
CREATE INDEX idx_relationship_type ON party_relationship(relationship_type);
CREATE INDEX idx_relationship_active ON party_relationship(from_party_id, to_party_id) 
    WHERE valid_to IS NULL;

Typowe zapytania

sql-- Find all employees of an organization
SELECT p.*, e.*
FROM party p
JOIN party_relationship pr ON pr.from_party_id = p.id
JOIN employment e ON e.id = pr.id
WHERE pr.to_party_id = :organizationId
  AND pr.relationship_type = 'Employment'
  AND pr.valid_to IS NULL;

-- Find organization hierarchy (ownership tree)
WITH RECURSIVE ownership_tree AS (
    -- Base: direct ownerships
    SELECT o.id, pr.from_party_id as owner_id, pr.to_party_id as owned_id,
           o.ownership_percentage, 1 as level
    FROM ownership o
    JOIN party_relationship pr ON pr.id = o.id
    WHERE pr.from_party_id = :rootOwnerId
      AND pr.valid_to IS NULL
    
    UNION ALL
    
    -- Recursive: owned companies' ownerships
    SELECT o.id, pr.from_party_id, pr.to_party_id,
           o.ownership_percentage, ot.level + 1
    FROM ownership o
    JOIN party_relationship pr ON pr.id = o.id
    JOIN ownership_tree ot ON pr.from_party_id = ot.owned_id
    WHERE pr.valid_to IS NULL
      AND ot.level < 10  -- Prevent infinite recursion
)
SELECT * FROM ownership_tree;

-- Find relationship history between two parties
SELECT pr.*, pr.relationship_type, pr.valid_from, pr.valid_to
FROM party_relationship pr
WHERE (pr.from_party_id = :party1Id AND pr.to_party_id = :party2Id)
   OR (pr.from_party_id = :party2Id AND pr.to_party_id = :party1Id)
ORDER BY pr.valid_from DESC;

Zagadnienia projektowe

Relacja a rola

Koncepcja Zastosowanie Przykład
Rola Zdolność/funkcja Party w systemie Customer może składać Order
Relacja Połączenie między dwoma Party Firma zatrudnia Person

Czasami potrzebne jest jedno i drugie: „Person ma rolę Employee" ORAZ „Party Relationship Employment między Person a Organization"

Nawigacja dwukierunkowa

Rozważ, czy potrzebujesz efektywnej nawigacji w obu kierunkach:

// From Person: "Where do I work?"
person.getEmployments()

// From Organization: "Who works here?"
organization.getEmployees()

Zaindeksuj zarówno from_party_id, jak i to_party_id, jeśli zapytania dwukierunkowe są częste.

Relacje historyczne

Przechowywanie zakończonych relacji (z ustawionym validTo) umożliwia:

  • Historię Employment
  • Ścieżki audytu
  • „Kto był właścicielem tej firmy w 2019 roku?"
  • Party — Encje będące w relacji
  • Party Role — Alternatywa dla zdolności Party (Customer, Supplier)
  • Accountability — Bardziej złożone wzorce relacji z regułami