8. An example – let’s change active policy title
to blue
if (policy.policyStatusCode === 'A')
{
// policy is active,
// display policy title in green
}
if (policy.policyStatus === 'Active')
{
// policy id active
// display policy title in green
}
As a new developer how easy would it be to find this code
and change the title to blue?
9. Agile breaks down each problem into smaller pieces,
which makes it hard to see the big picture.
Smaller problems can solved with quick solutions,
but the quick solutions end up staying.
15. As a developer, your code:
● Must work
● Needs to embrace changes
● Is production support friendly
● Should cover all known scenarios / bugs with automated tests
● Is not overkill
16. How can we have good design without being able
to see the big picture?
Design Principles
24. Single responsibility
Each module should only have one responsibility
That responsibility should only be handled by one
module
Size of responsibility depends on the scope
25. Open / Closed
Open for extension
● Subclass
● Composition
● Dependency Injection
Closed for modification
● Private scope by default
● Using accessors if possible
export const privateFunction = (policyType
= '') => {
let result = policyType;
...
return result;
};
export const publicFunction =(policy) =>{
const someVariable =
privateFunction(policy.policyType);
...
return someValue;
};
export default publicFunction;
In this talk, I want to share my understanding of design thinking, agile and how to apply design principles to improve our code quality. My name is Hebin Wei, I am a member of I manage my account team.
In all software developing, there are two major challenges: Doing the right thing and doing the thing right. At Liberty, we using Design thinking and the Agile process to make sure we do the right thing first.
By combining the Agile and Design thinking processes together, we make sure we focus our resources on the most important thing first, getting customer feedback faster.
The image on the left shows the steps of Design Thinking. It starts with researching, collecting customer requirements. Then we sort these requirements, find out customers’ pain points. Next, we define the problem. This is because sometimes customers said what they want is not what customers really need. When a customer want a drill from home depot, the drill is not what he needs, the hold on the wall is. Once we understand what customers need, we start to design and generate ideas. Next step we will pick up the best solution and build it, deploy it. This solution will be tested by customers, we will collect feedback and apply them in our next iteration.
One thing I want to emphasis here is, the “Design Thinking” is not just UI design, it is solution design. We design a solution to satisfy our customers’ needs.
These loops happen at different layers. At each layer, we break the big problem into smaller problems and focus on implement the solution for these smaller problems until we solve the big problem. Then the new cycle will start at a higher level.
If you ever did any agile training, you probably already see this diagram. It clearly demonstrates the difference between the Agile process and a traditional waterfall process. Instead of delivering a result at the last step, the Agile process focuses on delivering an acceptable result and get customer feedback early in each loop. This diagram did a good job to explain what the Agile process is, but it also causes some misunderstandings. One argument I heard most is, “we are on an Agile process, don’t worry about the future, Let’s implement a simple solution first, we will refactor it later.”
But when building software, massive refactor or rebuild from scratch is expensive. I cannot find any diagram online to demonstrate what I want, so I made one myself. I want to use this diagram to show, in the Agile process, the next phases should build on top of the previous one. Refactors should happen in small and manageable scope. I choose lego cars to show the idea that each building block should be modularized so that it can be improved or refactored independently.
When we working on the current code, we should consider future refactor and extension.
Before we get into how to do the thing right, let’s see some examples.
These are makeup codes. These two components, show the policy title in green if it is an active policy. But the way they check the policy status is different. Now the requirements changed, the active policy’s title needs to change to blue. Just think how difficult for a new developer on board to miss one of these components.
The agile process helps us break a bigger problem into smaller problems. We are more focused, but it also means we cannot see the whole picture in each loop. A smaller problem can be solved by a quick solution and many of these quick solutions end up staying.
The conclusion is the Agile process needs good software design.
It needs to be modularized. it should has clear layers / modular. The relative responsibility should be grouped together in a module. A module can be an npm package, a web service, a class, or a file. A module behaves like a black box. They communicate with each other using public methods, pre-defined by an interface. Except for these public methods, their implementation detail is not accessible.
A good design should easy to read and understand. When a piece of code becomes hard to understand, it should be break into different modules or functions. Do not depends on operators priority, using parentheses. When choosing between an optimized but complex algorithm and an easy understanding algorithm, I will vote for the second one. Unless it is a critical method.
Testing is important. Testability should be considered in software design. There are many design patterns can help. Such as Dependency injection pattern, creator patterns.
Customer requirements never end, especially in the Agile process, we will never have a “completed” requirements. So our design should be prepared for changes.
Reusability is the key to do little but accomplishes more. Before start coding, seeking existing code first. Implement something from scratch is expensive. The cost not just includes time, resources, but also includes finding bugs, lost customer satisfaction. When we do have to implement something new, make it reusable.
As a developer, we should make sure our code works. Commit a not working code to develop is a disaster. Let others review your not working code is a waste of time. One way to help your code working is writing unit tests cover all known scenarios.
Our code needs to embrace changes. Remember in the Agile process, we cannot see the whole picture. Even we do, the customers’ requirements change constantly, our code should be prepared for that.
An enterprise-level software system shall have a good logging system. The logs should be traceable, allow customer support techs to easily trace a service call back to its front-end user session.
Production Support BPM example:
https://git.forge.lmig.com/projects/USCM-ESERVICE/repos/eservice-service-modules/browse/PmInternetAccountServiceModule/com/lmig/pm/internet/service/account/registration/RegistrationCacheManager.java#62,103,108,119,130
Traceable logs also mean able to identify the execution path of code. The example above shows how BPM code logs the enter/exiting of function. My team in my last company, a major function without enter /exit log cannot pass review. Even this, the above example still not able to link the log with the user session.
There is no bug-free system. Found a bug in a software system is understandable. But what embarrassing is a known scenario not working or a fixed bug happens again. Using automation tests to cover all known scenarios and any bugs already fixed is a good way to prevent this embarrassing.
The last is to keep the balance.
Now we understand the Agile process needs good software design. We know what a good software design is. How do we have a good software design in the Agile process?
My answer is Design Principles.
The design principles help us to find out what is wrong and what is right while only see portion of the whole problem.
It is a set of well defined and time verified principles. It maybe not always be the best solution, but it can help avoid the most common mistakes.
The goal of design principles is to help build a good software system, which to provides values to our customers. We use design patterns to satisfy design principles. We use the CQRS pattern to separate the reading/writing responsibility, not because we need to create a Kraken service. There is no point to apply this pattern if your service only needs to read the resource.
When I doling software design, I like to put the related functionalities into a group and give it a name, like policy, dataLayer, billingSystem. It is very natural for me using object-oriented design. It works well with SOLID design principles.
There are some articles about the issue of object-oriented programming. Like the article posted by this guy. I think blindly accepting anyone’s idea is dangers. Instead, analyze what points make sense and apply them into our works is a better approach. I don’t think OOP is a disaster, and I don’t believe functional programming is a silly toy. They all have a place they work best and place they are not good. Use the technique at the place it works best instead of drop it completely.
The object-originated design does not equal to object-originated programming. Not a Class. OOD is a way to think. It is a mindset. It is a tool to separate concerns. OOD can be implemented with OOP. But you have a lot of choices. Any language can group state and behaviors into an entity can be used to implement OOD.
Single responsibility: one object should only have one responsibility.
Open/ closed: Open for extension but closed for change.
Liskov Substitution:
When implement Polymorphism, the children implementation can extend but not modify the parent behavior.
Interface segregation: the interface should be focus and small.
Dependency inversion.
Using interface to communicate. Do not using concrete implementation directly.
Module should has purpose, and only one purpose. A module here can be a package, an object or a web service.
At eservice, billing team responsibility for make a payment. At billing squad, FIN responsible for make a payment. At FIN level, they break a payment into billing request send to bank and payment received bank.
payByCreditCard and payByCheck can be implemented separately. But they should be grouped together within a bigger module.
Accessors can be override. A directly data access is hard to refactor. We all use lodash/get to access data. it coupled the code with the implementation detail. But it so easy to use, I like to use it.
I feel this is hard to understand. Here is a good example.
I don’t get how the overwrite align with this principle.
Interface used to describe a role.
At first round, IPersistedResource serve the need.
The second round, we realized some resource is read only. So IPersistedResource divided into ILoadResource & IPresistResource
The key here is don’t overkill. Seperate the role when only it need.
I want to talk about dependency Inversion first.
A system design should described using interfaces. Your parameter types, property types, returned data types should be interface. That is why I push our team switch to typescript.
When only interact with interfaces, you will not touch implementation detail. And dependency inversion principle will automatically followed.
How you implement it does not matter.
First, it helps us make design decisions objectively instead of subjectively. These principles will become a checklist. It help teams using the same standard when design and review. Our decision will base on these “facts” not “feeling”. “Feeling” is different between peoples. I feel a piece of code is easy to understand, but you may not agree.
Open / Close allow add new features
Interface segregation limit the affected scope if something has to be changed.
Lskov substitution ensure the new implementation continue to work after it replace the parent
Dependency inversion will decouple the concrete implementation.
Abstract method or dependency injection to allow caller extend the default behavor. Or implemented new features.
The key here is when Snake or switch between EmailLogger and EventViewerLogger, I don’t need to make any change in my Animal or Customer class. I can change the my implementation inside my module if I need to.
The Client represent different types of services. When they tangled together, Change Client A’s method maybe affect client B’s method. When they are defined separately, I will feel more confident to make change for Client A. Also when I need to pull each client’s service into it own service host, it can easily do it.
CIP also make the client side easier. Because Client A only care about the Service A interface, when Service A moved to its own instance, Client A don’t need to change.
You don’t know when you service need to be break into separate services. ICP will prepare you for that.
One responsibility only handle by one object, and one object only has one responsibility make code easy to trace.
Interface segregation help focus on the main logic when reading code.
Dependency inversion prevent code noises when reading code.
More focused responsibility make code easy to understand. Because more focused.
I only talk to the interface I need. I don’t care how you connected together behind.
Don't care how it was implemented, Only interact through interface. x
Single responsibility, the less path, the easier to set up some checkpoints.
Interface segregation make test suite small and focus.
Dependency inversion allow use same test suites to test multiple implementation. Because the tests is depended on abstraction layer tool. To test different implementation, just switch the concrete object to test.