Slide 1

Slide 1 text

© 2021 Thoughtworks Accelerate development with simple design Matteo Vaccari ShareIT online meetup, 2027/10/2021

Slide 2

Slide 2 text

© 2021 Thoughtworks 2

Slide 3

Slide 3 text

© 2021 Thoughtworks 3 So…… what works?

Slide 4

Slide 4 text

© 2021 Thoughtworks 4

Slide 5

Slide 5 text

© 2021 Thoughtworks 5 🤔

Slide 6

Slide 6 text

© 2021 Thoughtworks Kent Beck on Simple Design 6 https://martinfowler.com/bliki/BeckDesignRules.html

Slide 7

Slide 7 text

© 2021 Thoughtworks 7 Kent Beck on “good code” There are a few things I look for: ● Once and only once [...] ● Lots of little pieces [...] ● Replacing objects [... In a really good system, every time the user says “I want to do this radically new thing”, the developer says, “Oh, I’ll have to make a new kind of X and plug it in.” When you can extend a system solely by adding new objects without modifying any existing objects, then you have a system that is flexible and cheap to maintain ● Replacing objects [...] ● Moving objects [...] Kent Beck, Smalltalk Best Practice Patterns, 1997

Slide 8

Slide 8 text

© 2021 Thoughtworks The problem 8

Slide 9

Slide 9 text

© 2021 Thoughtworks @Test void addItemToCart_returnOk_addGiftItem_withNoFraudAlert() { Item previousItem = new Item("a tv"); Item addedItem = new Item("a book"); Item giftItem = new Item("gift", ItemFlag.GIFT); Cart cart = new Cart().withItem(previousItem); AddItemToCartCommand command = new AddItemToCartCommand(cart, addedItem); when(inventoryService.checkAvailability(addedItem)).thenReturn(AVAILABLE); when(fraudDetectionService.looksSuspicious(cart)).thenReturn(false); when(giftItemService.getTodaysGiftItem()).thenReturn(giftItem); AddItemToCartResult addItemToCartResult = cartService.addItem(command); assertThat(addItemToCartResult).isEqualTo(AddItemToCartResult.ok()); ArgumentCaptor cartArgumentCaptor = ArgumentCaptor.forClass(Cart.class); verify(cartRepository).update(cartArgumentCaptor.capture()); assertThat(cartArgumentCaptor.capture()).isEqualTo(cart .withItem(previousItem) .withItem(addedItem) .withItem(giftItem) ); verify(alertService, never()).signal(any()); } 9 In a codebase near you…

Slide 10

Slide 10 text

© 2021 Thoughtworks public CartService(CartRepository cartRepository, InventoryService inventoryService, FraudDetectionService fraudDetectionService, AlertService alertService, GiftItemService giftItemService) { this.cartRepository = cartRepository; this.inventoryService = inventoryService; this.fraudDetectionService = fraudDetectionService; this.alertService = alertService; this.giftItemService = giftItemService; } public AddItemToCartResult addItem(AddItemToCartCommand command) { if (inventoryService.checkAvailability(command.getItem()) == NOT_AVAILABLE) { // 󰍹 return AddItemToCartResult.itemNotAvailable(); } Cart cart = command.getCart(); cart.addItem(command.getItem()); // 󰍽 Item todaysGiftItem = giftItemService.getTodaysGiftItem(); // 󰍼 if (!cart.containsGiftItem()) { cart.addItem(todaysGiftItem); } if (fraudDetectionService.looksSuspicious(cart)) { // 󰍶 alertService.signal(new SuspiciousCartAlert(cart)); } cartRepository.update(cart); // 󰍵 return AddItemToCartResult.ok(); } 10 And the culprit is…

Slide 11

Slide 11 text

© 2021 Thoughtworks Searching for a solution 11

Slide 12

Slide 12 text

© 2021 Thoughtworks 12 Favor object composition over inheritance – Gamma, Helm, Johnson, Vlissides, 1995 All modules should be open for extension, and closed for modi ication – Bertrand Meyer (more or less), 1988 Be able to extend a system solely by adding new objects, without modifying any existing objects – Kent Beck, 1998 Composition is the Essence of Programming – Bartosz Milewski, 2014 Objects should be composable – David West, 2004

Slide 13

Slide 13 text

© 2021 Thoughtworks 13

Slide 14

Slide 14 text

© 2021 Thoughtworks 14 New features should be coded in new code files, with no modification to existing code files – Paraphrasing Kent Beck

Slide 15

Slide 15 text

© 2021 Thoughtworks 15 The goal of the Open-Closed Principle The aim of the OCP is to organize software around backplanes, that allow extension simply by developing and connecting a new module, just like hardware backplanes or cartridges.

Slide 16

Slide 16 text

© 2021 Thoughtworks 16 New features should be coded in new code files, with no modification to existing code files Consequences: ● If a feature must be removed, it can be removed by simply removing its code files; ● If a feature must be changed, it can be changed by changing only its code files. ● All the code that implements a given story/feature/business rule can be found in specific files, dedicated to implement just that story/feature/business rule

Slide 17

Slide 17 text

© 2021 Thoughtworks My life with the Open-Closed Principle 17 2010 blog about the OCP kata 2010 XP Days Benelux 2012 London Code Dojo 2013 XP Manchester 2014 Github repository created 2014 XP2014 in Rome … 2021 why is this kind of code still prevalent? http://matteo.vaccari.name/blog/archives/293

Slide 18

Slide 18 text

© 2021 Thoughtworks Unless we are fluent, we will not be able to do this at work! 18 18 © 2021 Thoughtworks

Slide 19

Slide 19 text

© 2021 Thoughtworks 19 Most kata are solved in a single object public class BowlingGame { private int rolls[] = new int[21]; private int currentRoll = 0; public void roll(int pins) { rolls[currentRoll++] = pins; } public int score() { int score = 0; int frameIndex = 0; for (int frame = 0; frame < 10; frame++) { if (isStrike(frameIndex)) { score += 10 + strikeBonus(frameIndex); frameIndex++; } else if (isSpare(frameIndex)) { score += 10 + spareBonus(frameIndex); frameIndex += 2; } else { score += sumOfBallsInFrame(frameIndex); frameIndex += 2; } } return score; } private boolean isStrike(int frameIndex) { return rolls[frameIndex] == 10; } public class FizzBuzzGame { public String say(int number) { if (number % 3 == 0 && number % 5 == 0) { return "FizzBuzz"; } if (number % 3 == 0) { return "Fizz"; } if (number % 5 == 0) { return "Buzz"; } return String.valueOf(number); } } The classic Bowling Game kata, in the version of Robert Martin Typical solution to the Fizz Buzz kata http://butunclebob.com/ArticleS.UncleBob.TheBowlingGameKata

Slide 20

Slide 20 text

© 2021 Thoughtworks What about upfront design? 20 Robert Martin starts his discussion of the Bowling Kata with what would be his thinking before TDD Then he discards the upfront design and proceeds to a totally different solution in a single class public class BowlingGame { private int rolls[] = new int[21]; private int currentRoll = 0; public void roll(int pins) { rolls[currentRoll++] = pins; } public int score() { int score = 0; int frameIndex = 0; for (int frame = 0; frame < 10; frame++) { if (isStrike(frameIndex)) { score += 10 + strikeBonus(frameIndex); frameIndex++; } else if (isSpare(frameIndex)) { score += 10 + spareBonus(frameIndex); frameIndex += 2; } else { score += sumOfBallsInFrame(frameIndex); frameIndex += 2; } } return score; } This is an OK way to do TDD. The result is fine if no new requirements arive

Slide 21

Slide 21 text

© 2021 Thoughtworks An OK solution if no new requirements arrive… 21 😂😂😂 https://www.slideshare.net/xpmatteo/20101125-ocpxpday

Slide 22

Slide 22 text

© 2021 Thoughtworks 22 If you try to implement all these… …You end up with IF spaghetti

Slide 23

Slide 23 text

© 2021 Thoughtworks 23 It seemed so simple… This design is inflexible. It is at the limit of what we can grasp intuitively You add one more IF and it becomes very hard to read Perhaps it’s not public class BowlingGame { private int rolls[] = new int[21]; public int score() { int score = 0; int frameIndex = 0; for (int frame = 0; frame < 10; frame++) { if (isStrike(frameIndex)) { score += 10 + strikeBonus(frameIndex); frameIndex++; } else if (isSpare(frameIndex)) { score += 10 + spareBonus(frameIndex); frameIndex += 2; } else { score += sumOfBallsInFrame(frameIndex); frameIndex += 2; } } return score; } }

Slide 24

Slide 24 text

© 2021 Thoughtworks 24 The original design might have been useful public class BowlingGame { private List frames; public int score() { int score = 0; for (Frame frame : frames) { score += frame.score(); } return score; } } Simple polymorphism takes care of most planetary Bowling requirements 😎

Slide 25

Slide 25 text

© 2021 Thoughtworks public CartService(CartRepository cartRepository, InventoryService inventoryService, FraudDetectionService fraudDetectionService, AlertService alertService, GiftItemService giftItemService) { this.cartRepository = cartRepository; this.inventoryService = inventoryService; this.fraudDetectionService = fraudDetectionService; this.alertService = alertService; this.giftItemService = giftItemService; } public AddItemToCartResult addItem(AddItemToCartCommand command) { if (inventoryService.checkAvailability(command.getItem()) == NOT_AVAILABLE) { return AddItemToCartResult.itemNotAvailable(); } Cart cart = command.getCart(); cart.addItem(command.getItem()); Item todaysGiftItem = giftItemService.getTodaysGiftItem(); if (!cart.containsGiftItem()) { cart.addItem(todaysGiftItem); } if (fraudDetectionService.looksSuspicious(cart)) { alertService.signal(new SuspiciousCartAlert(cart)); } cartRepository.update(cart); return AddItemToCartResult.ok(); } Developers don’t train nearly enough, and when they do, they train to build monolithic, inflexible solutions 25 25 © 2021 Thoughtworks

Slide 26

Slide 26 text

© 2021 Thoughtworks Becoming fluent at program composition 26

Slide 27

Slide 27 text

© 2021 Thoughtworks An exercise to train programming by composition 27 1. Take any standard kata solution 2. Add a new test for a new requirement 3. Can you make it pass by only adding new classes, without changing any existing code except construction code? 4. Yes → you rock! go back to 2 5. No → disable the new test and refactor until you can

Slide 28

Slide 28 text

© 2021 Thoughtworks Example: The FizzBuzz Game 28 Example: 1, 2, Fizz!, 4, Buzz!, Fizz!, 7, 8, Fizz!, Buzz!, 11, Fizz!, 13, 14, FizzBuzz!, 16, 17, Fizz!... Rules: If the number is a multiple of 3, say “Fizz” If it is a multiple of 5, say “Buzz” If it is a multiple of 3 and 5, say “FizzBuzz” Otherwise, just say the number. public class FizzBuzzGameTest { private FizzBuzzGame game; @BeforeEach public void setUp() { game = new FizzBuzzGame(); } @Test public void justSayTheNumber() { assertEquals("1", game.say(1)); assertEquals("2", game.say(2)); } @Test public void multiplesOfThree() { assertEquals("Fizz", game.say(3)); assertEquals("Fizz", game.say(6)); } @Test public void multiplesOfFive() { assertEquals("Buzz", game.say(5)); assertEquals("Buzz", game.say(10)); } @Test public void multiplesOfFiveAndThree() { assertEquals("FizzBuzz", game.say(15)); assertEquals("FizzBuzz", game.say(30)); }

Slide 29

Slide 29 text

© 2021 Thoughtworks 1. Take a standard solution 29 public class FizzBuzzGameTest { private FizzBuzzGame game; @BeforeEach public void setUp() { game = new FizzBuzzGame(); } @Test public void justSayTheNumber() { assertEquals("1", game.say(1)); assertEquals("2", game.say(2)); } @Test public void multiplesOfThree() { assertEquals("Fizz", game.say(3)); assertEquals("Fizz", game.say(6)); } @Test public void multiplesOfFive() { assertEquals("Buzz", game.say(5)); assertEquals("Buzz", game.say(10)); } @Test public void multiplesOfFiveAndThree() { assertEquals("FizzBuzz", game.say(15)); assertEquals("FizzBuzz", game.say(30)); } public class FizzBuzzGame { public String say(int number) { if (isMultipleOf(3, number) && isMultipleOf(5, number)) { return "FizzBuzz"; } if (isMultipleOf(3, number)) { return "Fizz"; } if (isMultipleOf(5, number)) { return "Buzz"; } return String.valueOf(number); } private boolean isMultipleOf(int divisor, int number) { return number % divisor == 0; } }

Slide 30

Slide 30 text

© 2021 Thoughtworks 2. A new requirement! 30 If it is a multiple of 7, say “Bang” @Test public void multiplesOfSeven() { assertEquals("Bang", game.say(7)); assertEquals("Bang", game.say(14)); } public class FizzBuzzGame { public String say(int number) { if (isMultipleOf(3, number) && isMultipleOf(5, number)) { return "FizzBuzz"; } if (isMultipleOf(3, number)) { return "Fizz"; } if (isMultipleOf(5, number)) { return "Buzz"; } return String.valueOf(number); } private boolean isMultipleOf(int divisor, int number) { return number % divisor == 0; } } Can we implementing by adding new classes, with no modification to existing classes?

Slide 31

Slide 31 text

© 2021 Thoughtworks 5. I don’t see a way! 31 So: Disable the new test and refactor until you can public class FizzBuzzGame { public String say(int number) { if (isMultipleOf(3, number) && isMultipleOf(5, number)) { return "FizzBuzz"; } if (isMultipleOf(3, number)) { return "Fizz"; } if (isMultipleOf(5, number)) { return "Buzz"; } return String.valueOf(number); } private boolean isMultipleOf(int divisor, int number) { return number % divisor == 0; } } public class FizzBuzzGame { public String say(int number) { String response = ""; if (isMultipleOf(3, number)) { response += "Fizz"; } if (isMultipleOf(5, number)) { response += "Buzz"; } if (response.isEmpty()) { return String.valueOf(number); } return response; } private boolean isMultipleOf(int divisor, int number) { return number % divisor == 0; } }

Slide 32

Slide 32 text

© 2021 Thoughtworks Keep going… 32 public class FizzBuzzGame { public String say(int number) { String response = ""; if (isMultipleOf(3, number)) { response += "Fizz"; } if (isMultipleOf(5, number)) { response += "Buzz"; } if (response.isEmpty()) { return String.valueOf(number); } return response; } private boolean isMultipleOf(int divisor, int number) { return number % divisor == 0; } } public String say(int number) { String response = ""; response += sayFizz(number); response += sayBuzz(number); if (response.isEmpty()) { return String.valueOf(number); } return response; } private String sayBuzz(int number) { if (isMultipleOf(5, number)) { return "Buzz"; } return ""; } private String sayFizz(int number) { if (isMultipleOf(3, number)) { return "Fizz"; } return ""; }

Slide 33

Slide 33 text

© 2021 Thoughtworks Keep going… 33 public String say(int number) { String response = ""; response += sayFizz(number); response += sayBuzz(number); if (response.isEmpty()) { return String.valueOf(number); } return response; } private String sayBuzz(int number) { if (isMultipleOf(5, number)) { return "Buzz"; } return ""; } private String sayFizz(int number) { if (isMultipleOf(3, number)) { return "Fizz"; } return ""; } public String say(int number) { String response = ""; response += sayWord(number, 3, "Fizz"); response += sayWord(number, 5, "Buzz"); if (response.isEmpty()) { return String.valueOf(number); } return response; } private String sayWord(int number, int divisor, String word) { if (isMultipleOf(divisor, number)) { return word; } return ""; }

Slide 34

Slide 34 text

© 2021 Thoughtworks Almost there… 34 public String say(int number) { String response = ""; response += sayWord(number, 3, "Fizz"); response += sayWord(number, 5, "Buzz"); if (response.isEmpty()) { return String.valueOf(number); } return response; } private String sayWord(int number, int divisor, String word) { if (isMultipleOf(divisor, number)) { return word; } return ""; } public class FizzBuzzGame { public String say(int number) { String response = ""; response += new WordRule(3, "Fizz").say(number); response += new WordRule(5, "Buzz").say(number); if (response.isEmpty()) { return String.valueOf(number); } return response; } } public class WordRule { private final int divisor; private final String word; public WordRule(int divisor, String word) { this.divisor = divisor; this.word = word; } public String say(int number) { if (isMultipleOf(divisor, number)) { return word; } return ""; } private boolean isMultipleOf(int divisor, int number) { return number % divisor == 0; } }

Slide 35

Slide 35 text

© 2021 Thoughtworks Refactoring done 35 public class FizzBuzzGame { private List rules; public FizzBuzzGame(List rules) { this.rules = rules; } public String say(int number) { String response = ""; for (WordRule rule: rules) { response += rule.say(number); } if (response.isEmpty()) { return String.valueOf(number); } return response; } } public class FizzBuzzGameTest { private FizzBuzzGame game; @BeforeEach public void setUp() { game = new FizzBuzzGame(List.of( new WordRule(3, "Fizz"), new WordRule(5, "Buzz") )); } public String say(int number) { String response = ""; response += sayWord(number, 3, "Fizz"); response += sayWord(number, 5, "Buzz"); if (response.isEmpty()) { return String.valueOf(number); } return response; } private String sayWord(int number, int divisor, String word) { if (isMultipleOf(divisor, number)) { return word; } return ""; }

Slide 36

Slide 36 text

© 2021 Thoughtworks There! 36 public class FizzBuzzGameTest { private FizzBuzzGame game; @BeforeEach public void setUp() { game = new FizzBuzzGame(List.of( new WordRule(3, "Fizz"), new WordRule(5, "Buzz") )); } public class FizzBuzzGameTest { private FizzBuzzGame game; @BeforeEach public void setUp() { game = new FizzBuzzGame(List.of( new WordRule(3, "Fizz"), new WordRule(5, "Buzz"), new WordRule(7, "Bang") )); }

Slide 37

Slide 37 text

© 2021 Thoughtworks Applying the OCP at work 37

Slide 38

Slide 38 text

© 2021 Thoughtworks public CartService(CartRepository cartRepository, InventoryService inventoryService, FraudDetectionService fraudDetectionService, AlertService alertService, GiftItemService giftItemService) { this.cartRepository = cartRepository; this.inventoryService = inventoryService; this.fraudDetectionService = fraudDetectionService; this.alertService = alertService; this.giftItemService = giftItemService; } public AddItemToCartResult addItem(AddItemToCartCommand command) { if (inventoryService.checkAvailability(command.getItem()) == NOT_AVAILABLE) { // 󰍹 return AddItemToCartResult.itemNotAvailable(); } Cart cart = command.getCart(); cart.addItem(command.getItem()); // 󰍽 Item todaysGiftItem = giftItemService.getTodaysGiftItem(); // 󰍼 if (!cart.containsGiftItem()) { cart.addItem(todaysGiftItem); } if (fraudDetectionService.looksSuspicious(cart)) { // 󰍶 alertService.signal(new SuspiciousCartAlert(cart)); } cartRepository.update(cart); // 󰍵 return AddItemToCartResult.ok(); } 38 How to fix this?! 🤔🤔 🤔

Slide 39

Slide 39 text

© 2021 Thoughtworks 39 Sources of ideas:

Slide 40

Slide 40 text

© 2021 Thoughtworks Sketching at a whiteboard… 40 Check inventory Add bought item Add gift item Fraud check Persist changes An OO perspective: these could all be decorators An FP perspective: these could all be “functions”

Slide 41

Slide 41 text

© 2021 Thoughtworks Let’s try with functions It’s the zeitgeist… 41 We can model a service as a function from command to result. In math notation: addItemToCart: AddItemToCartCommand → AddItemToCartResponse In Java: Function addItemToCart; Functions can be composed: (f ○ g)(x) = f(g(x)) In Java: Our services will be composed of many small functions f.andThen(g)

Slide 42

Slide 42 text

© 2021 Thoughtworks 42 @Configuration public class AddItemToCartServiceConfig { @Autowired private InventoryService inventoryService; @Autowired private GiftItemService giftItemService; @Autowired private CartRepository cartRepository; @Autowired private FraudDetectionService fraudDetectionService; @Autowired private AlertService alertService; @Bean public Function addItemToCartService() { Function checkInventory = new CheckInventory(inventoryService); Function addItemToCart = new AddItemToCart(); Function addGiftItem = new AddGiftItem(giftItemService); Function fraudCheck = new FraudCheck(fraudDetectionService, alertService); Function persistChanges = new PersistChanges(cartRepository); return checkInventory .andThen(addItemToCart) .andThen(addGiftItem) .andThen(fraudCheck) .andThen(persistChanges) ; } }

Slide 43

Slide 43 text

© 2021 Thoughtworks 43 Function addItemToCart = new AddItemToCart(); @Test void addItem_ok() { Item previousItem = new Item("a tv"); Item addedItem = new Item("a book"); Cart cart = new Cart().withItem(previousItem); AddItemToCartCommand command = new AddItemToCartCommand(cart, addedItem); AddItemToCartResult addItemToCartResult = addItemToCart.apply(command); assertThat(cart.getItems()).containsExactly(previousItem, addedItem); assertThat(addItemToCartResult).isEqualTo(AddItemToCartResult.ok()); }

Slide 44

Slide 44 text

© 2021 Thoughtworks Unless we are fluent, we will not be able to do this at work! 44 44 © 2021 Thoughtworks

Slide 45

Slide 45 text

© 2021 Thoughtworks ● We should refactor our code towards composition ● We will never be able to apply this in practice unless we achieve fluency ● A good way to achieve fluency is by doing and repeating exercises 45

Slide 46

Slide 46 text

© 2021 Thoughtworks Start getting the OCP under your fingers today 46 ● Choose a kata from the Kata-log: https://kata-log.rocks/ ● Solve it ● Invent a new requirement ● Apply the OCP kata rules ● Do it again; ● try different ways to make your code composable

Slide 47

Slide 47 text

© 2021 Thoughtworks Resources 47 ● The OCP kata repository https://github.com/xpmatteo/ocp-kata ● How to organize a Coding Dojo https://leanpub.com/codingdojohandbook ● Code Retreats are awesome, look for one near you https://twitter.com/coderetreat ● A well-organized repository of examples: https://kata-log.rocks/ ● Freeman & Pryce are very good at evolving a composable set of objects to express a complex domain: https://www.infoq.com/presentations/design-principles-code-structures/

Slide 48

Slide 48 text

© 2021 Thoughtworks Thanks for listening Matteo Vaccari Grumpy Developer [email protected] 48