 Hi, I'm Isaac. Today, I'm going to show you how to create a platform so that you and your team can write code that is more maintainable and extensible. Since last year at Clark, we've been working on reducing our tick debt. Our main application is a monorepo with 190 packages accrued over seven or eight years. We've been on Ember 328 for almost two years now, and trying to get the four hasn't been easy. These issues may sound familiar to you, and I think it's a good thing to know that we don't have to face them alone. The Ember Community Survey in 2022, estimated that three out of four work projects are stuck on version three or below, and a follow-up survey in December indicated the lack of resource and outdated add-ons as main reasons. The question is, why are we finding ourselves in projects that are hard to maintain and extend? What can you do differently? I want to highlight possible solutions and give practical tips based on what my colleagues and I achieved over the last year. Keep in mind that the problems that I'll describe, they're not specific to work or Ember version three, but more programming in general. And the reason is, I want you to be able to apply these solutions in other contexts, in open source projects like adopted Ember add-ons, or when it's time to update Ember from four to five. So, let's get started. When it's time to update a project that hasn't been maintained, you're going to feel at first overwhelmed, hopeless. How in the world do we get out of this mess? The key to doing so, and this is the most important lesson from my talk, do it in increments. Imagine a yarn that is wildly tangled up. The yarn is supposed to be just one simple thread, but instead it's a chaos, and you can't tell where it starts and where it ends. Now, if you try to untangle the yarn by applying force in every direction, all at once, you're going to fail and make an even bigger mess. But, if you get small little tucks, just one at a time, sooner or later, you'll find yourself that one simple thread. This incremental approach is also what mathematicians favor. Global algorithms like finding the inverse of a function work well in theory, but they're very limited in practice. Iterative methods, on the other hand, taking a step in one direction, a step in another, and so on, until we converge to the right solution, these methods are powerful because we can apply them to many different situations. They're also efficient and will end up saving us time. Now, what does an incremental approach mean for us developers? I think it means two things. First, when it's time to update a project, we have to avoid coming up with a plan that spans weeks or months. We also have to avoid planning down to the smallest detail. Now, I'm not saying that have no plan and just wing it, but what I'm pointing out is that your code is alive. It'll never stay still because someone on your team will add a feature or some package that you depend on will have a release. The assumptions for how to update your project will change quickly, and you'll have to adapt quickly as well. Second, an incremental approach means allowing mistakes. The right solution won't be obvious from the start, especially because your project hasn't been maintained. A poor request to update a project may introduce bugs, but that's okay. We can fix them in the next one, so we move on. This may sound obvious, but if you take no steps because we fear of breaking the app, then we'll get nowhere. Only by taking small steps and iterating the solution can you make impossible possible. So the rest of my talk is devoted to showing which small steps we can take. But before I do that, I'll share my thoughts on code metrics. Should we measure the improvements that we make? In math, a metric is a number that we use to describe a phenomenon without bias. For example, you are 10 centimeters taller than I, or the train was late by five minutes. One day programmers inspired by math came up with numbers to describe code quality. How many lines of code are there? How many components and routes do we have? And so on. The problem is, again, your code is alive, and the assumptions for your project will change from one day to the next. It's important to ignore these changes that were outside of your control by normalizing the metric. Maybe divide the metric by the number of lines of code, or by the square root, or the logarithm, where we can think of. And you're starting to see that we're visiting numbers and adding personal bias when the metric is supposed to be objective. I claim that it's impossible to measure the change of a code metric. In other words, how much the code improved over time. We can only measure what is now at this moment. So long story short, don't worry about code metrics, just do the right thing and improve that code you wanted to. Next question is, well, what is the right thing? What does it look like? And the answer is going to depend on which aspects of code we care about. To help narrow the answer, we're going to focus on two aspects, maintainability and extensibility. A code that we can maintain and extend exhibits three characteristics. It has a minimum API, it separates concerns, and has few dependencies. In math, we say that these are necessary conditions. This means that if your code doesn't meet one of these conditions, it cannot be maintained or extended. Think of these as a checklist where each item tells you which small steps you can take. To make things more concrete, I'll use reusable components, something that we are familiar with and use on a daily basis. One, the right code has a minimum API. An API application programming interface draws a boundary between two code and rules for communicating with each other. When the interface is good, the code is easy to use, maintain and extend. And when the interface is bad, the code becomes a hazard and can stop your project at some point. A common mistake that I see in components is supporting too many cases. That is, the component ended up with a large API by allowing many arguments. This can happen when you try to predict the future and over design things, or when you try to quickly fix something with an if statement. If there's just a new argument, I'm going to do something else. What we tend to ignore is how every case increases complexity. Not linearly, but I claim that it's quadratic or higher because of the combinatorial effect, how arguments interact with one another. And we can assume that the higher the complexity, the more likely that the component is untested and unmaintained. Back in Germany, we have this amazing animal that's going to sustain the future, called Eilegen de Vormich-Zau. If you've been to the Oregon Zoo, you might have seen it. It can give us eggs, wool, milk, meat, and companionship. It can do everything that we want because it does not exist. No idea what the Oregon Zoo. It's an idiom like jack-of-all-trades, master of none. So the lesson is design simple things. Got it. But what if I already have a component with a large API? How do I simplify it? And the key to answering this question is researching your current use cases. Find out which features were almost always used and which maybe once or twice. The code for the rarely used features, see if you can delete them. By doing so, you can reduce complexity and the chance that your component is going to cause an issue. And also, by removing code, you can create a chain reaction of additional refactors. Remember, the key to untangling yarn is to give small little tucks. Now, what if there is a feature that was used once that you have to support it? Your business depends on it. I would say if I can create another component, maybe through composition. The idea is that you want to treat this feature as an exception and not the norm. When designing reusable components, I want you to target the 80% and not give in to the other 20. Two, the right code separates concerns. Recall that an API draws a boundary between two code. In the best case, each side trusts the other to perform only certain tasks and no others. And when this happens, the tests for each side are simpler. The question is, for reusable components, how should we separate responsibilities? Which tasks belong to the reusable component and which to the consumer, whether that's a component or route? In my experience, we can maintain the reusable component well if it takes care of these three aspects. Accessibility, so the consumer doesn't have to be an expert in it. Styling, making sure that things inside the container look right. And test selectors, what should be tested and how should the selectors be named. Meanwhile, the consumer must provide these three. Data and translations, so what should the component render. Margin and padding for the container so that the component plays nice with each other. And callback functions, when the user takes an action, what should the component do. How I separate is styling. Namely, the container doesn't set its margin and padding, but the consumer does. This becomes a surprise. I learned this from Sean Messon and Trek Klawaki a few years ago and can really attest that this helps us reuse and refactor components. The rationale is reusable components should only care about what happens inside. We encountered this idea also in container queries. Another thing to note here is data and translations. Before Ember 3.25, we would have had these a bunch of arguments to pass these down. But now, thanks to name blocks, the reusable component can focus on the layout while the consumer on the content. Name blocks are one of my future features of Ember. A feature of Ember that I don't like and want you to use with caution is splat attributes. Splat attributes can easily destroy separation of concerns. I'll give you two examples. So one time, I tried to replace flex with grid to simplify a reusable component. Only to find out many of the consumers have passed a class attribute and have overwritten the flex properties. Thanks to splat attributes, implementation got leaked and the consumers are now forcing me to keep using flex. Another issue that we encountered at observed at Clark is too many test selectors because many consumers pass their own. When the same DOM element is referred to in several different ways, refactoring the component becomes tedious. I still don't have to find and update multiple test files in different packages. In general, I think splat attributes are signed that the component wasn't designed right. Maybe the component should ask the consumer to instead use arguments to define the styles. Name blocks to customize content were test helpers to write tests. I want you to use the right solution, the right tools to solve the right problems. And number three, the right code depends on fear. Just like arguments, every package that we install increases a chance that something goes wrong. But unlike arguments, we don't really have control over packages. When the package author doesn't do releases or they make breaking changes, our code can become stuck in time. Now, I'm not saying write every code yourself and have zero dependencies, but ask yourself, is there a code more stable than mine? And to me, stable doesn't mean there's a 1.0 release. It means that the code is well-written, documented, tested, and supported. It can install the package only if the answer is yes. Now, of course, at times you will have to install a package that is not stable. If so, see if you can wrap the code that you need, then write the test to document the wrapper's input and output. So this way, if the package turns out to be no-go, you won't have to replace the code in one place, the wrapper. I'll tell you about a mistake that we made at Clark and how it's now affecting us with upgrading Ember. It's a funny story, and you're going to laugh because you're not affected. We used Ember file upload, and I noticed that we ran version 5.0 beta when the latest is 8-something, and I can't update Ember to 4 without updating Ember file upload first. It turns out we had come up with like 10 different ways to render file upload, and since the beta, the add-on changes API and styling drastically. So now we have to fix deprecations, visual regressions, and failing tests for every one of these cases. If only we had come up with the wrapper component, right? So to help people design code right, we need a strong foundation, things like simple lint and test strategies, up-to-date dependencies, and short build and rebuild times. Replacing the foundation is a comparative, large task, but it's also something that we'll have to do only once in a while. The question is, for an existing project, how do we replace a foundation without stopping everything else? And again, the answer is in increments. A year ago, at Clark, every package had different linter configurations, so we couldn't easily update packages like ESLint and TypeScript. Furthermore, how we asked our developers lint files was different from how we asked RCI. Fast forward to now, our project is set up like this. There are only three scripts for developers to remember because every package has them, lint, lint fix, and test. Furthermore, we asked the CI to run the same script so that we can reproduce issues locally easily. Second, we used the flag cache and the package concurrently so that we can lint files faster and more exhaustively. And finally, we have limited resource, so we rely on the default as much as possible. Things like Blueprints from Ember CLI and official plugins like TSconfig Ember. We adopt third-party plugins only if they are stable in the sense that I mentioned earlier. And here's how we changed linting across 190 packages. It turns out packages are not equal. Leave no packages, packages that don't depend on others, we're actually seldom worked on, so we could update all of these at once. Packages for a business domain which belong together, they were updated together. In a single day, we updated ESLint Diversion 8 and reset all configurations by bypassing CI. We asked lint.js to return code 0, which means unconditional success. And afterwards, again, in groups of packages, we reverted the scripts, ran autofix, and ignored the errors that couldn't be fixed. With this dividing-conquer strategy, it took me one person about 10 pull requests and no more than five days to introduce a change. Next, we look up our deprecations and outdated packages that can block us from updating more critical dependencies like Ember Source. To find deprecations, we can use Ember CLI deprecation workflow and create a to-do list. The add-on does require that we have enough tests to avoid false negatives, so later, I'll show you how you can write simple tests when there aren't any. A second approach is to run your application and use Ember inspectors deprecation tab. Here are three deprecations that I think will affect many projects. If you want to learn more how to fix them, you can visit deprecations.emberjs.com. I also gather a list of packages, important packages, and the minimum version that you want to reach. I think it's important to update Ember Auto Import and Ember Modifier now because more add-ons will move on to support embroider. And to help people iterate on the solution many times, we need our build and rebuilds to be fast. As a forewarning, high-performance optimization isn't my expertise. I cannot give you definitive answers. But nonetheless, I'll share what we did at Clark, which seemed to help lower the times. Maybe they can help me too. We ended up with 190 packages because of premature abstractions. By combining packages and removing get code, we're now down to 150. In the process, we remove a few cyclic dependencies by creating a leaf node package. And if you have a component that is used by many packages, see if you can simplify it. Maybe you can make it template-only or replace the older syntax with more modern ones. An ongoing project for us is to declare dependencies correctly. Because we use yarn to manage from the modern repo, many of the packages that have been created by copy paste listed wrong dependencies. To find unused dependencies, you can search code by looking at how they would have been used. And finally, if you have Ember CLI 3.15 or higher, there's a hidden feature that you can enable to make regals faster. And Ember CLI build JS simply set this flag. Broccoli enable memoize the true. One moment. So my colleagues and I, we are in this lucky situation where it can maintain code full-time. But that's still a bus-factor of one. It's important that I share knowledge and get more people involved. This year, my colleagues and I started to tackle tech depth together. Each quarter, we discussed ideas and decide what to work on. To show that every one of you has the power to make change, first, I'll cover five techniques for refactoring. These are accessible and can be used on a daily basis. Next are code mods, something that's more advanced and takes time to do. And finally, we'll think about our interpersonal skills and how they affect how we collaborate. There's this book on refactoring that I just hate. It gives us 70 techniques and the examples are academic, so I can never tell which are actually important. I claim that we just need five to survive. Write tests, rename things, make early exits, extract functions, and then remove dead code. If your project doesn't have tests, you can use Ember CLI to write the simplest test, a tautology, true is equal to true. For example, you render a component and write assert okay true, or you look up a service and assert that it is truthy. Even these simple placeholder tests give us two valuable information. One, the minimum data needed to initialize our object. And two, the guarantee that this object when used won't cause issues. You can learn more about how to write tests from a talk at EmberFest 2019. The lessons from back then still apply today. If there is a variable condition or function that you don't understand, there's a good chance that other people won't either. When we understand the code better, give a name that describes the thing better. And three, messy conditions, in general, intentions to the right are a recipe for disaster. They encourage us to keep nesting to handle new exceptions. You can fix this problem by making early exits. If there is code that can happen only when the condition is true, instead, you exit immediately when false, using return, break, or continue. Early exits help us simplify logic and move code to the left. And four, at times you'll find a function that has many lines of code, but is actually performing a few key steps in sequence. When this is the case, you create a function for each key step and give a name that describes the step. This process of breaking a large function in the smaller ones is called extraction. And when you extract the functions, you then have the option to move them to, for example, utilities and write unitess. And lastly, if you see code that isn't being used, deleted. By doing so, you can simplify assumptions, remove dependencies, and allow further refactors. To find dead code, you can use gitgrip or your code editor's fine tool. However, for searches to be accurate, your code needs to be written well, catch 22, and ideally statically analyzable. It's hard to match names that are dynamically generated. Now, when you have to update many files, they follow a new format. You might ask yourself, should I write a code mod, a program to update the files for me? And the answer is it depends. When you write a code mod, you pay the cost up front. You have to create and configure your package, write code, and write tests to test that code. This can take days or weeks. But when you have a code mod that is backed by tests, the returns are manifold. The code mod can update your project in a second, and it can be reused to help other projects migrate. And here's the crux. When your project has many variations in code because it hasn't been maintained, updating the project by hand will be faster. A code mod will run into many edge cases that may or may not occur in other projects, and every edge case that you handle is extra code that you have to maintain. Nonetheless, the ability to help others, that just might be the deciding factor for you. So as a rule of thumb, if you can cover the 80%, consider writing a code mod. To get started, I would have recommended two years ago Robert Jackson's Code Mod CLI, but this project is unmaintained, doesn't support TypeScript, and makes a divide and conquer strategy, taking small steps in the code mod difficult to achieve. So I created Code Mod Youth Tools, a set of tools and conventions for writing code mods, and now powers all of mine. And just last week, I published a CLI. You can use this to create a modern project that comes with lint, test, CI, and documentation out of the box. My hope is that we can lower the entry barrier enough that if a person can write a function in Node.js, then they can start writing a code mod. I would love to see more people writing one given that Polaris is coming up, and the Glint and Template tag can use a higher adoption. Who knows? Maybe someday I'll give a talk on code mods after taking a long vacation, but we'll see how that goes. And finally, when solving problems together, I want you to think about your interpersonal skills and how they affect how you collaborate. Each of us with a unique background has a different way of thinking and verbalizing how we perceive the world. These differences surface when we discuss ideas and review each other's code. The more we're competent in our interpersonal skills, the better we can appreciate these differences and appreciate one another for who they are. We say interpersonal skills because they are something that we can learn by practice. I myself learned through Toastmasters, we're met many good people, but nowadays with more information online, I would recommend that you branch out and see what interests you. Back in Germany, and this time it's real, you can stream shows called Zaksme, unter anderen unter Geisenfragen, where two to six people with opposing ideas carry a conversation that is personal and civilized. The goal isn't to win the argument, but to actually listen to each other and come up with a compromise. I really like these shows because they can teach us what makes a discussion go well. And finally, I want to show you that you can move towards the future even when your project is behind. For example, you can install polyfills to start modernizing syntax and update Ember CLI to the latest independently of Ember source. If you maintain the V1 add-on, you can enable two Ember try scenarios, Embroider Safe and Embroider Optimize, so that you can discover issues early and come up with a plan to migrate to the V2 format. You can also support client and template tag users even before you migrate to V2. This April at Clark, we started using a private package registry. It helped us extract the linter configurations from the monorepo so that we can reuse them in other projects and standardize how we write code. It also helped us set up another monorepo, with PM came this time, where we extracted add-ons, converted them to V2, introduced Embroider CSS modules, supported client and template tag, and wrote test-ups to make sure that we're ready for the future. By thinking a bit outside of the box, you might just be able to solve many problems at once. In conclusion, when your project is currently hard to maintain and extend, you might feel overwhelmed and hopeless. By taking small steps and iterating a solution, you can introduce change that will help you and others. And you don't have to do it alone. You can ask your team and the community that we have here for help. In one year, when you're back again at EmberConf, I would love to hear how your project is doing. Maybe you'll strike a conversation with me in the hallway or in Discord, or you give the talk like I did today. Until then, box good. Take care.