Conf42 Incident Management 2024 - Online

- premiere 5PM GMT

Implementing SOLID Principles for Effective Code Architecture

Abstract

SOLID principles are the foundation of clean, maintainable code. My talk shows you how to apply them in real-world projects, creating software that’s adaptable, scalable, and easy to work with.

Summary

Transcript

This transcript was autogenerated. To make changes, submit a PR.
Hi, my name is Arpit Kaur, and today we'll be talking about implementing solid principles for effective code architecture. before we dive into solid, let's, let me give you my introduction. I have about more than 16 years of experience leading the large scale organizations such as Amazon, Microsoft, and a couple of other places. And what I've done in these places is delivered at scale, working software solutions below budget, which are still working in production without any bug and serving our customers worldwide. At Amazon, I let some of the contributed. I let some of the successful projects like video games, books, Kindle, Alexa, and I also heard a few patents specifically, which I'm proud of is on one click sell at Amazon and at Microsoft. I focus on Azure and open app projects where I leverage my expertise in building the disability system, which I learned at Amazon, where I spent about a decade here at Microsoft for solving a customer's problem using especially in this, And my questions include API first architectural patterns specifically on Azure and AWS cloud solutions and no SQL data modeling. So moving away from classical SQL based data modeling, which has been a standard in the industry from 1960s. But since in this modern age of big data, no SQL is a little bit lagging behind. Again, depends on the use case. And this is my LinkedIn. Please feel free to take a screenshot of this and connect me on LinkedIn. so let's quickly jump into the solid principles now. So the introduction about solid is it's an acronym It stands for five principles which are used in software design their design principles, but they are marketed as design personal design patterns for object oriented programming But that's not true. It's applicable in everything that we build in software world specifically from functions modules classes services and These solid principles can be used almost everywhere. They were introduced by Robert C. Martin, also known as Uncle Bob. And when he created these five principles, he did not have them in these orders. They were randomly juggled. And then, Michael Feathers, who is a friend of his, created this solid acronym and just got popularized because it's easy and catchy. now let's jump into what these are specifically. So as the name says, it's an acronym. So S stands for single responsibility. And again, we're going to dive deep into each one of these. O is for open close principle. L is for Liskov substitution principle. I is for interface segregation. And D is for dependence inversion principle. Each of these bring up some spotted aspect of software design. again, as I said earlier, ranging from modules, functions, classes, services, and whatnot. And they help in ensuring that our code quality over time remains flexible, changeable and also evolvable because at the end of the day, we are not writing the code for machines. In the last 20, 30 years, the machines have gotten really smart, efficient. We are writing code for our teammates. And our future self two months to six months in the future who would then go and either debug some of the code We are developing or add to the features which have been developed today So keeping that in mind, we have to ensure that our software that we develop is loosely coupled So we what do you mean by that is if you make a change here in one small component It doesn't change or break something which is five chains down the control flow And that is what these, principles solidify and bring to home. So let's start with the first one, which is SRP single responsibility principle. as it says, single responsibility, it's quite simple in terms of understanding single response, you mean one component, whatever it is, it's module class function, they should have only one responsibility. They should do one thing and they should do that one thing quite well. Try to understand it in a way, if you are working in a company as an employee. You should not be, you should be able to be fired by one CXO, so either a COO, CEOO, CEO, or somebody else, which is again CXO, because if we take COO and CEO as an example, if your employee class implements, let's say accounting, which comes under COO and payroll related to that, you And let's say it also implements the actual function of the employee, whatever it is, let's say writing code, so code could be a function. Do not implement both of them in the same class, because if you mess up that class, then you are fireable by 2 CXO. And that is how you can bring home this principle. Just take one responsibility in a class, in this example of employee, you can break it down to different class. And the benefits of this is, It's easy to understand and obviously it makes our code easily testable, easily modifiable. And the challenge is, how do you identify the granularity? Where do you define the boundary of being single? And what I use is, that being fireable by one CXO. So let's use an example. So the example that I was talking about earlier, an employee is trying to do three things here. Calculate pay, save to database, and generate report. Now calculate pay is something related to finances, right? So CFO is eventually responsible for it. Safety database is A technical aspect how your database is structured. So eventually Our database is going to roll up into a cto and generate report is somebody who's going to deal with the finances and the eventual reports of the business and how the business is performing. So a ceo or probably a coo is gonna Deal with these aspects. So there are three cx o level employees who are Directly getting impacted if you mess up this class, how can we break out? So this is a bad example How can we break it into smaller, singly responsible components? Let's try to do this. Take an employee and just implement CalculatePay. Take an employee repository, which is a different class altogether, completely different component, which saves to database and takes an instance of employee. So the functionality of saving to database resides inside this, but how the CalculatePay is happening is completely outside. And then report generator could be in the class. And again, this is just an example. You can structure it accordingly based on your use cases. Our generator board, this also takes an instance of employee, but how the base calculator, how the database is being updated by saving the entities is completely different out of the report generator. So here you see we have broken down the three function into three classes. And this is an example of single responsible. We have three classes, each of them doing the one of the three functions, and we're doing them really well. And jumping on to the next one, which is the second principle, which is open close principle here. It means open for extension, close for modification. What do we mean by that? That any component classes, modules function, they should be open for extension. That means we should be able to add more functionality or extend the existing functionality, but they should be closed for modification to add that extended functionality. We shouldn't be going back and modifying the already implemented code. So let's look at this example, which is, an area calculator and here we have, two, two functions which are overloaded and they take an instance of rectangle are and a circle C and in both the cases, we see that the implementation of the area is implemented on the calculation of areas implemented, but they're quite different. But now remember, what if we have to add a triangle, we will have to modify the class. Okay. Why because height radius and width and by these things are not related to a triangle, right? It's basically a base and height so we cannot extend this class to calculate the area of a rectangle And a better example would be this. So here what we're doing is we are saying break down the rectangle and circle into different classes and then inherit them from An interface has an implementation of calculate area or rather the definition and then each of these classes can implement their own implementation. So when we go into area of calculator, it takes an interface and the interface has the implementation of calculate area. So when we create the class like a triangle, it extends from I shape, but it implements the calculate area in itself. So when you're in area calculator, you don't really have to care about. by or height or width because they are meaningless in area calculating. Those details are abstracted out inside the specific classes of rectangle, circle and triangle, and the implementation is forced by inheriting from the interface. The benefits is that it ensures that or rather it encourages that the risk of introducing bugs in the already existing features. Is close to zero when you're trying to add new features or extend the existing functionality the challenge is that it's very difficult to think the future cases and how do you need to break it up? So that's where the experience and Good level of testing a testable smaller component come into picture which can help you in thinking with this process moving on to the next one, which is Liskov substitution So here, it's, it was created by Liskov and henceforth it's named like this. it doesn't give you any hint about it, but the details are that you should be able to, substitute the lower level implementation of any interface or an abstract class details without breaking the upper level function. Now, again, let's look at this, an example in C sharp. So we have a class rectangle, which has Over 10 in height, which is quite standard. Now, all of us know that a square is a rectangle. It's a type of rectangle with width equal to hide. So here, what we are saying is we are implementing in a cheeky way that we are saying overwrite the width to be equal, sorry, to make, to be able to the height and do the same thing for the height as well. So what happens when we try to. calculate an area. Now if we change the Height or the width of the rectangle. It only changes what dimension but Because we have forced this cheeky way of calculating the height and the width the square also gets updated and that is a forced mechanism because we are trying to Use this real world concept of square is an rectangle from a real world. And this is also a good point to Recollect the thought that even though in real world we think a square is a rectangle, but this is computer programming This Idea that object oriented represents objects from real life is just a marketing gimmick. It's not true And one example would be let's say there's a couple and there are two people who are getting divorced Now the each of them would have a lawyer representing them Now the lawyers who are representing the individuals are not getting divorced themselves Let's use that idea over here the class rectangle and the class square as lawyers of the actual rectangle or actual square in the real world. So even though a square is a rectangle, they share a relationship, but that does not mean that their representatives also share the same relationship. Henceforth, forcing the class square to extend from rectangle is that forcefulness, which is pushing us into this buggy situation. A better idea would be to not force a square to be rectangle, but other than keep under both of them are shapes essentially and calculation of area and Internal dimension should belong within those classes themselves. So let's look at how we can correct this example. Let's take an interface shape, which has a function of area calculation. And then when class Rectangle and Square implement the IShape interface, they can implement the details of the area within themselves. Here the class Rectangle would have its own integer width and height. But class square will only have side. It doesn't have to force two attributes and the area respectively would be inside. The rectangle would be height into width and square. It would be simply a square of the side. So it ensures that this principle of the scope substitution, ensures that we are not changing one implementation of interface and just getting surprised altogether. Because if we go back, to this home example, if we change this detail of because square is in a child class of rectangle here, if you change the implementation in the usage of rectangle to a square, the code completely breaks. Whereas here, it does not break because even if you change the eye shape to represent rectangle or square, it returns the right or the correct area. And here an analogy is child class should be able to fill it parentials without causing chaos because earlier we saw with example of forced relationship that they're causing chaos or Code breakage. the next one would be interface segregation principle. Here, the idea is that we should not force our clients to depend on interfaces that they do not use, and it comes from, 1980s and 90s forcefulness of code when we're trying to add more and more functionalities into interfaces and then just forcing the, child implementations to implement them. An example would be, this thing that is on our screen. here we see a worker, and a worker is a human worker. So a human worker works, eats, and sleeps. whereas a robot worker is also worker. Now, if we put it inside the, as a child glass of an eye worker, it'll have to implement forcefully the functions of work, eat, and sleep. But it doesn't really do these things, these latter things. It doesn't sleep, it doesn't eat. It only does the work. So forcing the robot to implement these two functions is, or rather these two implementations from the interface. Is a forcefulness that will cause us to write dirty code difficult to extend code and This just looks ugly over time. This is a very simple example But we go into production quality code which has many functionalities spread over multiple modules this immediately goes into an exponential explosion of having such Not implemented function or not implemented exception or such cases where you struggle to write basic unit test as well So an idea would be just break it out. Do not force a human worker and robot worker to be extended from the same interface of worker rather break it into a workable, eatable or sleepable kind of interface. A corrected example would look like interface workable, interface eatable, interfaceable, where they do respective work of work and sleep. And when we have a human workup, they implement these three keys, which is workable, eatable, and slippable as well. But robot only does the work so it only does the extension of workable interface and doesn't even test the other two. It ensures that we do not have unnecessary dependency in our code as it goes up. And the problem is, or rather the challenge is to figure out how to break down these functionalities. even though these examples are in C sharp, because I work at Microsoft, Java in itself has done a commendable job in breaking down these interfaces over the last approximately 10 years. Because earlier, if you see Java used to have these forced interfaces, but now smaller functionalities are being broken into individual interfaces. And if you can implement a serializability of cloud, for example, it's an interface, I force to a lot more, but not anymore. So they're doing a really good job in breaking down the interface. Segregation. moving on to the last one, which is dependency in version principle. And here the idea is quite simple, that you should depend on ab section and not con concrete implementations. And an example would be, let's say you have. An IDE. I don't know which one you use. Let's say one from JetBrains or Eclipse or Visual Studio Code or any of those. You install a bunch of plugins. Now, let's say there's a small company, which has created a plugin for each of these IDEs. If tomorrow that company decides to make changes to their implementation, would these, should the IDs break? Ideally not right. These big standard IDs should not be breaking down because one small company decided to change their functionality So if these IDEs depend on concrete implementation of that small companies plug in dependencies Or implementation it would break down if they have a code bug then all of these IDs break down which is Obviously not a good idea. Henceforth if these IDEs depend on the abstractions or interfaces of those low level plugins in this example, a low level module, small company plugins. In that case, if even if that small company plugin has a bug, it itself would fail, but it would not break down the entire IDE. And let's take an example over here. Here we have a customer service which has an implementation or a usage of SQL customer repository. Now, today we are using customer service. which depends on SQL database, let's say Oracle Postgres or any of those standard SQL one. But tomorrow your database grows super big, your rate of scale goes crazy high and you decide SQL is not serving my use case and you want to go to into NoSQL databases like DynamoDB, Mongo, Cassandra or any of those many options available. You will have to come and change inside the customer serve and if you do that. The chance of a bug getting introduced because there was a bug in the new implementation, which breaks down the customer service is high. How can we ensure that we do not depend on the concrete implementation of SQL customer repository inside the customer service? Let's look at the example and I'll come back to the slide in a second. we have iCustomerRepository, which deals with the database stuff. Today, it is using SQLCustomerRepository. Tomorrow we can create a DynamoDB customer repository or a MongoDB or a Cassandra customer repository, which extends from iCustomerRepository. So it deals with all the functions of save, get, fetch, and whatever the databases have to do. The customer service class only depends on the interface. It does not know about the low level implementation of the iCustomerRepository if it is coming from iCustomerRepository. SQL repository, or it is coming from Dynamo repository or any other database repository, it is completely independent of it. So the chances of a low level implementation breaking a high level implementation is super low. An analogy which we have on the screen over here, a car engine shouldn't be directly related to chassis. They should be connected via standardized interfaces like wires and electric cables. Because if you want to change the Engine, you should not break down the entire car. You shouldn't be cutting into the car and breaking down the chassis But simply take out the cables get the new engine in and while it works, let me go back to the previous slide because the This line is quite important abstraction should not depend on details should depend on abstractions. What we mean by that is The dependency of customer service shouldn't care about or depend on the lower level Implementation details of the sql customer repository, which in this bad example, we see it is Whereas if you go to the good example of the bad example, we see that it depends only on interface customer repository Service depending on the customer repository interface and it doesn't care about the low level implemented Which is a better way of doing this or You more modular, slightly flexible way off implementing this. So this is all about the solid design principles. Now we're going to talk about a couple of case studies and then some so even though we have written e commerce platform who overhaul, I've used example of Amazon, which is, which I have anonymized and completely removed all the details. So we know Amazon started in 1994 as us. Small company, which are selling books on the internet back. Then obviously it was a small company the software code base and even the technology stack was super small. It was a monolithic platform, but as it caught up as internet got more accessible to the folks worldwide The demand and the usage of that platform grew in size And back then the tight company between different components made it risky and time consuming, of course To grow and add more features which Obviously led to many bugs over time and there are many internet available or publicly available case studies about amazon went down During the late 90s and early 2000 And they were a result of this tight coupling and not following solid per se so What did they do? They adopted solid principles, even though they were not necessarily known as solid back in the day we broke down The bigger components to smaller codebases, for example, product catalog, auto management, payment processing, so on and so forth even offer because it's a marketplace many merchants come and place their bids to sell their items on the amazon home page we also followed the open close principle where we ensure that pluggable payment gateways could be added Now if today you go to amazon earlier, it was only a credit card But today you can add credit card different bank online mechanisms, and if you go to different countries have different mechanisms to pay. some also offer the feature of, throw away cards, also one time use cards. And also cash on delivery, for example, a card on delivery. Now imagine if the team had to implement all of these new payment mechanism each time A new feature was added. They would be just spending all the time in dealing with bugs But since they use the open close principle, they were simply adding new implementations without touching the older ones so open to extension but close for modification and the lsp the list of substitution is because You We also ensure that most of the components which are at higher level could be replaced without breaking down the system So we create a bunch of interfaces instead of directly depending on the payments. We depend on the payment interface And the lower level implementation of payment would just change according to the marketplace or the country isp, interface segregation, obviously we It was the dependency flow of the control flow of a lot of low level components than the high level components. And we saw in some of the examples earlier, the last one, which is decoupling high level business logic from low level interest. infrastructure concern, we just saw an example of database access and the example I use is a real one because amazon When it started was dependent on sql databases specifically from oracle, but as the company grew they Created their own no sql and sql databases which were which offers, today, postgres based apis and also dynamo db, which is world famous on AWS So different APIs could be used from these two type of different databases, SQL and non SQL. But the high level logic doesn't really care about it and shouldn't care about it. And it works. We have seen the results. We obviously reduced bugs count by a significant number. The development increased, the development time and the speed and efficiency increased by a factor of what we have on the screen. Thanks. And also it makes the developer's life slightly easy and happier to not deal with the bugs and be able to work on the new features at a reasonable speed. now with this modern software development with the everything being on the cloud and with this new AI way of catching up around the world, the same concept still applies. As I said earlier, they're not bound to object oriented or couple of technology. It's just a basic logic or basic framework for developing software pieces. So today we have microservices a lot and we're going into serverless where we're putting our microservice components on to a serverless component like lambda and function app on azure. So lambda on AWS and function app on azure, keeping those services and components or functions or lambdas separate and just making them making sure that they do one work. Effectively is how we can use single responsibility and some of these principles that we have learned along the way, on the cloud. And in this new age, Microsoft's architecture, we Also, now held by AI, OpenAI specifically, how it helps with our Agile, because since we are building smaller components, we are keeping things small. We are keeping the dependency between different components small. It also helps with agility. It helps us in moving faster in a better way. And we are able to get our. Entire dependency cycle of code development, testing, deploying into production, testing that feature, coming back, adding more in a reasonable way rather than a older way of being less agile in a waterfall that we had to develop a lot before we could even test and it could easily be iterated upon and automated using CI and CD pipelines. I said, since we are developing components in a smaller fashion, the testability gets better. The results of the tests gets better. Writing unit has become super easy because, instead of writing a unit as which takes five functions or five functionalities inside one module, we just have to test one. It also helps in other modern technology frameworks like TDD or methodologies, rather TDD test driven framework. Thank you. Test driven development because if you are developing and testing a smaller function You're getting a result right away instead of writing big functions and then big unit tests Which makes the tdd adaptability a bit more difficult and since we're able to test and iterate better just that Feature enables or the flexibility enables us to make our deployment more elaborate and less error prone. Now, let's talk about Common field of pitfalls and miscomption what generally happens is with soft engineering like us We learn something and then we immediately get tempted to go and use it. So don't force it Don't try to put solid into everything. it should come naturally Or try to question yourself every time you're developing or designing a new feature or even refactoring an older one Is there a way to make it simpler and if your code base is really easy and simple And it doesn't give you any problem. Let it be don't fix what isn't broken. Sometimes we try to do premature optimization. don't don't create unnecessary abstraction interfaces don't have 10 classes Doing one function each and having 10 interfaces on top of it and that can just have a simple class It doesn't make any sense to create One interface and one class just for one functionality, which will never ever change and As I have said multiple times, SOLID is not for object oriented programming. It's basically fundamental software designing. It has nothing to do with object. It's just a badly marketed sales gimmick. Just use the concepts of loose coupling, high cohesion, and flexibility everywhere in software design and development as well. in conclusion, the key takeaways for us are that SOLID principles are not just theoretical concepts, but practical tools in ensuring our software is easily extensible, adaptable, and scalable. Continues to stay robust and by using solid net data software development. We can create code bases which are Doubling I believe every year one point less than 1. 5 years it's easier. They are easy to understand and modify and extend by our Fellow soft engineers and ourselves in the future And as we use this since we are able to write better code And better test along with the code and cicd pipeline It is ensuring that we are keeping our tech debt To a lower number avoiding it is again a myth cannot avoid all the tech debt All we can do is strive to keep it low as possible And so in summary whether you're building cloud native app working in agile environment or in devops or in Non cloud on prem device situation. It doesn't matter you can use solid to build a solid foundation for success for your software And empower your customers to use your software without worrying about day to day debugging and getting stuck up into unnecessary Tight couple software. So with that, thank you very much. These are my details and happy to see you guys Thank you
...

Arpit Gaur

Principal Engineer @ Microsoft

Arpit Gaur's LinkedIn account



Awesome tech events for

Priority access to all content

Video hallway track

Community chat

Exclusive promotions and giveaways