Archetypy Programowania

Product Archetype (Archetyp produktu)

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

Problem

Product w katalogu mają złożone struktury:

  • Kategorie ze wspólnymi atrybutami: Wszystkie laptopy mają RAM i pamięć masową; wszystkie koszulki mają rozmiar i kolor
  • Katalog a fizyczne przedmioty: „iPhone 15 Pro 256GB Czarny" (katalog) vs „konkretny telefon z numerem seryjnym #XYZ" (egzemplarz)
  • Warianty: Ten sam produkt w różnych rozmiarach/kolorach
  • Złożoność cenowa: Cena bazowa, rabaty, ceny specyficzne dla klienta
  • Hierarchiczne kategorie: Elektronika → Komputery → Laptopy → Laptopy gamingowe

Koncepcja

Archetyp Product ma trzy warstwy:

  1. ProductType — Kategoria/szablon definiujący strukturę (atrybuty, reguły)
  2. Product — Pozycja katalogowa, którą można zamówić (SKU, cena, opis)
  3. ProductInstance — Fizyczny/serializowany przedmiot w magazynie

Można to sobie wyobrazić jako:

  • ProductType = „Laptop" (definiuje jakie atrybuty istnieją)
  • Product = „ThinkPad X1 Carbon Gen 11" (to, co zamawiasz)
  • ProductInstance = „Nr seryjny #ABC123" (to, co otrzymujesz)

Struktura

parent

0..1

0..1

0..1

1

1

1

0..*

0..*

0..*

0..*

0..*

0..*

ProductType

+id: UUID

+code: String

+name: String

+parent: ProductType

+children: ProductType[]

+attributeDefinitions: AttributeDef[]

+isLeaf() : : Boolean

Product

+id: UUID

+sku: String

+name: String

+description: String

+productType: ProductType

+basePrice: Money

+attributes: Map

+status: ProductStatus

+isAvailable() : : Boolean

ProductInstance

+id: UUID

+product: Product

+serialNumber: String

+batchNumber: String

+manufacturedDate: Date

+expiryDate: Date

+status: InstanceStatus

AttributeDefinition

+code: String

+name: String

+dataType: DataType

+required: Boolean

+options: String[]

Atrybuty

ProductType

Atrybut Typ Wymagane Opis
id UUID Tak Unikalny identyfikator
code String Tak Unikalny kod (LAPTOP, TSHIRT)
name String Tak Nazwa wyświetlana
description String Nie Opis kategorii
parent ProductType Nie Kategoria nadrzędna (null = korzeń)
attributeDefinitions AttributeDef[] Nie Atrybuty dla tego typu
active Boolean Tak Czy kategoria jest aktywna?

Product

Atrybut Typ Wymagane Opis
id UUID Tak Unikalny identyfikator
sku String Tak Stock Keeping Unit (unikalny)
name String Tak Nazwa produktu
description String Nie Szczegółowy opis
productType ProductType Tak Kategoria, do której należy
basePrice Money Tak Bazowa cena sprzedaży
costPrice Money Nie Cena kosztu/zakupu
attributes Map Nie Atrybuty specyficzne dla typu
status ProductStatus Tak Draft, Active, Discontinued
weight Decimal Nie Waga w kg
dimensions Dimensions Nie D × S × W w cm

ProductInstance

Atrybut Typ Wymagane Opis
id UUID Tak Unikalny identyfikator
product Product Tak Którego produktu dotyczy
serialNumber String Nie Unikalny nr seryjny (dla serializowanych)
batchNumber String Nie Numer partii/serii
manufacturedDate Date Nie Data produkcji
expiryDate Date Nie Data ważności (towary łatwo psujące się)
status InstanceStatus Tak Available, Sold, Damaged, itd.

AttributeDefinition

Atrybut Typ Wymagane Opis
code String Tak Kod atrybutu (RAM, SIZE)
name String Tak Nazwa wyświetlana
dataType Enum Tak String, Integer, Decimal, Boolean, Date, Enum
required Boolean Tak Czy musi być ustawiony na produkcie?
options String[] Nie Dozwolone wartości (dla typu Enum)
unit String Nie Jednostka miary (GB, cm)
minValue Decimal Nie Minimalna dozwolona wartość
maxValue Decimal Nie Maksymalna dozwolona wartość

Zachowania

