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