Laracon US 2018 Live Blog
📢 That's it! Thank you for following along with me, and see you next year!
Colin DeCarlo, Design Patterns with Laravel
- Colin works for Vehikl
- Check out the Torqd podcast (podcast.vehikl.com)
- Starting with a story
- Year is 2008. Buttons are ful of gel, everything has a drop shadow, Taylor had hair
- Friend has imposter syndrome. He muscles up all his courage and asks a senior developer "How do I be a good a good developer?" "Read the Design Patterns book, it's by the Gang of Four". Friend frantically tries to find the gang of four. Book is literally named "Design Patterns: Elements of Reusable Object-Oriented Software"
- Harder to understand
- Adapter: Convert the interface of a class into another interface clients expect...not...very clear
- Strategy Pattern..again, huh?
- Friend realizes that was the problem wasn't him, it wasn't even with the book either. It was a combination of the two.
- Friend is a web developer, but the book doesn't relate to web developers.
- Inaccessible to him.
- We'll see a few patterns
Agenda
- Adapter Pattern
- Strategy Pattern
- Factory Pattern
- Identify Factories hiding in code, Extract Class refactor, tage advantage of auto-wiring service container
Adapter Pattern
- Primer
- Two components with incompatible interfaces
- Converts messages passed between the components into something each can understand
- Scenario
- News aggregator site wants to provide "local news" on the sidebar, use geolocation based on IP address
- Sounds doable, right? We can create a LocalNewsController, pull in the location from a vendor library, pass that into our object scope and take that..but now we're tied to that vendor library.
- Instead, let's create a Mark object that takes in the location country_name, region_name, city. Now we depend on something that we created
- Hey, it works! Problem is, ops is sad because the geolocation service is sad. They fixed it by buying an IP database. Now we need to put that into our codebase
- So we change the codebase, put it in for PR...but in code review, we needed to change the controller. Instead, let's define an interface LOcator with one public function (fromIp that returns Mark)
- So let's create an IpLocationLocator that implements Locator, take the code we originally had and essentially just drop it in
- Now let's create an IPDatabaseLocator, same thing
- Now that we have these two adapters, we need to tell/teach Laravel which one adapter to use. So let's bind a Locator singleton in our App Service Provider
- Check the config. if API, use the location locator, if database, use the database locator
- Now we no longer need to set it up, we can pass in a Locator to the controller method, and that code can stay the same.
- Still a bit of a problem...with tests. It could be reaching out over the network, slowing things down and making the test suite even more fragile.
- But with the adapter pattern, we can create a fake locator to the interface returning a set-in-stone Mark
- Now tests will run quickly, in isolation
- Benefits
- Inverts dependency, can swap out implementations, easier to test
Strategy Pattern
- Primer
- Goal to accomplish a single task, but multiple ways to accomplish the task dependant on certain criteria.
- Strategy classes adhere to an agreed interface to handle which one to use
- Scenario
- Personal finance app that imports transactions from a bank and support multiple import formats
- So first, create a command that loads a ledger and parses it, iterates over it to categorize and save each transaction.
- The ledger reader has a parse method that creates a file object, says it's a CSV file and reads the transactions, parse each line as a record
- getType determines if it's a debit or credit.
- Worked really well, but really interested in year-over-year
- But in a different year, credit/debit positions are swapped and in cents, not as a decimal!
- Maybe in parse, we pass in a format. If it's raw, separate with tabs, or if csv, separate with commas.
- If it's raw, parse it as a raw record, else parse it as a csv record...but yikes. Maybe we need to do a JSON one next year!
- Where we fork based on how to parse the record, let's new up a ParseRawStrategy/ParseCsvStrategy, move them to the top...but in parse, we're making that same branch.
- "Make the change easy, then make the easy change" - Kent Beck
- Make the change easy by stop assuming the record delimiter, extract the algorithm to a strategy , use different strategies based on file format and fail if it's an unsupported format.
- Then make the easy change by creating a strategy for CSV records and supporting the CSV format
- How to teach the LedgerReader how to choose a strategy...in the LedgerReader, let's determine the parser in the constructor, simplifying the readTransactions call
- In makeParser, if the format isn't raw, throw an unsupported exception
- Now let's make the easy change by creating the strategy for CSV
- Now in makeParser, change it to a switch case to determine the format
- Now we need to change our load command by
- Benefits
- Simplifies containing classes by removing conditional logic, allows you to defer decisions until runtime, makes the classes using the Strategies "pluggible"
Factory
- Primer
- Only responsible for creating objects of a specific type, encapsulate the creation process
- Usage
- Back to the Locator Factory, let's not switch on the config, but let's change the method name to make, passing in the type
- Same for parser, ParserFactory, then makeParser
- Why can't the parser be given directly to the command?
- We can remove it from the factory, but then it has to go to the command, and complicate the command handler function
- Now let's try doing a LedgerReaderFactory...
- If the factory lives on the outside, the container already knows how to construct it. Now we're simplifying the load command again.
- Benefits
- Moves creation logic out of dependant classes, simple and composable, lean on the Service Container to construct and inject.