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
DDD a Legacy
By Marcin Markowski
DDD a Legacy
- 708