Basic principles of Scrum
Agile methodologies were developed to provide a mechanism that facilitates adaptation to change in …
read moreIn the vast universe of programming, there are principles that, although not commandments, provide great guidance for those who aspire to write clean, maintainable, and efficient code. These principles not only represent best practices but are also the fruits of years of experience and learning from the developer community. Below, we’ll delve deeper into some of these principles.
Repetition is the downfall of the programmer.
This statement encapsulates the essence of the DRY principle - Don’t Repeat Yourself. It aims to reduce duplicities in our code as repetition leads to errors, inefficiencies, and difficulties in software creation.
Common violations of the DRY principle include practices like repeatedly copying and pasting code, implementing functions or methods that execute very similar or identical operations, and designing classes with notably similar structures.
# Incorrect approach
def calculate_rectangle_area(width, height):
return width * height
def calculate_circle_area(radius):
return 3.14 * radius * radius
# Using DRY Principle
class Shape:
def area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14 * self.radius * self.radius
Abstracting to avoid repetition is crucial, but you wouldn’t want to overcomplicate matters. Before abstracting, question whether the abstraction muddles the code or if it remains comprehensible. At times, a tad repetition is preferable if it aids clarity.
Simple is better than complex, and complex is better than complicated.
This is a variant of the philosophy behind KISS, a principle that champions simplicity in design and code over intricate and convoluted solutions. Simplicity is a desirable aim in software development, leading to cleaner code that’s easy to understand and maintain.
The key to implementing KISS is constant reflection and self-evaluation. Every time you’re about to write or design something, ask yourself: “Is there a simpler way to do this?”.
Suppose you want to filter even numbers from a list:
# Overboard Approach
def rectangle_area(width, height):
return width * height
def circle_area(radius):
return 3.14159 * radius * radius
# And so on for each shape...
A simpler and scalable solution uses classes and polymorphism:
# Using the KISS principle
class Shape:
def area(self):
pass
class Rectangle(Shape):
def __init__(self, width, height):
self.width = width
self.height = height
def area(self):
return self.width * self.height
class Circle(Shape):
def __init__(self, radius):
self.radius = radius
def area(self):
return 3.14159 * self.radius * self.radius
With this class-based approach, you have a cleaner structure that allows adding new shapes without needing to craft a new function for each one. It’s a design simplification rather than a code reduction. Practical Tip
Don’t be enticed by technical elegance or “smart” solutions. Often, the most direct and simple method is superior. When facing a problem, before diving into the code, take a moment to ponder and plan. Remember that what seems clear and straightforward to you now might not be for someone else (or even yourself) in the future.
The best code is the code you never write.
YAGNI, or You Aren’t Gonna Need It, is a principle that encourages developers to only implement functionality when it is necessary. It warns against over-engineering or adding features based on speculation about future needs.
Many developers fall into the trap of anticipating every potential future requirement, leading to bloated and over-engineered solutions. YAGNI suggests that you should only add functionality when there’s a definitive requirement for it.
Suppose you’re creating a blogging platform. While a tagging system or advanced search might be useful in the future, if the current requirements only specify basic blogging features, YAGNI would advise against implementing them at the outset.
# Over-Engineered Approach
class Blog:
def __init__(self):
self.posts = []
self.tags = []
self.advanced_search = []
# YAGNI Principle
class Blog:
def __init__(self):
self.posts = []
Always prioritize current, concrete requirements over speculative ones. When designing or coding, ask yourself, “Do I need this now? Is there a clear requirement for this?” If the answer is “no” or “not sure,” consider postponing that feature or functionality. Avoid the temptation to add “just in case” code. Remember, you can always add more later when it’s truly needed.
Divide and conquer.
This age-old adage holds profound meaning in software design, and the Separation of Concerns is its standard-bearer. This principle urges us to break down a program into distinct parts, where each has a uniquely and clearly defined responsibility.
Always bear in mind the singularity of responsibility and think of software in terms of autonomous modules or components. If a function or class has more than one responsibility, it probably needs to be broken down into smaller pieces. It’s not just a best practice; it’s a design philosophy that should be a priority in any software project since it enhances code quality, efficiency, and scalability.
Imagine a web application involving a database, a user interface, and a server:
# Incorrect Approach
class App:
def get_user_data(self, user_id):
# Fetch from database
pass
def render_user_page(self, user_data):
# Render user page
pass
def handle_request(self, request):
# Handle web request
pass
# Correct Approach
class Database:
def get_user_data(self, user_id):
# Fetch from database
pass
class UserInterface:
def render_user_page(self, user_data):
# Render user page
pass
class Server:
def handle_request(self, request, db, ui):
user_data = db.get_user_data(request.user_id)
ui.render_user_page(user_data)
Imagine you’re solving a puzzle; if you mix pieces from different puzzles, it becomes extremely challenging, if not impossible, to complete the picture. Similarly, in software development, each component or function should be like a unique puzzle piece with a defined purpose and shape. Keep your components and functions focused on a single task or responsibility. By doing so, not only will it be easier for you to identify and resolve issues, but you’ll also be able to reuse, test, and understand your code much more easily. When a component or function begins to handle multiple tasks, it’s time to pause, reevaluate, and likely split it.
The Law of Demeter, often referred to as the Principle of Least Knowledge, is rooted in the idea of reducing dependencies between modules or classes in software. Essentially, it suggests that each unit should have limited knowledge about other units and should only interact with its “close friends.”
The Law of Demeter is violated when navigating through multiple objects accessing chained methods, like a.b().c(), when initializing objects in a class that should be injected as dependencies, or when a class interacts with several others to retrieve data or execute functions. The benefits in terms of code clarity and structure are invaluable. As with all design principles, the key is to apply it with judgment and balance.
Imagine a case where a driver wants to start a car:
# Incorrect Approach
class Engine:
class SparkPlug:
def ignite(self):
pass
class Car:
def __init__(self):
self.engine = Engine()
def start(self):
self.engine.SparkPlug().ignite()
# Correct Approach
class Engine:
def start(self):
spark_plug = SparkPlug()
spark_plug.ignite()
class SparkPlug:
def ignite(self):
pass
class Car:
def __init__(self):
self.engine = Engine()
def start(self):
self.engine.start()
In the correct approach, Car doesn’t need to know about SparkPlug. It just knows that the engine has a start() method. The internal implementation of how the engine starts is encapsulated within the Engine class.
Think of the Law of Demeter as an instruction manual for a device. If you need to perform a specific function on it, the manual directs you to a direct button or command to carry out that action. It doesn’t tell you to press a button that then takes you to another panel, where you turn a knob to then take you somewhere else to finally perform the function. In programming, this principle suggests we interact directly with the components we need, rather than chaining multiple actions through different modules.
Users, both developers and end-users, expect software components, functions, or modules to operate in a specific manner. The Principle of Least Astonishment argues that the behavior of any piece of software shouldn’t astonish or confuse its users. Instead, it should be clear, consistent, and predictable.
When a user interacts with a system, they already have certain expectations of how it should function based on past experiences or established conventions. An application behaving in a manner that contradicts those expectations can lead to frustration and errors.
In many systems, clicking on the “X” in one of the top corners of a window will close it. If instead, clicking on that " X" opened a new window or deleted a file, it would counteract the user’s expectation and violate the Principle of Least Astonishment.
# Wrong
def saveFile(file):
# logic to save the file
sendEmail()
deleteAnotherFile()
# Right
def saveFile(file):
# logic to save the file
pass
In the first example, besides saving the file, two other operations are carried out which would not be expected based on the function’s name. These additional actions are unexpected and could be harmful, especially if the user is unaware of them.
Imagine reading a book and, out of the blue, in the middle of a detective story, a fire-breathing dragon appears. This sudden transition causes astonishment and confusion. The same applies to software development. When designing a function or component, ensure its actions are intuitive and expected. Maintain consistency and avoid “unexpected surprises” in your design. Clarity and predictability are essential for robust and reliable software.
The Fail-Fast principle suggests that a system or application should identify error conditions or anomalies as soon as they occur, halt its operation, and immediately report the problem. The idea is that it’s much better to detect issues early on and address them right away, rather than allowing a system to continue operating in a potentially unstable state.
This principle is foundational in many aspects of programming, from algorithm design to the development of entire systems. By spotting and handling errors as soon as they arise, one can prevent undesirable consequences later in the process, such as inaccurate computations, data corruption, or even system failures.
Applying this principle not only aids in building more robust and reliable systems but also makes it easier to detect and fix errors during development and testing phases. This can save significant time and resources compared to rectifying problems after a product has been launched or deployed.
Moreover, by providing immediate feedback about issues, the user or developer experience is enhanced, as they can address and resolve problems promptly.
Imagine a function that carries out a division operation. Instead of letting the operation proceed with a divisor of zero (resulting in an error), it’s better to check for this condition and generate an error right away, notifying the user or developer about the problem.
# Incorrect
def divide_incorrect(a, b):
if b == 0:
return 0 # Returns a default value without signaling the error
return a / b
# Correct
def divide(a, b):
if b == 0:
raise ValueError("The divisor cannot be zero.")
return a / b
Imagine pouring water into a strainer thinking it’s a glass; the sooner you realize the mistake, the less water you’ll waste. Similarly, in software development, when something goes awry, it’s crucial to notice and act immediately. If your code detects an abnormal condition or a potential error, don’t ignore or conceal it; make your system signal it right away. Adopt a fail-fast mindset so that your programs inform you and not the other way around. A system that fails quickly and informatively cares about its integrity and its users'.
Over the years, and with accumulated experience in software development, a series of principles and practices have been established to act as guides for producing high-quality code. These are not merely rules or standards to abide by but a distillation of collective knowledge for designing and implementing robust, maintainable, and effective software. Applying these principles reflects the developer’s commitment to producing software that not only works but is also enduring and valuable for its users. On the path to mastering programming, these principles are the beacons that guide us. However, it’s essential to remember that an overly rigid or excessive application of these can be counterproductive.
That may interest you
Agile methodologies were developed to provide a mechanism that facilitates adaptation to change in …
read moreIn the mobile app development world, ensuring quality and user satisfaction is as crucial as …
read moreIn the last chapter, we delved into the depths of GNU/Linux, exploring user management, file system …
read moreConcept to value