Journey into writing code that is readable, understandable ,maintainable and testable as well as how to prepare codebase to embrace future changes.
I wonder from time to time whether I'm doing the right thing with respect to guidelines and principles when using an object-oriented programming approach to create application. I started reading Object Design Style Guide book last year to learn more about objects. And beginning this year, I decided to delve deeper into writing code that is readable, understandable and maintainable as well as how to prepare codebase to embrace future changes.
Here are the resources used:
Book
- Object Design Style Guide:
- Principles of Package Design
Pluralsight Courses
- Writing Readable and Maintainable Code
- SOLID Software Design Principles
- Abstract Art: Getting Things "Just Right"
- Getting Started with Dependency Injection
- Writing Testable Code
- Refactoring: Best Practices
With Object Design Style Guide, I shared series of blog posts on my lessons on Dev.to Click Here to read.
Apart from Object Design Style Guide, below are the key takeaways from the resources including Class Design which is the part I section of Principles of Package Design.
As a developer you need to develop and maintain software.
When you stare at code for longer period of time trying to figure out what the code does? Questions like Why can't I understand this code?
pop up in my head?
- Is it because I'm not experienced enough?
- Do I need 5yrs of experience to get to another level where I will be able to understand all code that I encounter?
- Is it because the code is terrible or poorly written?
Also, there is one question that I keep asking myself:
- How do I know if I write professional code? Thus when the person reviewing my code does not give any bad reaction; what a heck is this?
The answer to these questions is defined by Donald Knuth as The best programs are written so that computing machines can perform them quickly and human beings can understand them clearly.
In this course, you will learn how to write code that you and others will enjoy working with; thus easy to read, understand and maintain.
CLEAN CODE IS ABOUT READABILITY
Clean code is not a nice-to-have. It is practically a necessity.
Dirty Code has consequences like
- Less understanding of code which in turn leads to bugs
- Technical debt and that reduce productivity.
- Lower job satisfaction
What do you naturally care about as a developer?
- Clean code
- Naming things: Classes, methods, variables
- Better Constructors:
- Implementing methods
- Handling Exceptions: Happens in methods
- Class: What it should contain and how it should be organized
- Writing Comments
- Improving Tests
- Maintaining clean code
John Woods:His quote really made always improve my own code. It reads as follows: Always code as f the guy who ends up maintaining your code will be a violent psychopath who knows where you live.
Name should be noun
- Concrete like Calculator
- Abstract like EmailSender
It should be specific
- How specific: No clear answer. For instance a house with specific names, getting the keys will always be problem?
Code Demo:
-
CommonUtils: Noun but not but not specific at all. It probably has a lot of unrelated stuff. Hence we have to take a look inside and the methods will help you get what it does. The question that comes next how are the methods related to the class.
-
Solution to the CommonUtils: The class has a lot of helper methods for time, strings and numbers which could be names TimeNumberStringUtils but this breaks the SRP rule hence we should split them into individual classes like TimeUtils, NumberUtils, StringUtils.
-
Client: Client is noun but not specific. What kind of client? Got to look inside the class to find out. It has a comment to tell what the client is about.
Techniques for fixing poorlyChosenName
- Split
- Rename
Coordinator Class Class meant for delegating tasks to other classes. They are meant to bring classes together.
- People like to add suffix manage. Eg. StoreManager, FlightManger.
- It is okay to call entire application a manager but a single class not.
- Narrow to scope. Eg. Writer, Builder, Controller, Container,
- To narrow you can follow design patterns naming like CarFactor(for producing car objects), HTTPBuilder(for creating http objects with method chaining)
- Always specific
- never single letter
- ideally 1-2 words
- use camelCase
- prefix booleans with "is" like isValid.
- use ALL_CAPS for constant.
- Name after what the server returns
Code
- The http response was assigned to a variable called $data. This should be named after what the method returns. if it is details of a customer should be customerDetails, etc.
The should reveal intent Just by looking at the name the method should be known what it does. Not how it does it but what
- One rule could be if you have to look inside the method to understand what it does- the name should be improved.
- They are actions and here is template Verb(Do what) Noun(To what) Result load + Page = loadPage() set + Price = setPrice convert + Currency = convertCurrency() Sometimes the noun could be omitted if the noun itself is the class. For instance Currency class can have convert as method on convertCurrency.
Code Demo Our send method on the WebHttpClient is not meeting the template for naming methods. So it should refactored. On being specific, it should be named to sendGetRequest or sendGet is cool Methods Anti-patterns
- Method does more than the names says
function getCustomerData()
{
//get Data
//format it
//pre convert
return $data
}
This methods does more than just getting data. It should have be named get&Format&ConvertCustomerData()
which is bad. This should be split.
EXceptions
- Static methods
- Fluent interface
Usually creating object of class requires using the new
keyword. When the creation of the object of the class is very complex used static factory methods.
Static factory methods Encapsulate the construction logic thus helps hide the complexity. And you want to change the construction of class, you do that in one place not all the places the class is used.
Constructors chaining Used for several constructors and chaining helps to avoid boilerplate code to keep things DRY.
Builder Pattern When there constructor telescoping anti-pattern, this comes to a rescue
Majority of code happens in methods. Classes are just containers, constructors are for creating objects. With implementing methods we will look at:
- What methods should return
- Parameters
- Inside the methods: Fail fast and return early principle and how they contribute to clean code
- conditionals and best way to handle them
- code duplication
Clean Code Concepts
- DRY: Don't Repeat Yourself
- WET: Write Everything Twice
- Cyclomatic Complexity: Software metric to measure the complexity of a program. Aim for lower CYC.
- Signal to Noise Ratio: Signal is code that is clean. Noise is everything that prevent us from understanding code.
Avoid returning null.
- NullPointerException
- Check Catching exception or checking for null adds more CYC.
Avoid Magic numbers
Fewer method arguments are better. 0-2 is Ok, Refactor 3 or more.
Methods with 3+ arguments might:
- Do too many things hence split it
- Too many primitive types hence pass object
- Takes boolean (flag) args hence remove it
They proclaim the function does more than one thing.
Split functions For instance instead of one function with flag, create two function for each state of the flag. Like
switchLights(room, boolean)
shd beswitchLightsOn(room)and switchLightsOff(room)
if its one then the function name shd describe it if not then create descriptive variables for the numbers
Checking for validity of values might end up writing nested if-else statements hence increasing the CYC. To reduce much CYC, return early
Check illegal args and throw some exception b4 the application classes with some errors.
Simply conveys if something is true or false so they give u one option out of many
Boolean Checks
- Not this `(!doorClosed == false)
- is Prefix (isDoorClosed)
- not antinegatve like isDoorOpen is simpler than !isDoorClosed
Logical Evaluation
- Extract conditionals to descriptive method that sounds like a boolean than just using primitives
Throw clearer exception message Throw new custom exception in the catch or log it
SRP A class should do one thing. Hence a class with two methods breaks it. This definition is not valid and that could be a direct translation for the SRP of method. SRP of class means a class should have only one reason to change. Functionality of class should be based on role hence asking the question who does this functionality rather than what
Cohesion SRP leads to higher cohesion. Cohesion means tendency to unite or logically connected together. Makes sense to group things together to work together. Cohesion in context of programming refers to the degree to which elements inside a class or module belong together. Cohesive class has
- Fields and methods are co-dependent
- methods that use multiple class fields
Coupling The degree of interdependencies btn software components
- Tight coupling Classes are tied that you cannot change one without changing the other. Tight coupling is maintenance hell.
- Loose Coupling Changes in one class requires less or no changes in other class
- To reduce Tight coupling: Program to interface, adhere to SRP.
Adhering to SRP creates high cohesion and also try to make code low coupled hence there is code quality leading to maintainability and Readability
Principle of Proximity or the Proximity Rule: It is about placing interconnected methods in the right order so it reads like a book; top-down approach
Developers needs to make decisions almost all the time. And this applies when developers are designing their classes. Hence there are principles to serve as guide during these decisions. Here are some of the principles.
- General Principles.
When it comes to class design there are so many guidelines we should follow, like: choose descriptive names, keep number of variables low, etc. These are general guidelines to keep code
readable, understandable and therefore maintainable
. - SOLID principle: They are meant to prepare the codebase for future changes. Hence providing guidelines on how codebase could be writing to suit business changes. Thus most business change over time and so do the software requirements.
This states that a class should have one and only one reason to change.
Here reason to change denotes the responsibility of the class.
RECOGNIZING CLASSES THAT VIOLATE THE SRP PRINCIPLE To determine if a class violates SRP:
- Too many many dependencies
- Too many instance variables
- Too many public methods
- Each method of the class use different variables
- Specific tasks are delegated to private methods
Refactor
- Use collaborator classes
This states that you should be able to extend a class' behavior without modifying it.
A unit of code can be considered open for extension
when its behavior can be easily changed without modifying it. The fact that no actual modification is needed to change the behavior of a unit of code makes it “closed” for modification.
RECOGNIZING CLASSES THAT VIOLATE THE OPEN/CLOSED PRINCIPLE This is a list of characteristics of a class that may not be open for extension:
- it contains conditions to determine a strategy.
- Conditions using the same variables or constants are recurring inside the class or related classes.
- the class contains hard-coded references to other classes or class names.
- inside the class, objects are being created using the new operator.
- The class has protected properties or methods, to allow changing its behavior by overriding state or behavior
Refactoring
- Abstract Factory: Responsible for finding the right class to instantiate. Hence a new class is designed to follow the Abstract Factory pattern. The abstractness is represented by the fact that its published method is bound to return an instance of a given interface. We don’t care about its actual class; we only want to retrieve an object with method of the published interface. So we need an interface for such specific strategies. And then, we make sure every strategy is transferred into a class that implements this interface.
Refactoring Abstract factory to obey extension
- Dynamic factories: Dynamic class names
- Callable factories: Pushing the knowledge of object creation logic outside factory method
States that derived class must be substitutable for their base.
This has two conceptual parts:
- Derived and base class: Derived class extends some other class which is the base class. Base class can be concrete class(concretion) abstract class or interface(abstraction)
- Being Substitutable: When derived class behaves as expected. That means implements all the methods of the base class and abides by its return type.
RECOGNIZING CLASSES THAT VIOLATE THE LISKOV SUBSTITUTION PRINCIPLE
- A derived class does not have implementation of all methods
- Different return types of substitutes
- A derived class is less permissive with regards to method arguments
Refactoring
- Split base classes
- Define return types on the base class
States that make fine-grained interfaces that are client specific.
Fine-grained interfaces stands for interfaces with a small amount of methods. Client specific means that interfaces should define methods that make sense from the point of view of the client that uses the interface.
RECOGNIZING CLASSES THAT VIOLATE THE INTERFACE SEGREGATION PRINCIPLE
- No interface just a class: This creates implicit interfaces.
- Multiple use cases. Interface with too many published methods are usually used for meeting different client needs. Example is the service container. They are usually used as dependency injection container and service locator.
Refactoring
- Refactor implicit interface into Header and Role Interfaces.
- Split interfaces
States that a class should depend on abstractions, not on concretions.
The last of the SOLID principles of class design focuses on class dependencies. It tells you what kinds of things a class should depend on.
The name of this principle contains the word “inversion,” from which we may infer that without following this principle we would usually depend on concretions, not on abstractions. The principle tells us to invert that direction: we should always depend on abstractions.
VIOLATIONS
- A High-Level Class Depends on a Low-Level Class
Refactoring
- Abstractions and Concretions Both Depend on Abstractions.
Learn how to apply the SOLID principles of object-oriented design to create a loosely coupled software systems that easy to change, test and maintain.
Problems that appear when Solid Principles are not used.
It is not enough for code to work -- Robert C Martin, Clean Code. If every bug is extremely difficult to fix, if every change is very costly and code is unmaintainable nightmare.
This is where SOLID principles come to play. This is the foundation on which we cn build clean, maintainable architectures.
Couple of Examples where there is problem without SOLID
- App has many modules: Documents, Payment, Employees, Reporting, Persistence
- Change Request: add new payment method
As developers, we go to the payment module, make the change, deploy and after deployment we notice we have bugs in sub-systems(other modules); This is called Code Fragility, a term coined by Robert C Martin or Uncle Bob. It is the tendency of the software to break in many places every time it is changed.
- Change Request: Update Report with new data
We go to the reporting module, start implementing change, have to modify other parts of the system. This is another symptom of code that is not robust. And it is called Code Rigidity; the tendency for software to be difficult to change , even in simple ways. Every change causes a cascade subsequent changes in dependent modules.
Both are symptoms of high technical Debt.
Technical Debt is the silent killer of all software projects. The cost of prioritizing fast delivery over code quality for long periods of time.
Controlling Technical Debt Write code, pay debt(refactor - SOLID Principle, Design Patterns, Decoupled Components, test),
Acronym for 5 software design principles that helps us to keep technical debt under control.
The 5 SOLID principles are:
- Single Responsibility Principle
- Open Closed Principle
- Liskov Substitution Principle
- Interface Segregation Principle
- Dependency Inversion Principle
Understanding Solid Principle in theory is nothing but knowing how and when to apply them in real application is completely different stories.
Globomantics HR
- Employee Management
- Tax Calculation
- Pay slip generation
- Reporting
Understand SRP Identify multiple reasons to change in your own codebase Danger of having multiple responsibilities
States that every function, class or module should have one and only one reason to change.
Responsibilities = Access of change Examples: Business Logic, UI, Persistence, Logging, Orchestration(Component comm), Users.
One important habit you need to develop as a software developer is the ability to identify the reasons to change that your components have and reduce them to a single one.
Tricks to identify multiple reasons to change
- If Statements are clear signs that there are multiple reasons to change
if(employee.getMonthlyIncome() > 2000){
//some logic
}else {
//some logic
}
-
Switch Statements each case represents one responsibility
-
Monster Methods Large number of lines of code, mix level of abstractions in the implementation, do more than expected
Income getIncome(Employee e){
Income income = employeeRepository.getIncome(e.id);
StateAuthorityApi.send(income, e.fullName);
Payslip payslip = PayslipGenerator.get(income);
JsonObject payslipJson = convertToJson(payslip)
EmailService.send(e.emil, payslipJson);
return income;
}
- God Class A class with all helper methods doing different stuff.
class Utils
{
void saveToDb(Object o){}
void convertTOJson(Object o){}
byte[] serialize(Object o){}
void log(String msg){}
int roundDoubleToInt(double val){}
}
- People method used by one actor can have a little bit of different
SRP Example Well defined purpose Names should be explicit Functions should be inline with the purpose.
class ConsoleLogger
{
void logInfo(String msg)
{
System.out.println(msg)
}
void logError(String msg, Exception e){}
}
Dangers of Multiple Responsibilities
- Code is more difficult to read and reason about
- Decreased quality due to testing difficulty
- Side Effects - Lies of function indicating it has something but does other things internally
- High Coupling Coupling is the level of inter-dependency between various software components. It exposes the internal implementation of a particular class
Refactoring 0Starter to SRP
- Employee class has a save method with 3 responsibilities(serialization, file access, logging) and over coordination with try and catch block
- Employee class is a business entity which means the save method should not be here since it deals with persistence -Create classes for each responsibility and change the implementation of the save method -Take orchestration(try/catch) to the caller
Evolving Code with the OCP Classes, functions, and modules should be closed for modification but open for extension
A class is closed for modification if each new feature to be added does not require modifying the existing source code. The source code becomes immutable
A class is open for extension if it is extendable to make it behave in new ways by writing new code
Conceptual Example: Class A has B and C as dependency. If implementing a new feature in A by modifying the existing code, there is high risk of breaking B and C. This causes rigidity. In a real, application the dependency graph is much more complex and changes to sub-structure will have ripple events on the system.
A better approach is to write the code in new class to reduce regression bugs.
OCP Implementation Strategies
-
Inheritance Coupling especially with concrete class
-
Strategy Pattern Extract functionality into interface(s) Create classes that implements the interface Factory to build the class based on a property
-
Which approach to choose Progressively apply OCP
Very useful for creating correct type hierarchies. It states that any object of type must be a substitutable by objects of a derived type without altering the correctness of the program.
Substitutable is all about relationship between types.
Relationship is not is a relationship in object orientation. Instead we should ask ourselves; Is particular type substitutable by another type. Eg; Is the class rectangle fully substitutable by class square.
Violations
- Empty Derived Methods or functions.
class Bird
{
public void fly(int altitude)
{
setAltitude(altitude);
//fly logic
}
}
class Ostrich extends Bird
{
@Override public void fly(int altitude)
{
// Do nothing: An ostrich can't fly
}
}
Bird ostrich = new Ostrich();
ostrich.fly(1000)
The class Bird is not fully substitutable by class Ostrich
- Harden Conditions Setting values to fit derived class based on its unique properties
class Rectangle
{
public void setHeight(int height)
public void setWidth(int width)
public int calculateArea()
{
return this.width * this.height
}
}
class Square extends Rectangle
{
public void setHeight(int height){
this.height = height
this.width = height
}
public void setWidth(int width){
this.height = width
this.width = width
}
}
Rectangle r = new Square();
r.setWith(10);
r.setHeight(20);
r.calculateArea() // return 400 instead of 200
Rectangle is not fully substitutable by Square
- Partial Implemented Interfaces Not implementing all the methods of the interface
interface Account
{
void processLocalAccount(double amount)
void processInternationalAccount(double amount)
}
class SchoolAccount implements Account
{
void processLocalAccount(double amount)
{
//business logic here
}
void processInternationalAccount(double amount)
{
throw new RuntimeException("Not Implemented")
}
}
Throwing exception to mark that the method is not support by this class violates LSP Hence an Account is not fully substitutable by SchoolAccount.
- Type checking
for(Task t : tasks){
if(t instanceof BugFix){
BugFix bf = (BugFix)t
bf.initializeBugDescription()
}
t.setInProgress();
}
Indication that the sub types are not adhering to the base class
Fixing Violations Two great ways
- Eliminate incorrect relations between objects
- Use "Tell, don't ask!" principle to eliminate type checking and casting
- Empty Derived Methods or functions.
- Break the relationship
class Bird
{
//Bird data and capabilities
public void fly(int altitude)
{
setAltitude(altitude);
//fly logic
}
}
class Ostrich
{
//Ostrich data and capabilities. No fly method
}
- Partial Implemented Interfaces
- Break interfaces down into smaller and more focused
interface LocalAccount
{
void processLocalTransfer(double amount)
}
interface InternationalAccount
{
void processInternationalTransfer(double amount)
}
class SchoolAccount implements LocalAccount
{
void processLocalTransfer(double amount)
{
//business logic here
}
}
- Type checking Create a class for the checking object and override the default method.
// for(Task t : tasks){
// if(t instanceof BugFix){
// BugFix bf = (BugFix)t
// bf.initializeBugDescription()
// }
// t.setInProgress();
// }
class BugFix extends Task
{
@Override
public void setInProgress(){
this.initializeBugDescription();
super.setInProgress();
}
}
for(Task t : tasks){
t.setInProgress();
}
Summary of LSP Dont think of relationships in terms if "IS A". Instead ask yourself is a particular type always substitutable by another sub type. Empty methods, type checking and hardened preconditions are signs that you are violating the LSP.
Modularizing Abstractions with ISP The perfect granularity level for abstractions It is defined as clients should not be forced to depend on methods that they do not use. The interface here does not mean interface keyword for programming language. It means any public methods that all class depends upon.
It reinforces other principles.
- Keeping interfaces lean makes the derived class fully substitutable by the base class
- Class with few interfaces are more focused and have single purpose.
- Identifying Fat Interfaces There are couple of symptoms that manifest themselves that tell you precisely that the interface should be refactored and made smaller
- Interface with many methods
interface LoginService
{
void signIn()
void signOut()
void updateRememberMeCookie()
void getIUserDetails();
void setSessionExpiration(int seconds)
void validateToken(jwt token)
}
class GoogleLoginService implements LoginService
{
void signIn()
void signOut()
void getUserDetails()
void updateRememberMeCookie(){
throw new Exception("not implemented")
}
}
- Interfaces with Low Cohesion Cohesion refers to the purpose of a particular component and all the methods are aligned with the overall purpose
interface ShoppingCart
{
void addItem(Item item)
void removeItem(Item item)
void processPayment()
void checkItemInStock(Item)
}
class ShoppingCartImp implements ShoppingCart
{
public void processPayment()
{
PaymentService ps = new PaymentService();
ps.pay(this.totalService)
User user = UserService.getUserForTransaction();
EmailService emailService = new EmailService();
emailService.notify(user)
}
}
Here processPayment and checkItemInStock do not conceptually belong to a shopping cart and that makes it not cohesive, increases coupling. They could belong to another interface.
- Empty Method
public class SchoolAccount extends Account
{
void processInternationalPayment(double amt){
// Do nothing, Better than throwing an error
}
}
- Refactoring Code that depends on Large Interfaces
- If you own the code; break interfaces into pretty easy and safe due to the possibility to implement as many interface you want.
- External Legacy Code: You can't control the interfaces in external code so you used design patterns like Adapter.
//From a fat interface.
interface Account
{
double getBalance()
void processLocalPayment(double amount)
void processInternationalPayment(double amount)
}
//To three lean interfaces
interface BaseAccount
{
double getBalance()
}
interface LocalMoneyTransferCapability
{
void processLocalPayment(double amount)
}
interface InternationalMoneyTransferCapability
{
void processInternationalPayment(double amount)
}
I think this is the most important principle among the SOLID and allows creating systems that are loosely coupled, easier to change and maintain. The DIP states that
- High level modules should not depend on low level modules, both should depend on abstractions.
- Abstractions should not depend on details. Details should depend on abstraction.
A whole lot of questions on the definition:
- High Level Modules: They are the part of applications that bring real value, modules written to solve real problems and use cases. They are business logic; provides the features of the application. Tells what the software should do but not how they should do it. EG: Payment, User Management,
- Low Level Modules: implementation details require to execute business logic. They re the internal of a system, tells how the software should do various tasks. EG: Logging, Data Access, Network Communication, IO;
They are not absolute terms, they are relative to each other.
- Abstraction is something that is not concrete. Something that as developers we cannot "new" up. In Java applications, we tend to model abstractions using interfaces and abstract classes.
Putting all together A(H)-> Abs <- B(L)
Writing Code That Respects the DIP
- Low Level Class -- Concrete data access class that uses SQL to return data from database
class SqlProductRepo
{
public Product getById(String productId)
{
//Grab product from SQL database
}
}
- High Level -- High level(PaymentProcessor) class depends directly on low level(SqlProductRepo) as it news it up.
class PaymentProcessor
{
public void pay(String productId)
{
SqlProductRepo repo = new SqlProductRepo();
Product product = repo.getById(productId);
this.processPayment(product);
}
}
- Abstract the dependency -- Interface
interface ProductRepo
{
Product getById(String productId)
}
class SqlProductRepo implements ProductRepo
{
getById(String productId){
//concrete details for fetching the product
}
}
class ProductRepoFactory
{
public static ProductRepo create(String type){
if(type.equals('mongo')){
return new MongoProductRepo();
}
return new SqlProductRepo();
}
}
class PaymentProcessor
{
public void pay(String productId)
{
ProductRepo repo = ProductFactory.create();
Product product = repo.getById(productId);
this.processPayment(product);
}
}
After implementing Dependency inversion, we had pretty much abstraction as dependency, however, there are still some coupling, this is where dependency injection comes in.
Dependency Injection A technique in which a component does not have to bear the responsibility of creating its one dependencies. Simply, the technique that allows the creation of dependent objects outside of a class and provides those objects to a class. There are several approaches of doing this.
- Public setters
- Constructor Injection
class PaymentProcessor
{
public PaymentProcessor(ProductRepo $repo){
this.repo = repo;
}
public void pay(String productId)
{
Product product = this.repo.getById(productId);
this.processPayment(product);
}
}
ProductRepo repo = ProductFactory.create();
PaymentProcessor paymentProc = new PaymentProcessor(repo)
paymentProc.pay('123')
Manual DI is complex and hence we need a better approach, which is the Inversion of Control (IoC) principle .
Inversion of Control (IoC) Helps to create large systems by taking away the responsibility of creating objects. This is the design principle in which the control of object creation, configuration and life cycle is passed to a container or framework. The object creation, configuration and life-cycle is inverted from the programmer to the container. Eg. Spring Bean.
Recap: DIP, DI nd IoC
Demo: Applying the DIP
- Decoupling components
- Improving testability
- Payment Component Highly coupled with EmployeeFileRepository and EmailService In the constructor, it is creating its own dependencies. Also sendPayments method of the class is using static calls which is also another sign though we don't new up. Questions to ask while refactoring?
- What if a particular client does not want to read employees out of CSV file, what's going to happen if you want to read from SQL database?
- What happens if a particular client does not want employees receive emails but text messages or notifications?
The way this component is coded makes it difficult to modify behaviour at runtime
- Testing is also affected. The main goal is to test the payment class where the total amount of service paid is equal to the sum of salaries of employees. We don't care about where the employees are retrieved from, we don't care about how they are notified, these problems are caused by coupling
- Fixed the Design with DIP Right now our high level component which is PaymentProcessor on low level module(Email, FileStorage). We need to break the dependency, both should depend upon abstraction.
Time to recap some few takeaways
- Technical Debt: The silent killer of software projects
- Coupling: The main reason of technical Debt(Code Rigidity and Frigidity)
- SOLID: Tackling coupling and promote designs that evolve and grow over time The SOLID principles are the foundation of clean system design
- Tools for writing clean code: Pyramid of clean code
- SOLID -> Design Pattern -> TDD -> Continuous refactoring
- Remember that you always have to pay your technical debt, otherwise, it will grow out of control and it will lead your project to a halt.
Abstraction provides great advantages since it makes applications easier to maintain, test and extend.
As developers we are either over-abstractors or over-abstractors. Using principles, will guide us to find the right balance of abstraction.
No matter how careful we are, there is the possibility of LEAKY ABSTRACTION.
With Mechanics of Abstraction such as Design Patterns, Interfaces, Inheritance, Layering, we will be able to get Abstractions just right.
It is about simplifying Reality, ignoring extraneous details so as to focus on what is important for a purpose. Basically, it's about hiding details to work with. Everything is an abstraction when dealing with computers.
Abstraction in Programming Technically, we could go the bits of current that exist at the hardware level but with programming, at its low level, we have machine instructions. Computer Languages are abstractions for dealing with these machine instructions. These provides as for humans to easily interact ith systems without having to know the details of the process architecture. Up another level is the language primitives and constructs which are abstractions for dealing specific pieces of computer memory. Up next is the Programming for user to deal with things at higher level through an interface which could be the web,mobile, desktop or command-line. At this level developers focus on techniques and code construct that makes application easier to maintain, extend and test. These take several forms and some common abstractions are layering, wrappers and methods,and interface and inheritance.
Layering
- UI layering: MVVM, MVC
- Application Layering: Separate Responsibilities
Wrappers and Methods
- Facade Pattern: Wraps complex system
- Method: Clear parameters and return value to wrap a complex process
- Component: Wrap complex process into properties and methods Eg: URL class to easy work on a given url.
Interface and Inheritance
- Interface: No worries about details, provide the contract
Abstraction is awesome with benefits like maintainability, extensibility and testability.
- With Layering we know where to go when there is a problem.
- With wrappers, we can work with higher modules without knowing the ugly details
- With interfaces, we can easily drop new implementations without modifying existing calling code and easily drop test objects for testing our code.
Abstraction can be awful
- Complexity
- Confusion
- Debugging complexity
Over or Under Abstractor which is the natural state of developers without external factors
They prefer to build highly extensible systems. These people usually say we'll have a good use for this in the future Build overly complex and difficult to maintain systems. These people:
- When building UI: They prefer to download the latest UI framework
- When coding an application, focus on: the current deliverable features
- When need to share state, they prefer to: create a state manager object
- When getting data from SQL database, they rather: raw query like SELECT from Customers
- When instance of object is needed, they would rather: raw instantiation(new Logger())
They prefer to keep things simple, but are rigid and difficult to maintain. These people:
- When building UI: They prefer to use what comes "in the box" with my developer tools.
- When coding an application, focus on: all the cool stuff the application could do in the future
- When need to share state, they prefer to: use a global variable
- When getting data from SQL database, they rather: set up ORM layer to manage customer entities
- When instance of object is needed, they would rather: use service locators (DependencyResolver.get(ILogger())
There is no right or wrong response, what is right in one environment will be wrong in another.
There is always a pendulum effect in architecting software. And thats why having both abstractors helps. The over-abstractor helps the under-abstractor get things Just Right. Same as the over-abstractor helps the under-abstractor to get things Just Right. Understand your environment which means to know
- Yourself: Whether you are over/under abstractor
- Tools: PL, Coding techniques
- Infrastructure: Hardware and the OS the app runs on
- Business: The people using the application
- Team: Decision making on dev process.
Abstractions are compelling but they do have drawbacks. Here are some principles to get our Abstractions JUST RIGHT.
This is usually meant for Under-Abstractors. When we need to code and the functionality is similar to a code we coded before, it is tempting we copy, paste and edit few places. This is easy but difficult to maintain. Instead, we keep common code in a shared location
Code Demo: Our base project has many duplicates action hence introducing Interface and Inject into method to reduce code duplication
This is also for under-abstractors. This means we want to keep a different functionality of application into separate methods, classes, or projects. For instance, we want to keep data access from the business logic. This is related to SRP principle, the "S" in SOLID principle. This brings the concentration on making sure that our classes and methods do one thing.
Here are some techniques we could use:
- Split functionality into separate methods/classes
- Add layering to separate responsibilities(Presentation, Business Logic, Data Access, Data Storage)
- Use wrappers to isolate functionality
- Dependency Injection to get reference to objects rather than creating it
Code Demo The
fetchData
has the responsibility to choose the type of Repository before fetching the data and displaying it. The main concern ofApp
class is displaying the data. So we moved setting up the repository to factory class.
This advice is for Over-Abstractors. We want to code for our current needs not for future speculations. This does not mean we don't think about the future, we do but we don't implement it yet. This tells us if we need abstraction, it tells us how far we can go.
This is for Over-Abstractors. To make it less insulting, it could be called Keep It Short & Simple
or Keep It Simple & Straightforward
.
This tells us if we need abstraction, it tells us how we can keep
This is for Over/Under-Abstractors. Before building something, we should check to see what is available. For Over-Abstractors: They like to build things to solve specific problems. For Under-Abstractors: They shy away from external frameworks and libraries.
Joel Spolksy stated that All non-trivial abstractions, to some degree, are leaky. Abstractions fail. Sometimes a little, sometimes a lot. There's a leakage. Things go wrong. It happens all over the place when you have abstractions.
Details that are not completely hidden by abstraction. When the developers don't see the details to understand the details of the underlying abstraction.
Interface Wrappers Layers
Add abstraction as you need it(not before). Add Repository: Connecting to different data sources. If you are not swapping a data source often there is no need for a repository.
Designing object-oriented program means creating interactions between involving objects and that usually create dependencies among the interacting objects. The dependencies could sometimes lead to designing application which is hard to maintain and extend. In this article, we would look at designing loosely-coupled programs with the help of dependency injection.
The diagram above is a typical monolith application that fetches data from a data source and renders that to a certain user interface, in this case, a web browser. First, the application is structured to contain View
-- the actual user interface elements, in this case, a list of items displayed in the browser. It also has a Presentation
layer that contains logic for driving our UI. Here, anytime the web browser loads, a method call is made to retrieve all people and populate the browser with such data. Then we have our Repository layer
, here it is called PersonRepository and this is a repository that's responsible for interacting with Service which could be a data store. And the bottom layer is the Service
layer, helps to provide data for the application, it can database, API or any data store. Currently, the application service uses a hardcoded array of data.
Let's start diving a little bit deeper into each of these layers.
<?php
namespace testapp\sharedObjects;
final class Person
{
public $name;
public $gender;
public $age;
public function __construct(string $name, string $gender, string $age)
{
$this->name = $name;
$this->gender = $gender;
$this->age = $age;
}
}
Above is a sample object that could be considered as a data transfer object as it provides a structure of how a Person Entity should be and shared across various layers. In the end, it makes it easier to model either incoming data from the outside world into a structure the application can work with or create an entity that could be persisted in the application. It's a very simple class with three properties - a name, gender, and age.
NB These properties could be value objects instead of primitives but for brevity, we would maintain the primitive types.
<?php
namespace testapp\services;
use testapp\sharedObjects\Person;
final class PersonService
{
public function getPeople(): array
{
return [
new Person("Oliver Mensah", "Male", 27),
new Person("Olivia Ennin", "Female", 22),
];
}
}
The service has a method that creates an entity object and eventually persists it. It also has a method to return the available entities.
<?php
namespace testapp\repositories;
use testapp\services\PersonService;
final class PersonRepository
{
public function __construct()
{
$this->personService = new PersonService();
}
public function getPeople(): array
{
return $this->personService->getPeople();
}
}
The repository helps to interact with the service. Here it reads the data and then presents the results to the presentation layer.
<?php
namespace testapp\presentation;
use testapp\repositories\PersonRepository;
final class UserModel
{
public function __construct()
{
$this->personRepository = new PersonRepository();
}
public function getPeople(): array
{
return $this->personRepository->getPeople();
}
}
Here, we have the presentation layer that contains all of the presentation logic for driving our UI.
<?php foreach ($people as $person): ?>
<li><?php echo $person->name?></li>
<?php endforeach;?>
use testapp\views\Template;
use testapp\presentation\UserModel;
$userModel = new UserModel();
$indexPage = new Template("./testapp/views/pages/index.php");
$indexPage->people = $userModel->getPeople();
echo $indexPage;
This accepts the incoming request and calls a method from the presentation layer to render the UI by basically listing users' name in the browser.
The above structure might have a pretty good design since we have good separation of concerns between our layers. But if you take a closer look, the repository right now has a direct reference to the service. Anytime, a repository class is instantiated, the service is automatically newed up. This means, anytime we need up the PersonRepository the data should come from the PersonService. This makes the repository tightly coupled to the repository to the service thereby taking the responsibility for creating and managing the lifetime of the service.
The presentation layer which drives the UI too is tightly-coupled to the Repository and this is where the real issue comes in. Indirectly, it makes the View tightly coupled to the service as well. And in that case, if you want to have different service say load data from a CSV file or get data from an API, the Presentation layer now takes the responsibility to know which repository is needed before making a call to the such a service. In code this would look as shown below;
//.. namespace and imports here
final class UserModel
{
public function __construct()
{
switch($service){
case 'raw': $this->service = new PersonRepository();
break;
case 'csv': $this->service = new PersonCSVRepository();
break;
case 'sql': $this->service = new PersonSQLRepository();
break;
}
}
public function getPeople(): array
{
return $this->service->getPeople();
}
}
This is really painful, the presentation is not meant to do such work. Its responsibility is to send UI actions to data storage through a repository.
We have gotten to know that our application has a tightly-coupled code that When the application wants to switch to a different data source, the cyclomatic complexity increases. ANd that is what should be avoided.
To make sure each layer performs its single responsibility, "someone" else must take up the responsibility of knowing what should be done at a certain part of the application. And Dependency Injection comes can help in achieving that. Next, we will look at Dependency Injection, what it is and how that can help in creating loosely-coupled code.
When you google for the meaning of dependency injection, there are so many definitions but to keep it pretty much simple, it is a set of software design principles and patterns that enable software practitioners like yourself to develop loosely-coupled code. And below are the various means that dependency injection can be implemented;
- Constructor injection
- Property injection
- Method injection
- Ambient context
- Service locator
In our case, we can apply constructor injection to resolve most of the issue since the tight coupling happens in most of the class constructors. But the question then becomes which of the services should be injected? And currently, they are PersonRepository, PersonCSVRepository, and PersonSQLRepository. And there could be other services in the future. So instead of injecting specific class implementations like;
//.. namespace and imports here
final class UserModel
{
public function __construct(PersonRepository $personRepo,PersonCSVRepository $csvRepo, ... )
{
switch($service){
case 'raw': $this->service = $personRepo;
break;
case 'csv': $this->service = $csvRepo;
break;
case 'sql': $this->service = $sqlRepo;
break;
}
}
// ...other methods
}
Even though the responsibility is shifted from the presentation layer, however, the application is limited in terms of extending to other services. In this case, injecting an interface that establishes class contracts can help solve the issue.
An Interface provides solutions to a lot of the problems that we've been looking at up to this point. Conceptually, an interface is a contract that defines a set of methods that have to be implemented by whatever class implements the interface. It's a formalization of a general design term called a class contract, though the idea of a class contract encompasses not just the set of methods, but also the behavior of those methods. In other words, classes that implement the contract have to behave in certain ways, not just implement a set of functions and in this case, talk to various services in the application.
For now, most of the repositories just retrieve information from the service and populate it to the UI. We could just work with that functionality or add more to the interface.
<?php
namespace testapp\repositories;
use testapp\sharedObjects\Person;
interface IPerson
{
public function getPeople(): array;
public function addPerson(Person $newPerson);
}
We can now just inject the interface and any service that the application needs can be worked with once its repository implements the defined interface.
<?php
namespace testapp\presentation;
// ...imports here
final class UserModel
{
public function __construct(IPerson $repository)
{
$this->repository = $repository;
}
public function getPeople(): array
{
return $this->repository->getPeople();
}
}
With the interface defined, all you need to do is to allow your repositories that interact with the service implements it, that's the interface. Now, the repositories implementation would look like below;
<?php
namespace testapp\repositories;
use testapp\services\PersonService;
use testapp\sharedObjects\Person;
final class PersonRepository implements IPerson
{
public function __construct()
{
$this->personService = new PersonService();
}
public function getPeople(): array
{
return $this->personService->getPeople();
}
public function addPerson(Person $person)
{
}
}
<?php
namespace testapp\repositories;
use testapp\sharedObjects\Person;
final class PersonCSVRepository implements IPerson
{
public function __construct()
{
$this->csvService = new CSVService();
}
public function getPeople(): array
{
return $this->csvService->getPeople();
}
public function addPerson(Person $person)
{
}
}
<?php
namespace testapp\repositories;
use testapp\sharedObjects\Person;
final class PersonSQLRepository implements IPerson
{
public function __construct()
{
$this->sqlService = new SQLService();
}
public function getPeople(): array
{
return $this->sqlService->getPeople();
}
public function addPerson(Person $person)
{
}
}
We can implement as many repositories depending on our services. We could also see the services are also tightly coupled to the repositories and this is much of an issue when you want to test the code. During testing, you may not get the actual service running hence mocking it would be the best option.
To mock a service, dependency injection plays a key role. And henceforth, the repositories can now work with services through injection as shown below;
<?php
namespace testapp\repositories;
use testapp\services\PersonService;
use testapp\sharedObjects\Person;
final class PersonRepository implements IPerson
{
public function __construct(PersonService $service)
{
$this->personService = $service;
}
public function getPeople(): array
{
return $this->personService->getPeople();
}
public function addPerson(Person $person)
{
}
}
<?php
namespace testapp\repositories;
use testapp\sharedObjects\Person;
final class PersonCSVRepository implements IPerson
{
public function __construct(CSVService $service)
{
$this->csvService = $service;
}
public function getPeople(): array
{
return $this->csvService->getPeople();
}
public function addPerson(Person $person)
{
}
}
<?php
namespace testapp\repositories;
use testapp\sharedObjects\Person;
final class PersonSQLRepository implements IPerson
{
public function __construct(SQLService $service)
{
$this->sqlService = $service;
}
public function getPeople(): array
{
return $this->sqlService->getPeople();
}
public function addPerson(Person $person)
{
}
}
With these changes, we can easily mock any services during unit testing of the code.
You can use any kind of tool out there to test your code. In this case, Mockery is used.
<?php
use PHPUnit\Framework\TestCase;
use testapp\presentation\UserModel;
use testapp\sharedObjects\Person;
class UserModelTest extends TestCase
{
public function testGetPeople()
{
$repository = \Mockery::mock('testapp\repositories\IPerson');
$repository->shouldReceive('getPeople')
->once()
->andReturn(
[
new Person("Oliver Mensah", "Male", 24),
new Person("Geddy Addo", "Female", 21)
]
);
$userModel = new UserModel($repository);
// Verify
$this->assertEquals(
[
new Person("Oliver Mensah", "Male", 24),
new Person("Geddy Addo", "Female", 21)
], $userModel->getPeople());
}
}
In this case, we are just testing the getPeople
method by mocking our service.
We had manually be managing dependencies by abstracting to constructor parameter and making someone else manage the responsibility of creating the object and its life time. This could be difficult sometimes hence DI containers to the rescue. They help to resolve dependencies. At the core of the DI containers, thus when you strip away all the nice things to have and trim it down to what is really essential, DI container is just big array of factories. Or better say this class is mapped to its factory. Hence it is just recursive calls to bind classes to factories. It has great features like
- auto-registration: Container will search for objects that will use dependencies and catalogue them.
- auto-wiring which has the ability to automatically bind a class to factories without explicit binding.
- Lifetime management: Container is responsible for managing the lifetime of the object such as using same objects or creating new ones when requested.
Simple DI container.
<?php
class Container
{
private $bindings = [];
public function set($abstract, callable $factory)
{
$this->bindings[$abstract] = $factory;
}
public function get($abstract)
{
return $this->bindings[$abstract]($this);
}
}
$container = new Container();
$container->set(User::class, function(Container $c){
return new User($c->get(SessionStorage::class));
});
$container->set(SessionStorage::class, function(Container $c){
return new SessionStorage();
});
$user = $container->get(User::class);
$user->setLanguage("aaa");
echo $user->getLanguage();
Depending on your language you should look for DI Container you can use in your projects.
Here, we looked at applying dependency injection rule, a good principle in writing effective production-quality object-oriented systems. We familiarize ourselves on how to structure code to be highly maintainable, extensible and easy to modify by avoiding common pitfalls. And thus below should be the common takeaways from this little piece of work;
- Layering of Application
- The problem of Tight Coupling
- Dependency Injection(DI) to the Rescue
- Interfacing the Repositories
- Ease Code Testing by Mocking Services.
Code that makes testing easy. Most software devs dont write code for testability hence they struggle with software practices like unit testing and test driven development. Best practices on object construction, working with dependencies and managing application states
- making software components interchangeable or replaceable makes it easier to test individual components
- Easier to maintain later
- Approach to unit testing and tdd practices
For SWE:
- Very little testing
- Ineffective testing
- Inefficient testing
Why these:
- It's too hard to test
- Not enough time to test
- Not my job to test as SWE
Why write test
- Reduce bugs
- Reduce cost
- Improve design
- Documentation
- Eliminate fear
Types of Tests:
- What they are testing
- Unit test
- Integration
- Component
- Service
- UI
- Why they are being tested
- Functional
- Acceptance
- Smoke Test
- Exploratory
- How they are being tested
- Automated
- Semi automated
- Manual
With the code in 1TestableCode
folder, the bad code can be tested only by running the program manually.
The Good code got tested automatically by creating its own class for it.
The idea is to take code that is difficult to test to a testable phase.
Code is said to be testable if it is written in a way that makes it possible to write unit tests for the code. A seam is a way to make code testable by allowing for the use of test doubles— it makes it possible to use different dependencies for the system under test rather than the dependencies used in a production environment.
Dependency injection is a common technique for introducing seams. In short, when a class utilizes dependency injection, any classes it needs to use (i.e., the class’s dependencies) are passed to it rather than instantiated directly, making it possible for these dependencies to be substituted in tests.
Problems without Seams:
- Cannot pull apart code
- Cannot connect test harness
- Cannot replace dependencies with test doubles
- Cannot test in isolation Symptoms
- Keep eye on new keyword
- Keep eye on static method calls
- Direct coupling Solution Create seams by
- Program to interface
- Decoupling dependencies
- Inject dependencies by constructor injection
Goal is to get the invoice details and print them.
This code for PrintInvoiceCommand
class would be difficult to test since there is dependency on the database
-> Database -Access to DB
PrintInvoice -> Printer -Access to Printer -> DateTime -Access to OS Clock
Code rewritten to make it testable.
Interface -> Database -Access to DB
PrintInvoice Interface -> Printer -Access to Printer Interface -> DateTime -Access to OS Clock
During testing you can replace the dependencies with mock objects
//TODO:: Find out how to test a method to see number of times another method was executed in that method.
Here learned to create seams in code so classes can be tested in isolation by decoupling dependencies, program to interface rather than implementation
Learn about ways to construct objects that are easily testable. In all, this is about
- constructors
- problems that occur when we mix construction of objects and their behavior
- Identify symptoms of object construction that are difficult to tests and refactor them
Constructing and Testing objects are different tasks. When constructing an object, we are assembling the components of the object in such away that when we are done, we have a fully functional object to perform some tasks.
- Constructors are methods used to build instances of our class.
- Inside the constructor, we execute code necessary to prepare the object for use.
However, its quite common to see developers doing additional work inside the constructor;
- Set up dependencies
- Talk to external services
- Execute initialization logic
- Execute application logic This creates problems
- Tight coupling for setting up dependencies
- Difficult to test the logic in constructors
- Logic is difficult to test
Symptoms for such problems
- The keyword new: Indicates tight coupling to a dependency. But its fine with value objects
- Logic in constructor: Like if-else, switch statement.
- Any code aside than assignment of values is an indication
Solution
- Inject dependencies and assign them to properties
- Avoid logic in constructors such as conditional, or call methods meant for initialization from constructor,etc.
- Use well established patterns like factory, builder, IoC/DI
- Don't mix construction and logic
- Separate injectables(Services that implements interfaces) and newables(Object graph like entities and value objects)
Injectables vs Newables Injectables can ask for injectables in its constructor but not newables. Newables can ask for newables in its constructor but not injectables
Hard:
- Tight coupling btn PrintInvoiceCommand and InvoiceWriter class
- Additional work on the InvoiceWriter construction like setting up page-layout and setting up ink, hence mixing application logic with construction
Easy
- Let constructor of the InvoiceWriter which is Injectables accept injectables hence we replace Invoice which is newable with PageLayout which is injectable. Object construction must follow Newable and Injectable rules
Learn to work with dependencies that makes testing easier.
General principle of OOP proposed by The principle states that a module should have access to a certain information. In other words, a software component or an object should not have the knowledge of the internal working of other objects or components.
This can be summed as only talk to your friends, don't talk to strangers.
Symptoms
- Series of appended methods
Solutions
- Talk to immediate dependencies; thus inject the dependencies we need
The PrintInvoiceCommand class has a dependency on container and then uses the container to resolve all the dependencies needed for the class to function as expected. This makes testing difficult. The IoC container now acts as a service locator
To fix this; We refactored the PrintInvoiceCommand to ask for the dependencies needed directly.
Follow the law of Demeter for testability
Learn about global states, it problems in terms of testing, identify when app is having global state and then refactor that.
Global states Also,known as application state is the set of variables that maintain the high level state of software application. It might implemented in our of the two states
- Single Application state Object like PHP HTTP Status code
- Global variables: Can be accessed anywhere in the application.
No matter their implication they have only one instance in memory.
Why do we do so: To share data btn two or more classes.
Problems
- Coupling to global state
- Difficult to set up tests
- Prevents parallel tests
Usually unusual changes to the state value is termed as spooky action at a distance.
Symptoms
- Global variables
- Static methods and fields
- GoF Singleton
- Units tests that run parallel fail but works in isolation
Solution
- Keep state local if possible. if only one method then scope should be method, if more than one method in the class then scope should be the class, it should not evolve outside the class.
- Wrap static methods of third party libraries.
- Use IoC/DI singleton rather than GoF singleton
Learn to limit class to a single responsibility for testability
Symptoms for multiple Responsibilities
- Described with "and or "
- Class or method too large
- Many Injected dependencies
- Identify frequently changing class.
Solutions
- Look for multiple responsibilities in a class. Look for multiple reasons why the class will need to change, multiple people, different roles, multiple tasks in the same class
- Label responsibilities. Simple describes what is doing and ecompose that into SRP class
- Begin to learn to write testable code
- Learn to write effective unit tests
- Learn test-driven development
Any software project accumulates technical debts over time and without any refactoring, its likely to fail sooner or later. Refactoring is an essential skills for any software engineer adn this course gives you the ability to do so. You will learn how to convert clunky and difficult to maintain codes into elegant and flexible.
Software engineers main programming tasks are about reading, writing and changing existing(maintaining) codes.
Refactoring means taking old ugly code and changing it better without changing how it behaves.
Several questions about refactoring includes
Why, What, When not and When?
-
What Martin Fowler puts refactoring as a change made to the internal structure of a software to make it easier to understand and cheaper to modify without changing its observable behavior.
-
Why i. There is technical Debt(Can't we all code nicely and there would be no need for refactoring skills) People don't have enough experience(Junior, Mid-Level, Senior) and that has relation to code quality written(Low to high). Other factors could be lazy coding(sloppy solutions), tight deadlines.
ii. Without refactoring productivity decreases -
When Continuously. At least minor improvement when adding new features. And Boy Scout Rule(Leave the campgroud cleaner than you found it). The same can be applied to code; Leave the code cleaner than you found it. Follow guidelines to write cleaner code.
-
When not to refactor i. When current code doesn't work. ii. Deadlines must be met. iii. Not gold-plating(When code is in good shape and works fine)
Business is well served by continuous refactoring, yet the practice of refactoring must coexist harmoniously with business priorities.
Before you refactor, you actually need to know how to identify a problem. Code Smell is a surface indication that usually corresponds to a deeper problem in the system.
The pattern of refactoring Identify a Code Smell > Understanding why it is bad > Identify it in code > Fix it
The Refactoring Process Verify existing behavior > Ensure tests are valid and passing > refactor > Verify behavior again
Code Smell Taxonomy A way of grouping code smell and that was the work of Mika Mantyla and Casper Lassenius.
And they are as follows: Bloaters, Object-Oriented Abusers, Change Preventer, Couplers, Dispensable.
Large methods and classes
- Method Bloaters: 20+ lines
- Class Bloaters: Count responsibilities(SRP rule). 2+ responsibilities
- Occurs as software evolves and hence it accumulates over time.
- Implementing new features in that single places.
- Most Common bloaters Long parameter lists, long methods, contrived complexity, primitive obsessions, data clumps, large class
Demo project Online shop that sells food. It has a customer class, bunch of items to buy, and checkout-handler The checkout-handler helps to calculate the price of shopping list.
Calculating the prices involves looping over the items, get their prices, sum it up; check if a voucher is valid to change the price; checking for membership on delivery and finally return the total prices.
Then we have we have tests for this methods on many scenarios.
Issues with the calculateTotal method
- Long perimeter list
- Long methods
- Does many things
- As more features get added when paying for items, the method will blow up.
One or two is typically fine, 3 should make you think and 4 or more is already suffering from this code smell and needs refactoring
Long Parameter List Issues
- Hard to understand
- Acts like a magnet for even more arguments and code.
- Acts as harbour for other code smells: Long method, primitive obsessions and data clumps. Hence solving others would take away this long parameter list issue
How to fix Long Parameter List Fix Long methods, primitive obsessions and data clumps.
Contains too many lines of code. Aim for small methods; up to 10 lines or more but less than 20;
Long Method Issues
- Difficult to maintain
- Easy to break.
- Attracts even more code.
How to fix Long Method
-
Extract method: Splitting a method into smaller methods Here we extracted the one big method into three methods by delegation. Even after refactoring into methods with good names, we can take away the comments
-
Decompose Conditional This is similar a form of extract method but it focuses on if or switch condition. When the condition is too complex, we should extract them into methods or classes with descriptive names. Here we replaced our if conditions with function with descriptive name.
Code that achieves an objective but is too complex. An easier, shorter or more elegant way exists to achieve the same goal.
Contrived Complexity Issues
- Hard to understand
- Higher chance of breaking when changing
How to fix Contrived Complexity
- Substitute algorithm: Replace with simpler and elegant code. Here we replaced the two arrays with just one array. No need to store and later add, add where fetching the data.
Using primitive type way too much instead of object
Primitive Obsessions
- Major cause of long parameter lists
- Cause code duplication
- Not type safe and prone to errors
How to Fix Primitive Obsessions
- Preserve whole object: Passing in a whole object as parameter. Here we noticed that the customer can have membership and address, hence we used our already existing customer object and get the data using its getters.
- Introduce Parameter Object: Create an object, move primitives there and pass in the object. Here we created new class called Order to hold a customer and items.
Primitive Obsession: What We Didn't Cover
We can do more to prevent our code from being error prone. For instance, the membership string is a perfect example for enum; this way it prevents mistyping of each membership. Our voucher is a simple string but it could contain code, startDate and expiration date as well hence a class would be perfect. Here the business logic on voucher is simple but if it becomes complex, you should consider a class. Also, address could be more than single string. A class could be introduced.
They are group of variables which are passed around together(in a clump) throughout various parts of the program. They always or have to go together. Eg:
- Fill in email template. ("Mr", "John", "Doe")
- Booking. ("startTime", "endTime")
- Date and time for return flight. ("there", "back")
- time window for delivery. ("from", "to")
Data Clumps Issues
- Long parameter list
- Code duplication
Both data clumps and Primitive Obsessions often suggestion a specific problem: Missing domain objects. Hence you have bunch of primitive types that should be encapsulated into a class.
How to Fix Primitive Obsessions
- Introduce Parameter Object: Create an object, move primitives there and pass in the object. Example here is the delivery time window
- Combine Entities We learned that the customer entity always go together with order entity hence they could be combined. Formulating their relationship, an order has a customer or a customer has or or more orders.
A class that does many things(more responsibilities). A God class is a class that does almost everything.
Large class issues
- Very difficult to maintain
- Violates the SRP rule
How to Fix Large Class
- Extract class Split into several smaller classes. Here we splitted checkout handler into three classes.
- Methods and classes can shd shd be split
- Strive for the most simplest and elegant solution
- Too many primitives indicates you should introduce new classes
Demo code started with Long Primitive List which got solved with Long Method, Primitive Obsessions, Data Clumps. Then later solved other issues like Contrived complexity and large class
Refers to code that incorrectly applies object oriented principles. Examples: Conditional Complexity, Refused Bequest, Temporary field, Alternative classes with different interfaces.
Demo Project There is additional requirement to verify the age of the user buying the products. The Shop is expanding to different countries and it should be tracked where from the address of a customer ordering an item. So basically with the new changes, Order has reference to a Customer, A Customer has an Address and Address has a field Country.
A complex switch operator or a sequence of if statements.
Complex Conditional often means
- Missing Domain Objects
- Not using polymorphism
- Not using inheritance
Conditional Complexity Issues
- Starts simple but gradually harder to understand as logic is added.
- High likelihood of breaking
- Breaks OCP Here: Age checking before selling items
How to fix conditional complexity
- Replace with polymorphism: Here we added the age checking to the country classes and made a caller used it.
Bequest is the act of giving or leaving personal property by a will. Scenario: You can your property out for inheritance and the other personal may refuse. And that how Refused Bequest is Refused Bequest is when subclass inherits fields and methods it doesn't need.
Refused Bequest Issues
- Unwanted Inheritance
How to Fix Refused Bequest
- Rename methods
- Push Down: Introduce new class to hold the unwanted methods Here we rename a base class method to give meaning to all items
Fields that have values only under certain conditions.
Occurs when two classes are similar on the inside but different on the outside. The code are similar or identical but the method name or signature are different.
Issues
- Not DRY - code is duplicated ith just minor variations
- Lack of common interface for a closely related classes.
Solution
- Abstract class
- Interface
Here: We made used of the class that does the conversion and deleted the method the does the same thing as the class
Business logic grows and that result in a lot of if-else statements. Every growing if statement should be replaced with delegation or polymorphism.
When code change in one place results changing code in many other places Examples: Divergent Change, Solution sprawl & Shotgun Surgery, Parallel Inheritance Hierarchies.
Get the currency conversion rate from online service instead of hard-coded values; The Code smell with this code: Divergent of Change,
Changing several unrelated things within the same class
Issues
- Requires more typing
- Requires additional knowledge of what to change where.
Fixing
- Extract method: Split method into several methods
- Extract class: Split into several classes
NB: New Requirement: Print currency rates
Questions to ask:
- Do we have a situation where we will change the class for different reasons? The answer is yes, this converter class does not only does the conversion but it also an expert in making http calls. Thats a lot of responsibilities to bear.
Use extract class to take the http functionality to a new class.
Solution Sprawl A feature spread across multiple classes. Shotgun Surgery The process of looking for all the repeated functionality in multiple classes to meet the new requirement. Solution sprawl leads to shotgun surgery and mostly used interchangeably Issues
- Difficult to remember all the interconnected places
- New team members are likely to make mistake by forgetting to update one of the places
Fixing
- Combine into one: Consolidate the responsibility into a single class
Changing in one inheritance tree causes a change in other sub trees. A special case of shotgun surgery
Changing a software that requires multiple changes.
Code smell that result in tightly coupled classes.
Coupling refers to the degree to which classes depend on each other. It could be Tight or Loose
Tight Coupling Cascade changes due to change made. Thus classes are so tied that you cannot change one without changing the other. Loose Coupling Change in one class requires no or minimum changes in other classes. List of Couplers: Feature envy, inappropriate intimacy, excessive exposure, message chains and middle man
Class uses methods or accesses data of another class more than its own.
Issues
- Hard to understand code that logically belongs elsewhere
Fixes
- Move methods to where it is needed most
Code: Here we have a phone class and the customer class calls several methods of the phone class to achieve certain functionality.
A class knows too much of or has to deal with internal details of another class
Code: A voucher class with public field(code) which could be accessed and assigned values outside the class. A business requirement changed and there has to be a validation on there code. What happens is wherever the code field is used we have to find it to do the validation. Best is to create private field and set a value on it and can check for validation in that single place
Happens when a class or a module exposes too many internal details about itself. Example of good encapsulation and minimum exposure: Calendar.getInstance.
Code: In simpleCurrencyConverter class, when you don't want to know about where to get the rates from, you only need to convert hence the rates access is of less advantage to you.
Code that calls(chains) multiple methods to get to the required data.
Customer.getAddress.getCountry.toString();
Issues
- Learn class organization
Code: Getting a customer address and a country, checking if the customer is not from the US before doing the currency conversion.
All the class does is delegation to one class
Issues Every class has a cost and you shd know h ow it brings value, if there is no value in that delete it. MiddleMan Patterns Delegation can exist for a purpose Facade, proxy, adapter
Codes that are not needed and can be removed. Eg. Bad Comments, Dead code, duplicate code, speculative generality, lazy and data classes.
They should not compensate for bad codes. Refactor code that you don't need comments in the first place.
Unused code
Same code writing in multiple places Code with DRY in mind
Code created just in case to support anticipated future features that never get implemented
Code with YAGNI(You ain't gonna need it) in mind
Lazy class has no functionalities Data class used as containers for data. Data class are code smell that are debateable. That is where the DTO pattern comes in. This is a like data carrier btn processes.
You should write a book wai