Beginner’s Guide to Software Design Principles
Unlock the secrets of writing meaningful code to elevate your programming skills
Welcome to the world of software development, where the clarity of your code can make or break your project! Understanding software design principles is crucial whether you’re just starting or looking to learn the basics.
In this first part of our series, we’ll dive into the practical side of clean coding — starting with how you name your variables, functions, and classes. Think of these names as the first clues you give to someone else (or your future self) about what your code does. Mastering this, you’re on your way to becoming a coding wizard.
We’ll also dive deep into fundamental principles that every developer should know, like DRY, KISS, and YAGNI (I’ll explain it later). These might sound like jargon now, but by the end of the article, you’ll see how they can become your best friends in coding.
If you want to check out the more advanced one about the clean coding, you can check out the part two of the series by the link below:
The Art of Naming in Coding
Sometimes, naming things becomes the most challenging part of clean code. Most people use single or double-letter variable names like A
, v
, d
, mp
, etc., or generic variable names like flag
, value
, map
, arr
, etc. These variable names might be easy to write, but they make the code difficult to read, and debugging takes longer.
Effective naming in your programs is critical. It improves readability and maintainability and can even reflect professionalism in your code.
Use Intention-Revealing Names
Choose names that reveal why a variable exists, what it does, and how it is used. If a name requires a comment to understand its purpose. It’s not revealing its intentions clearly.
// Poor choice
int d; // elapsed time in days
// Better choice
int elapsedTimeInDays;
Name Functions as Verbs
Functions usually perform actions, so their names should include a verb that describes what they do, followed by a noun that describes what is being manipulated.
// Not descriptive
void value(int order) {}
// Descriptive
void calculateTotalOrderValue(int order) {}
Name Classes as Nouns
Since classes often represent objects or concepts in your application, their names should be nouns. This makes your code read more like a series of statements about the model of your application.
// Not quite clear
class HandleData {}
// Clear and to the point
class CustomerProfile {}
Use Meaningful Distinction
Distinguish names in a way that explains their differences. Avoid using numbers or vagus distinctions like data1
, data2
, etc., which don’t provide information about what the variables represent.
// Unclear distinction
class DataProcessor {
void processData1() {}
void processData2() {}
}
// Clear distinction
class DataProcessor {
void processCustomerData() {}
void processSupplierData() {}
}
Use Pronounceable Names
Names should be easy to pronounce and remember. This facilitates verbal communication among developers, especially during code reviews or pair programming.
// Hard to pronounce
class DtaRcrd102 {}
// Easy to pronounce
class CustomerRecord {}
Use Searchable Names
Choose names that will be easily searchable throughout your code base. Single-letter names can be problematic in this regard, except for local variables inside short methods.
// Hard to search
int e; // stands for "errorCount"
// Easy to search
int errorCount;
Avoid Encodings
Avoid adding type or scope of information to names (Hungarian notation). Modern IDEs provide this information, so adding it to names adds unnecessary complexity.
// Encoded
int iErrCnt; // Integer Error Count
// Clean
int errorCount;
By following these naming rules, your code will be easier to understand and maintain and more enjoyable to work with for everyone involved. These simple yet effective naming strategies ensure your codebase remains clean and professional.
Designing Good Function
Functions are one of the most essential parts of writing code. They make the code reusable, easy to read, and maintain. One should look at the function name and understand what that function does. If they want to know more, they should be able to skim through the function to get details at a lower level. For more lower-level information, they should look at the implementation of the function called inside it.
Should Be Small
The function should be short. A good rule of thumb is that the functions should be small enough to be viewed without scrolling too much in your IDE. This makes them easier to read, understand, and debug.
// Good practice
void updateCustomerRecord(Customer customer) {
validateCustomer(customer);
saveRecord(customer);
}
Should Do Just One Thing
A function should have one responsibility and perform it well. This aligns with the Single Responsibility Principle and makes your function more robust and more accessible to test.
// Poor practice - does too much
void processUserInputAndSave(String input) {
String processedInput = input.trim();
validateInput(processedInput);
saveInput(processedInput);
}
// Better practice - each function does one thing
void processAndSaveUserInput(String input) {
String processedInput = processInput(input);
saveProcessedInput(processedInput);
}
String processInput(String input) {
return input.trim();
}
Should Have Fewer Arguments
“The ideal number of arguments for a function is zero (niladic). Next comes one (monadic), followed closely by two (dyadic). Three arguments (triadic) should be avoided where possible. More than three (polyadic) requires very special justification — and then shouldn’t be used anyway.”
- Robert C. Martin (Uncle Bob)
The fewer parameters a function requires, the easier it is to use and understand. Aim for no arguments; keep it to two or fewer.
// Too many arguments
void createMenu(String title, String description, boolean isVisible, int width, int height) {}
// Refactored to fewer arguments by using an object to encapsulate parameters
void createMenu(MenuOptions options) {}
Should Not Have Side Effects
A function should not produce unexpected changes outside its scope, such as modifying global variables or external data states. This improves predictability and reliability.
// Has side effects
void checkUserAccess(User user) {
user.lastAccess = new Date(); // Modifies user object
}
// No side effects
boolean isUserAccessAllowed(User user) {
// Perform check without modifying the user object
return user.hasAccess();
}
Designing Good Classes
A class should keep all data attributes and utility functions private. Only the functions that are supposed to be exposed should be kept public.
Organized and Encapsulated
Classes should encapsulate their data and behaviors, exposing what is necessary to the outside world. This hides the internal implementation details and protects the class’s integrity.
// Good encapsulation
class Account {
private double balance;
double getBalance() {
return balance;
}
void deposit(double amount) {
if (amount > 0) {
balance += amount;
}
}
}
Should Be Small and Do Just One Thing
Classes, like functions, should have a single responsibility. This makes them easier to manage and less prone to errors.
// Poor practice - multiple responsibilities
class UserManager {
void addUser(User user) {}
void sendUserEmail(User user) {}
}
// Better practice - single responsibility
class UserRegistration {
void registerUser(User user) {}
}
class UserNotification {
void notifyUser(User user) {}
}
Small Number of Instance Variables
The fewer instance variables a class has, the less complex it is. This reduces dependencies and makes the class easier to maintain.
// Too complex
class Product {
String name;
String description;
double price;
double discount;
String category;
List<String> tags;
}
// Simplified
class Product {
String name;
double price;
ProductDetails details;
}
Following the principles above will help ensure your functions and classes are well-designed, maintainable, and robust.
Basic Principles of Clean Coding
When starting out in the coding world, it’s essential to grasp some foundational principles that guide professional developers in creating high-quality, maintainable code. Let’s explore three of the most impactful principles: DRY, KISS, and YAGNI.
Don’t Repeat Yourself (DRY)
Code duplication can make code difficult to maintain. Any change in logic can make the code prone to bugs or make code changes difficult.
// Violating DRY
public void calculateBonus(double salary) {
double bonus = salary * 0.1;
System.out.println("Bonus: " + bonus);
}
public void calculateSalaryIncrease(double salary) {
double increase = salary * 0.1;
System.out.println("Increase: " + increase);
}
// Adhering to DRY
public double calculateTenPercent(double amount) {
return amount * 0.1;
}
public void displayCalculationResults(double amount, String type) {
System.out.println(type + ": " + calculateTenPercent(amount));
}
By implementing the DRY principle, code repetition can be reduced. You should aim to have a single source of truth for every piece of knowledge, which means avoiding duplicate code. This makes your code cleaner and less prone to errors because changes made in one place are reflected everywhere that piece of code is used.
Keep It Simple, Stupid (KISS)
The KISS principle advises that simplicity should be a key goal in design and that unnecessary complexity should be avoided. This means that the simpler solution is usually better when there are two potential solutions. Simplifying your code enhances readability and reduces the chance of errors.
// Complicated method
public void printDayOfWeek(int day) {
switch (day) {
case 1: System.out.println("Monday"); break;
case 2: System.out.println("Tuesday"); break;
case 3: System.out.println("Wednesday"); break;
case 4: System.out.println("Thursday"); break;
case 5: System.out.println("Friday"); break;
case 6: System.out.println("Saturday"); break;
case 7: System.out.println("Sunday"); break;
default: System.out.println("This is not a valid day");
}
}
// Simplified using an array
public void printDayOfWeek(int day) {
String[] days = {"Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"};
if (day > 0 && day <= days.length) {
System.out.println(days[day - 1]);
} else {
System.out.println("This is not a valid day");
}
}
Do the simplest thing that could work. The code should be easy to read and understand without much thought. If it isn’t, then there is a prospect of simplification.
You Aren’t Gonna Need It (YAGNI)
YAGNI is a principle of extreme programming that states a programmer should only add functionality once necessary. In other words, do not write code thinking, “I might need this later.” It’s best to leave your code simple if it’s optional now.
There are two main reasons to practice YAGNI. First, you save time by avoiding writing code you do not need. Second, your code is better because you avoid polluting it with ‘guesses’ that are more or less wrong but stick around anyway.
// Unnecessary feature implementation
public class ProductFeatures {
public void applyDiscount(Product product, double discount) {
// Implementation
}
public void estimateDelivery(Product product) {
// Implementation might not be needed yet
}
}
// Adhering to YAGNI
public class ProductDiscount {
public void applyDiscount(Product product, double discount) {
// Just implement what's needed right now
}
}
Code maintenance is expensive and challenging. Always consider someone else as the maintainer and make changes accordingly, even if you’re the maintainer. After a while, you’ll remember the code as much as a stranger.
Next Steps
Ready to dive even deeper into the world of software design? Please take advantage of the second part of our series, where we’ll explore advanced software design principles, including Abstraction, Cohesion, and SOLID principles. These concepts will help you refine your coding skills and develop functional, well-architected, easy-to-maintain software.
Continue to Part Two:
Conclusion
In this first part, we’ve covered a lot, from naming conventions that clear the path for better understanding to the foundational principles of DRY, KISS, and YAGNI that keep your codebase clean and efficient.
Remember, coding is not just about getting the program to work — it’s about writing code other developers can read, understand, and maintain easily. Applying these basic principles will set you on the right path to becoming a proficient programmer who writes quality code.