Entity ProductType:
    
    function isLeaf(): Boolean
        return children is empty
    
    function isRoot(): Boolean
        return parent is None
    
    function getPath(): String[]
        // Returns ["Electronics", "Computers", "Laptops"]
        if parent is None:
            return [name]
        return parent.getPath() + [name]
    
    function getAllAttributeDefinitions(): AttributeDefinition[]
        // Inherit from ancestors + own
        inherited = parent?.getAllAttributeDefinitions() or []
        return inherited + attributeDefinitions
    
    function canHaveProducts(): Boolean
        // Only leaf types can have products directly
        return isLeaf()
    
    function getDescendants(): ProductType[]
        result = children.copy()
        for child in children:
            result += child.getDescendants()
        return result


Entity Product:
    
    function isAvailable(): Boolean
        return status == ProductStatus.Active
    
    function getAttribute(code: String): Any
        return attributes.get(code)
    
    function setAttribute(code: String, value: Any):
        definition = productType.getAllAttributeDefinitions()
            .find(d => d.code == code)
        
        require definition is not None  // Attribute must be defined
        require definition.validate(value)  // Value must be valid
        
        attributes[code] = value
    
    function validate(): ValidationResult
        errors = []
        
        for definition in productType.getAllAttributeDefinitions():
            value = attributes.get(definition.code)
            
            if definition.required and value is None:
                errors.add("Missing required attribute: " + definition.code)
            
            if value is not None and not definition.validate(value):
                errors.add("Invalid value for " + definition.code)
        
        return ValidationResult(isValid: errors.isEmpty(), errors: errors)
    
    function calculatePrice(customer: Customer): Money
        price = basePrice
        
        // Apply customer-specific pricing
        if customer.loyaltyTier == "Gold":
            price = price * 0.95  // 5% discount
        
        // Apply quantity discounts, promotions, etc.
        return price
    
    function discontinue():
        require status == ProductStatus.Active
        status = ProductStatus.Discontinued
        emit ProductDiscontinued(this)


Entity ProductInstance:
    
    function isAvailable(): Boolean
        return status == InstanceStatus.Available
    
    function isExpired(): Boolean
        return expiryDate is not None and expiryDate < today()
    
    function sell(order: Order):
        require isAvailable()
        require not isExpired()
        
        status = InstanceStatus.Sold
        soldTo = order
        soldAt = now()
        
        emit InstanceSold(this, order)
    
    function markDamaged(reason: String):
        status = InstanceStatus.Damaged
        damageReason = reason
        damagedAt = now()


Entity AttributeDefinition:
    
    function validate(value: Any): Boolean
        if value is None:
            return not required
        
        // Type check
        match dataType:
            case DataType.String:
                if not (value is String): return false
            case DataType.Integer:
                if not (value is Integer): return false
            case DataType.Decimal:
                if not (value is Number): return false
            case DataType.Boolean:
                if not (value is Boolean): return false
            case DataType.Enum:
                if value not in options: return false
        
        // Range check
        if minValue is not None and value < minValue:
            return false
        if maxValue is not None and value > maxValue:
            return false
        
        return true

Przykłady

Konfiguracja katalogu produktów

// ProductType hierarchy: Electronics > Laptops
electronics = new ProductType(code: "ELECTRONICS", name: "Electronics")
laptops = new ProductType(code: "LAPTOP", name: "Laptops", parent: electronics)

// Define attributes for Laptops
laptops.attributeDefinitions.add(new AttributeDefinition(
    code: "RAM_GB", name: "RAM", dataType: DataType.Integer,
    required: true, minValue: 4, maxValue: 128, unit: "GB"
))
laptops.attributeDefinitions.add(new AttributeDefinition(
    code: "STORAGE_GB", name: "Storage", dataType: DataType.Integer,
    required: true, unit: "GB"
))
laptops.attributeDefinitions.add(new AttributeDefinition(
    code: "SCREEN_SIZE", name: "Screen Size", dataType: DataType.Decimal,
    required: true, unit: "inches"
))

Tworzenie Product i Product Instance

// Product (catalog item)
thinkpad = new Product(
    sku: "LEN-X1C-G11-16-512",
    name: "ThinkPad X1 Carbon Gen 11 (16GB/512GB)",
    productType: laptops,
    basePrice: Money(649900, "PLN"),  // 6499.00 PLN
    status: ProductStatus.Draft
)
thinkpad.setAttribute("RAM_GB", 16)
thinkpad.setAttribute("STORAGE_GB", 512)
thinkpad.setAttribute("SCREEN_SIZE", 14.0)

