Dlaczego DDD skutecznie zapobiega chorobie legacy ?

@technites_pl

@technites_pl

Dlaczego odziedziczone

to złe ?

@technites_pl

@technites_pl

Legacy to choroba całej organizacji

@technites_pl

Antidotum ?

WSPÓŁPRACA

DDD

Marcin Markowski

@technites_pl

http://itlibrium.com

Szymon Janikowski

@szjanikowski

http://github.com/itlibrium

@itlibrium

marcin.markowski@itlibrium.com

szymon.janikowski@itlibrium.com

@technites_pl

Osoby dramatu

Stefan

Maciek

@technites_pl

Agile ?

Produkt

ŹLE !

Biznes

IT

Wymagania

Estymaty

Spójność

Możliwości

Wiedza

Wizja

Realizacja

Review

@technites_pl

Agile !

Produkt

Biznes

IT

Środki i Ograniczenia

Spójność i Możliwości

Wiedza, Wizja

Realizacja

Review

@technites_pl

Kompleksowa ochrona przeciw
chorobie legacy !

WSPÓŁPRACA

DDD

@technites_pl

Cool Domain

@technites_pl

Diagram kontekstu

SYSTEM

@technites_pl

Podstawowy proces

x

=

@technites_pl

Gwarancje

x

=

0

G

@technites_pl

Umowy serwisowe

x

=

?

U

@technites_pl

Czy z tym DDD
to nie przerost
formy nad treścią ?

@technites_pl

A może by jednak proceduralnie ?

Przecież to wygląda
bardzo prosto ...

@technites_pl

public class ServiceService {
    […]

    public void finish(int serviceId, Collection<Integer> sparePartIds)
            throws BusinessException {
        Service service = serviceRepository.Get(serviceId);
        if (service.getStatus() != ServiceStatus.Scheduled) {
            throw new BusinessException("Nieprawidłowy status usługi");
        }

        List<SparePart> spareParts = new ArrayList<>();
        for (int sparePartId : sparePartIds) {
            spareParts.add(sparePartRepository.Get(sparePartId));
        }
        service.setSpareParts(spareParts);

        BigDecimal sparePartsCost = BigDecimal.valueOf(0);
        for (SparePart sparePart : service.getSpareParts()) {
            sparePartsCost = sparePartsCost.add(sparePart.Price);
        }

        PricingCategory pricingCategory =
                service.getClient().getEquipmentModel().getPricingCategory();
        
        BigDecimal labourCost = pricingCategory.getPricePerHour()
                .multiply(BigDecimal.valueOf(service.getDuration()));
        
        service.setPrice(pricingCategory.getMinPrice().max(labourCost)
                .add(sparePartsCost));

        service.setStatus(ServiceStatus.Done);
        serviceRepository.Save(service);
    }
}

@technites_pl

public void finish(int serviceId, Collection<Integer> sparePartIds) 
    throws BusinessException {
    Service service = serviceRepository.Get(serviceId);
    if (service.getStatus() != ServiceStatus.Scheduled)
        throw new BusinessException("Nieprawidłowy status usługi");

    List<SparePart> spareParts = new ArrayList<>();
    for (int sparePartId : sparePartIds)
    {
        spareParts.add(sparePartRepository.Get(sparePartId));
    }
    service.setSpareParts(spareParts);

    BigDecimal price = BigDecimal.ZERO;
    if (service.isWarranty())
    {
        price = BigDecimal.valueOf(0);
    }
    else
    {
        EquipmentModel equipmentModel = service.getClient().getEquipmentModel();
        PricingCategory pricingCategory = equipmentModel.getPricingCategory();
        Contract contract = service.getClient().getContract();

        BigDecimal sparePartsCost = BigDecimal.valueOf(0);
        for (SparePart sparePart : service.getSpareParts())
        {
            sparePartsCost = sparePartsCost.add(sparePart.Price);
        }

        if (contract == null)
        {
            price = pricingCategory.getMinPrice().max(pricingCategory.getPricePerHour()
                    .multiply(BigDecimal.valueOf(service.getDuration()))).add(sparePartsCost);
        }
        else
        {
            BigDecimal sparePartsCostLimit = contract.getSparePartsCostLimit()
                    .subtract(contract.getSparePartsCostLimitUsed());
            if (sparePartsCostLimit.compareTo(sparePartsCost) >= 0)
            {
                contract.setSparePartsCostLimitUsed(contract.getSparePartsCostLimitUsed()
                    .subtract(sparePartsCost));
                sparePartsCost = BigDecimal.ZERO;
            }
            else
            {
                contract.setSparePartsCostLimitUsed(BigDecimal.ZERO);
                sparePartsCost = sparePartsCost.subtract(sparePartsCostLimit);
            }
            price = price.add(sparePartsCost);

            if (contract.getFreeInterventionsUsed() < contract.getFreeInterventions())
            {
                contract.setFreeInterventionsUsed(contract.getFreeInterventionsUsed() + 1);

                int freeInterventionTimeLimit = equipmentModel.getFreeInterventionTimeLimit();
                if (service.getDuration() > freeInterventionTimeLimit)
                {
                    price = price.add(pricingCategory.getPricePerHour()
                            .multiply(BigDecimal.valueOf(service.getDuration() - freeInterventionTimeLimit)));
                }
            }
            else
            {
                price = price.add(pricingCategory.getMinPrice().max(pricingCategory.getPricePerHour()
                        .multiply(BigDecimal.valueOf(service.getDuration()))));
            }

            serviceContractRepository.Save(contract);
        }
    }

    service.setPrice(price);
    service.setStatus(ServiceStatus.Done);
    serviceRepository.Save(service);
}

