$30 off During Our Annual Pro Sale. View Details »

Accelerate development with simple design - ShareIT

Accelerate development with simple design - ShareIT

One thing you sometimes hear is “SOLID principles are useless; all you need is to just write simple code!” Ha! It’s much easier said than done. In this talk I’d like to share what “simple code” means to me and a few tricks to get it. I do not present any novel idea… it’s well-known stuff, but seldom applied in practice.

Matteo Vaccari

October 28, 2021
Tweet

More Decks by Matteo Vaccari

Other Decks in Technology

Transcript

  1. © 2021 Thoughtworks Accelerate development with simple design Matteo Vaccari

    ShareIT online meetup, 2027/10/2021
  2. © 2021 Thoughtworks 2

  3. © 2021 Thoughtworks 3 So…… what works?

  4. © 2021 Thoughtworks 4

  5. © 2021 Thoughtworks 5 🤔

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

  7. © 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
  8. © 2021 Thoughtworks The problem 8

  9. © 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<Cart> 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…
  10. © 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…
  11. © 2021 Thoughtworks Searching for a solution 11

  12. © 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
  13. © 2021 Thoughtworks 13

  14. © 2021 Thoughtworks 14 New features should be coded in

    new code files, with no modification to existing code files – Paraphrasing Kent Beck
  15. © 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.
  16. © 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
  17. © 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
  18. © 2021 Thoughtworks Unless we are fluent, we will not

    be able to do this at work! 18 18 © 2021 Thoughtworks
  19. © 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
  20. © 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
  21. © 2021 Thoughtworks An OK solution if no new requirements

    arrive… 21 😂😂😂 https://www.slideshare.net/xpmatteo/20101125-ocpxpday
  22. © 2021 Thoughtworks 22 If you try to implement all

    these… …You end up with IF spaghetti
  23. © 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; } }
  24. © 2021 Thoughtworks 24 The original design might have been

    useful public class BowlingGame { private List<Frame> 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 😎
  25. © 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
  26. © 2021 Thoughtworks Becoming fluent at program composition 26

  27. © 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
  28. © 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)); }
  29. © 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; } }
  30. © 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?
  31. © 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; } }
  32. © 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 ""; }
  33. © 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 ""; }
  34. © 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; } }
  35. © 2021 Thoughtworks Refactoring done 35 public class FizzBuzzGame {

    private List<WordRule> rules; public FizzBuzzGame(List<WordRule> 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 ""; }
  36. © 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") )); }
  37. © 2021 Thoughtworks Applying the OCP at work 37

  38. © 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?! 🤔🤔 🤔
  39. © 2021 Thoughtworks 39 Sources of ideas:

  40. © 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”
  41. © 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<AddItemToCartCommand, AddItemToCartResponse> 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)
  42. © 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<AddItemToCartCommand, AddItemToCartResult> addItemToCartService() { Function<AddItemToCartCommand, AddItemToCartCommand> checkInventory = new CheckInventory(inventoryService); Function<AddItemToCartCommand, AddItemToCartResult> addItemToCart = new AddItemToCart(); Function<AddItemToCartResult, AddItemToCartResult> addGiftItem = new AddGiftItem(giftItemService); Function<AddItemToCartResult, AddItemToCartResult> fraudCheck = new FraudCheck(fraudDetectionService, alertService); Function<AddItemToCartResult, AddItemToCartResult> persistChanges = new PersistChanges(cartRepository); return checkInventory .andThen(addItemToCart) .andThen(addGiftItem) .andThen(fraudCheck) .andThen(persistChanges) ; } }
  43. © 2021 Thoughtworks 43 Function<AddItemToCartCommand, AddItemToCartResult> 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()); }
  44. © 2021 Thoughtworks Unless we are fluent, we will not

    be able to do this at work! 44 44 © 2021 Thoughtworks
  45. © 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
  46. © 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
  47. © 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/
  48. © 2021 Thoughtworks Thanks for listening Matteo Vaccari Grumpy Developer

    mvaccari@thoughtworks.com 48