thinkpad.validate()  // isValid: true
thinkpad.publish()   // status = Active

// ProductInstance (specific unit in warehouse)
unit1 = new ProductInstance(
    product: thinkpad,
    serialNumber: "PF4ABC123",
    manufacturedDate: 2025-01-15,
    status: InstanceStatus.Available
)

Maszyny stanów

Status Product

create

publish()

discontinue()

delete()

Draft

Active

Discontinued

Deleted

Status Product Instance

receive into inventory

reserve for order

release reservation

ship to customer

mark damaged

expiry date passed

customer return

restock

return inspection failed

Available

Reserved

Sold

Damaged

Expired

Returned

Niezmienniki

  1. Unikalny SKU — Żadne dwa Product nie mogą mieć tego samego SKU
  2. Unikalny numer seryjny — Żadne dwa Product Instance nie mogą mieć tego samego numeru seryjnego
  3. Acykliczna hierarchia typów — ProductType nie może być swoim własnym przodkiem
  4. Product na typach liściowych — Product mogą należeć tylko do liściowych ProductType
  5. Wymagane atrybuty ustawione — Aktywne Product muszą mieć ustawione wszystkie wymagane atrybuty
  6. Poprawne wartości atrybutów — Wartości atrybutów muszą odpowiadać ich definicjom

Obsługa błędów

Operacja Naruszony warunek wstępny Błąd
setAttribute(code, value) Atrybut niezdefiniowany dla typu PreconditionError: unknown attribute 'FOO' for type Laptop
setAttribute(code, value) Wartość nie przechodzi walidacji PreconditionError: RAM_GB must be between 4 and 128
discontinue() Status nie jest Active PreconditionError: can only discontinue active products
ProductInstance.sell(order) Niedostępny PreconditionError: instance is not available (status: Sold)
ProductInstance.sell(order) Przeterminowany PreconditionError: instance has expired
validate() Brakujące wymagane atrybuty Zwraca ValidationResult(isValid: false, errors: [...]) (bez wyjątku)

Schemat SQL