@technites_pl

Wygląda znajomo ?

@technites_pl

Czym w zasadzie jest to DDD ?

@technites_pl

Software development is a learning process, working code is a side effect.

- Alberto Brandolini

@technites_pl

Bounded Contexts

@technites_pl

@technites_pl

Building Blocki

App Service

Domain Service

Modeluje Use Case'a

Koordynuje obiekty domenowe
Wyznacza transakcje biznesowe

Modeluje fragmentu procesu
Koordynuje obiekty domenowe
Nie posiada własnego stanu

@technites_pl

Building Blocki

Aggregate

Value Object

Posiada globalną tożsamość
Chroni reguły biznesowe
Udostępnia zachowania
Enkapsuluje dane i algorytmy

Tożsamość wynika z właściwości
Jest niezmienny

Udostępnia proste zachowania
Służy jako nośnik informacji

@technites_pl

Building Blocki

Policy

Factory

Dostraja zachowania i procesy
Nie posiada własnego stanu
Otwiera model na rozbudowę

Tworzy agregaty, polityki, etc.
Ma dostęp do szerokiego kontekstu
Enkapsuluje reguły kompozycji

@technites_pl

Building Blocki

to role obiektów

w systemie

@technites_pl

Gdzie jest logika jak nie ma jej w Building Blockach ?

@technites_pl

Do dzieła !

@technites_pl

Podstawowy proces

x

=

@technites_pl

Spotkanie z managementem

Z wyceną interwencji

będziemy sporo

eksperymentować

Chcemy wejść w nowe

modele współpracy

@technites_pl

Spotkanie z ekspertem

Jak wyceniamy interwencje ?

Wyceniamy
czynności serwisowe !

Przeglądy i naprawy

Na podstawie robocizny i kosztu części

Cena interwencji to suma cen czynności

@technites_pl

Spotkanie z ekspertem

Interwencja może
być zakończona jeżeli wszystkie czynności serwisowe są wycenione

Można to zrobić
tylko raz !

@technites_pl

Zakończenie interwencji

Interwencja

Czynność

Robocizna

<App Service>

<Aggregate>

<Value Object>

Koszt części

<Policy>

Ustalanie sposobu wyceny

<Factory>

Zakończ przy danych

czynnościach i sposobie wyceny

Utwórz sposób wyceny dla interwencji

@technites_pl

Od czego zaczynamy ?

def "Labour cost calculated correctly"() {
    given:
        _minPrice = minPrice;
        _pricePerHour = pph;
        _sparePartsPrices = [
            158 : Money.fromDouble(30), 
            333 : Money.fromDouble(40)];
        _duration = duration;
        _usedParts = usedPartsIds;
        _actionType = actionType;
    when:
        serviceIsFinished();
    then:
        getInterventionPrice() == Money.fromDouble(price);
    where:
        minPrice | pph | duration | usedPartsIds | actionType     ||   price
           200   | 100 |  4       |  []          | Review         ||     400
           200   | 100 |  1       |  [158, 333]  | Repair         ||     270
           200   | 100 |  1       |  []          | Review         ||     200
           200   | 100 |  0       |  [333]       | Repair         ||     240
           300   | 100 |  0       |  []          | Review         ||     300
           400   | 0   |  10      |  [158]       | Repair         ||     430

@technites_pl

Serwis aplikacyjny

public class FinishInterventionHandler {
    
    [...]

    public void finish(FinishInterventionCommand command) 
        throws BusinessException {
        Intervention intervention = this.repository
            .get(command.getInterventionId());

        PricePolicy pricePolicy = this.pricePolicyFactory
            .createFor(intervention);
        intervention.finish(command.getServiceActions(), pricePolicy);

        repository.save(intervention);
    }
}

@technites_pl

Agregat

public class Intervention {
    
    [...]

    public void finish(Collection<ServiceAction> serviceActions, PricePolicy pricePolicy)
            throws BusinessException {
        
        if (this.serviceActions != null)
            throw new BusinessException("Nie można zakończyć interwencji więcej niż raz");

        this.serviceActions = new ArrayList<>(serviceActions);
        price =  this.serviceActions.stream()
            .map(pricePolicy::apply)
            .reduce(Money.ZERO, Money::sum);
    }
}

@technites_pl

Polityki

public class PricePolicies {
    public static PricePolicy labour(Money pricePerHour, Money minPrice) {
        return serviceAction ->
            Money.max(
                Money.multiply(pricePerHour, serviceAction.getDuration().getHours()),
                minPrice);
    }

    public static PricePolicy sparePartsCost(Map<Integer, Money> sparePartPrices) {
        return serviceAction ->
            serviceAction.getSparePartIds().stream()
                .map(sparePartPrices::get)
                .reduce(Money.ZERO, Money::sum);
    }

    [...]
}

@technites_pl

Fabryka polityk

public class PricePolicyFactory implements IPricePolicyFactory {
    [...]

    public PricePolicy createFor(Intervention intervention) {
        [...]

        return PricePolicies.sum(
            PricePolicies.labour(
                Money.fromDecimal(pricingCategory.getPricePerHour()),
                Money.fromDecimal(pricingCategory.getMinPrice())),
            PricePolicies.sparePartsCost(sparePartPrices));
    }
}

@technites_pl

A co jak nie zdążymy z implementacją
w jednym sprincie ?

@technites_pl

Gwarancje

x

=

0

G

@technites_pl

Spotkanie z ekspertem

Dodajemy 2 nowe

Czynności Serwisowe:

Przegląd Gwarancyjny i

Naprawę Gwarancyjną

Klient za nie nie płaci !

@technites_pl

Zakończenie interwencji

Interwencja

Czynność

Robocizna

<App Service>

<Aggregate>

<Value Object>

Koszt części

<Policy>

Ustalanie sposobu wyceny

<Factory>

Zakończ przy danych

czynnościach i sposobie wyceny

Utwórz sposób wyceny dla interwencji

0 PLN

@technites_pl

Fabryka polityk

public class PricePolicyFactory implements IPricePolicyFactory {
    [...]

    public PricePolicy CreateFor(Intervention intervention) {
       [...]

        return PricePolicies.sum(
            PricePolicies.when(
                PricePolicyFactory::isRegularAction,
                PricePolicies.sum(
                    PricePolicies.labour(
                        pricingCategory.getPricePerHour(),
                        pricingCategory.getMinPrice(),
                    PricePolicies.sparePartsCost(sparePartPrices))),
                PricePolicies.when(
                    PricePolicyFactory::isWarrantyAction,
                    PricePolicies.free()));
    }
}

@technites_pl

To było sztuczne

i zbyt proste ?

@technites_pl

public void finish(int serviceId, Collection<Integer> sparePartIds) 
    throws BusinessException {
    Service service = serviceRepository.Get(serviceId);
    if (service.getStatus() != ServiceStatus.Scheduled)
        throw new BusinessException("Nieprawidłowy status usługi");

    List<SparePart> spareParts = new ArrayList<>();
    for (int sparePartId : sparePartIds)
    {
        spareParts.add(sparePartRepository.Get(sparePartId));
    }
    service.setSpareParts(spareParts);

    BigDecimal price = BigDecimal.ZERO;
    if (service.isWarranty())
    {
        price = BigDecimal.valueOf(0);
    }
    else
    {
        EquipmentModel equipmentModel = service.getClient().getEquipmentModel();
        PricingCategory pricingCategory = equipmentModel.getPricingCategory();
        Contract contract = service.getClient().getContract();

        BigDecimal sparePartsCost = BigDecimal.valueOf(0);
        for (SparePart sparePart : service.getSpareParts())
        {
            sparePartsCost = sparePartsCost.add(sparePart.Price);
        }

        if (contract == null)
        {
            price = pricingCategory.getMinPrice().max(pricingCategory.getPricePerHour()
                    .multiply(BigDecimal.valueOf(service.getDuration()))).add(sparePartsCost);
        }
        else
        {
            BigDecimal sparePartsCostLimit = contract.getSparePartsCostLimit()
                    .subtract(contract.getSparePartsCostLimitUsed());
            if (sparePartsCostLimit.compareTo(sparePartsCost) >= 0)
            {
                contract.setSparePartsCostLimitUsed(contract.getSparePartsCostLimitUsed()
                    .subtract(sparePartsCost));
                sparePartsCost = BigDecimal.ZERO;
            }
            else
            {
                contract.setSparePartsCostLimitUsed(BigDecimal.ZERO);
                sparePartsCost = sparePartsCost.subtract(sparePartsCostLimit);
            }
            price = price.add(sparePartsCost);

            if (contract.getFreeInterventionsUsed() < contract.getFreeInterventions())
            {
                contract.setFreeInterventionsUsed(contract.getFreeInterventionsUsed() + 1);

                int freeInterventionTimeLimit = equipmentModel.getFreeInterventionTimeLimit();
                if (service.getDuration() > freeInterventionTimeLimit)
                {
                    price = price.add(pricingCategory.getPricePerHour()
                            .multiply(BigDecimal.valueOf(service.getDuration() - freeInterventionTimeLimit)));
                }
            }
            else
            {
                price = price.add(pricingCategory.getMinPrice().max(pricingCategory.getPricePerHour()
                        .multiply(BigDecimal.valueOf(service.getDuration()))));
            }

            serviceContractRepository.Save(contract);
        }
    }

    service.setPrice(price);
    service.setStatus(ServiceStatus.Done);
    serviceRepository.Save(service);
}

@technites_pl

Wygląda znajomo ?

@technites_pl

Umowy serwisowe

x

=

?

U

@technites_pl

Spotkanie z ekspertem

Klient z umową nie płaci za robociznę od czynności serwisowych wykonanych w ramach puli darmowych interwencji

@technites_pl

Spotkanie z ekspertem

Klient z umową nie płaci za części zamienne do określonego limitu

@technites_pl

Interwencja

Umowa serwisowa

Wpływa na wycenę czynności serwisowych

Zakończenie wykorzystuje limity

Czynność

Zupełnie nowa koncepcja biznesowa

@technites_pl

Może zmodyfikować umowę serwisową w polityce ?

@technites_pl

Co to znaczy, że jakiś kawałek kodu gdzieś pasuje ?

@technites_pl

Zakończenie interwencji

<App Service>

Ustalanie sposobu wyceny

<Factory>

Zakończ przy danych

czynnościach i wycenie

Utwórz

sposób wyceny

dla interwencji

Wycena interwencji

<Domain Service>

Umowa serwisowa

<Aggregate>

Pobierz
limity

Uaktualnij
limity

Wyceń interwencję przy danych czynnościach i limitach

Interwencja

Czynność

<Aggregate>

<Value Object>

@technites_pl

Co to znaczy być

otwartym na rozbudowę ?

@technites_pl

Skoro tyle trzeba przerobić to

po co to wszystko ?

@technites_pl

Wygląda znajomo ?

@technites_pl

Przełom

To nie jest łatwe,

nawet w dobrym zespole

@technites_pl

Kompleksowa ochrona przeciw
chorobie legacy !

WSPÓŁPRACA

DDD

@technites_pl

Czy zawsze trzeba robić DDD ?

A czy da się go nie robić ?

@technites_pl

Bez zrozumienia biznesu nie zbudujemy zaufania !

@technites_pl

I odtąd żyli długo i szczęśliwie ...

... tylko czy na pewno ?

@technites_pl

Dlaczego DDD skutecznie zapobiega chorobie legacy ?

@technites_pl

DDD daje nam narzędzia do eliminacji niedopasowań pomiędzy kodem a biznesem

@technites_pl

Kompleksowa ochrona przeciw
chorobie legacy !

WSPÓŁPRACA

DDD

Marcin Markowski

@technites_pl

http://itlibrium.com

Szymon Janikowski

@szjanikowski

http://github.com/itlibrium

@itlibrium

marcin.markowski@itlibrium.com

szymon.janikowski@itlibrium.com