sql-- Product type hierarchy
CREATE TABLE product_type (
    id              UUID PRIMARY KEY,
    code            VARCHAR(50) NOT NULL UNIQUE,
    name            VARCHAR(100) NOT NULL,
    description     TEXT,
    parent_id       UUID REFERENCES product_type(id),
    active          BOOLEAN NOT NULL DEFAULT TRUE,
    created_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Attribute definitions for product types
CREATE TABLE attribute_definition (
    id              UUID PRIMARY KEY,
    product_type_id UUID NOT NULL REFERENCES product_type(id) ON DELETE CASCADE,
    code            VARCHAR(50) NOT NULL,
    name            VARCHAR(100) NOT NULL,
    data_type       VARCHAR(20) NOT NULL,  -- 'String', 'Integer', 'Decimal', 'Boolean', 'Enum'
    required        BOOLEAN NOT NULL DEFAULT FALSE,
    options         TEXT,  -- JSON array for Enum type: '["S", "M", "L", "XL"]'
    unit            VARCHAR(20),
    min_value       DECIMAL,
    max_value       DECIMAL,
    sort_order      INTEGER NOT NULL DEFAULT 0,
    
    UNIQUE(product_type_id, code)
);

-- Products (catalog items)
CREATE TABLE product (
    id              UUID PRIMARY KEY,
    sku             VARCHAR(50) NOT NULL UNIQUE,
    name            VARCHAR(255) NOT NULL,
    description     TEXT,
    product_type_id UUID NOT NULL REFERENCES product_type(id),
    base_price_cents INTEGER NOT NULL,
    base_price_currency CHAR(3) NOT NULL DEFAULT 'PLN',
    cost_price_cents INTEGER,
    cost_price_currency CHAR(3),
    status          VARCHAR(20) NOT NULL DEFAULT 'Draft',
    weight_kg       DECIMAL(10,3),
    length_cm       DECIMAL(10,2),
    width_cm        DECIMAL(10,2),
    height_cm       DECIMAL(10,2),
    created_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    updated_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    
    CONSTRAINT chk_product_status CHECK (status IN ('Draft', 'Active', 'Discontinued', 'Deleted'))
);

-- Product attribute values (EAV pattern)
CREATE TABLE product_attribute (
    product_id      UUID NOT NULL REFERENCES product(id) ON DELETE CASCADE,
    attribute_code  VARCHAR(50) NOT NULL,
    value_string    VARCHAR(255),
    value_integer   INTEGER,
    value_decimal   DECIMAL(15,4),
    value_boolean   BOOLEAN,
    value_date      DATE,
    
    PRIMARY KEY (product_id, attribute_code)
);

-- Product instances (serialized items)
CREATE TABLE product_instance (
    id              UUID PRIMARY KEY,
    product_id      UUID NOT NULL REFERENCES product(id),
    serial_number   VARCHAR(100) UNIQUE,
    batch_number    VARCHAR(50),
    manufactured_date DATE,
    expiry_date     DATE,
    status          VARCHAR(20) NOT NULL DEFAULT 'Available',
    created_at      TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
    
    CONSTRAINT chk_instance_status CHECK (
        status IN ('Available', 'Reserved', 'Sold', 'Damaged', 'Expired', 'Returned')
    )
);

-- Indexes
CREATE INDEX idx_product_type ON product(product_type_id);
CREATE INDEX idx_product_status ON product(status);
CREATE INDEX idx_product_type_parent ON product_type(parent_id);
CREATE INDEX idx_instance_product ON product_instance(product_id);
CREATE INDEX idx_instance_status ON product_instance(status);
CREATE INDEX idx_instance_expiry ON product_instance(expiry_date) WHERE expiry_date IS NOT NULL;

Typowe zapytania

sql-- Get product with all attributes
SELECT 
    p.*,
    json_object_agg(pa.attribute_code, 
        COALESCE(pa.value_string, pa.value_integer::text, 
                 pa.value_decimal::text, pa.value_boolean::text, 
                 pa.value_date::text)
    ) as attributes
FROM product p
LEFT JOIN product_attribute pa ON pa.product_id = p.id
WHERE p.id = :productId
GROUP BY p.id;

-- Get products by type (including subtypes)
WITH RECURSIVE type_tree AS (
    SELECT id FROM product_type WHERE id = :typeId
    UNION ALL
    SELECT pt.id FROM product_type pt
    JOIN type_tree tt ON pt.parent_id = tt.id
)
SELECT p.* FROM product p
WHERE p.product_type_id IN (SELECT id FROM type_tree)
  AND p.status = 'Active';

-- Get type hierarchy path
WITH RECURSIVE type_path AS (
    SELECT id, name, parent_id, 1 as level
    FROM product_type WHERE id = :typeId
    UNION ALL
    SELECT pt.id, pt.name, pt.parent_id, tp.level + 1
    FROM product_type pt
    JOIN type_path tp ON pt.id = tp.parent_id
)
SELECT name FROM type_path ORDER BY level DESC;

-- Find available instances with earliest expiry (FEFO)
SELECT pi.* FROM product_instance pi
WHERE pi.product_id = :productId
  AND pi.status = 'Available'
  AND (pi.expiry_date IS NULL OR pi.expiry_date > CURRENT_DATE)
ORDER BY pi.expiry_date NULLS LAST
LIMIT :quantity;

Zagadnienia projektowe

EAV vs JSON vs szerokie tabele

Podejście Zalety Wady
EAV Elastyczne, rzadkie atrybuty Złożone zapytania, brak bezpieczeństwa typów
Kolumna JSON Elastyczne, jedna kolumna Ograniczone indeksowanie, walidacja
Szerokie tabele Szybkie, typowane, indeksowalne Zmiany schematu, wiele NULL-i

Zalecenie:

  • EAV dla naprawdę dynamicznych atrybutów
  • JSON dla danych semi-strukturalnych
  • Dedykowane kolumny dla często wyszukiwanych/filtrowanych atrybutów

Kiedy stosować Product Instance

Scenariusz Użyć Product Instance? Dlaczego
Serializowana elektronika Tak Śledzenie poszczególnych sztuk
Towary łatwo psujące się Tak Śledzenie dat ważności
Towary masowe (śruby) Nie Wystarczy śledzić ilość
Product cyfrowe Nie Brak fizycznego egzemplarza
Produkcja na Order Tak Śledzenie poszczególnych sztuk

Strategie cenowe

Archetyp Product obsługuje różne modele cenowe:

  • Cena bazowa — W encji Product
  • Ceny według poziomu klienta — Obliczane w czasie rzeczywistym
  • Progi ilościowe — Osobna encja PriceBreak
  • Promocje czasowe — Encja Promotion z datami
  • Order — Product są zamawiane
  • Inventory — Product Instance śledzony w Inventory
  • Party Role (Supplier) — Kto dostarcza Product