DRY for Cleaner Code (with Dart Examples)

Brahim Guaali
6 min readMay 25, 2023

--

Thats true!

In the world of software development, maintaining clean and efficient code is of utmost importance. One fundamental principle that guides developers toward this goal is the DRY principle, which stands for “Don’t Repeat Yourself.” This principle emphasizes the importance of avoiding code duplication, promoting code reusability, and enhancing maintainability. In this article, we’ll explore the DRY principle and its benefits, along with practical examples in Dart, a popular programming language.

What is the DRY Principle?

The DRY principle is a software development principle that encourages developers to avoid duplicating code wherever possible. Instead of writing the same or similar code in multiple places, the principle suggests creating reusable components, functions, or modules to eliminate redundancy. By following the DRY principle, developers can minimize bugs, reduce code size, enhance readability, and simplify the maintenance process.

Code Example 1: Reusable Functions

Consider a scenario where you need to perform some common mathematical calculations in multiple places within your Dart codebase. Instead of duplicating the same code, you can create a reusable function to encapsulate the logic.

double calculateArea(double width, double height) {
return width * height;
}

void main() {
double rectangleWidth = 5;
double rectangleHeight = 10;

double area = calculateArea(rectangleWidth, rectangleHeight);
print('The area of the rectangle is $area');
}

In the example above, the calculateArea function takes the width and height as parameters and returns the calculated area. By using this function, you can easily calculate the area of a rectangle by calling it with the appropriate values. This way, you avoid duplicating the area calculation logic and improve code readability and maintainability.

Code Example 2: Extracting Common Behavior into a Class

Another way to apply the DRY principle is by extracting common behavior into a reusable class. Let’s say you have different classes representing different shapes, each with its own calculation logic. Instead of duplicating the calculation code in each class, you can create a base class and inherit from it.

abstract class Shape {
double calculateArea();
}

class Rectangle extends Shape {
double width;
double height;

Rectangle(this.width, this.height);

@override
double calculateArea() {
return width * height;
}
}

class Circle extends Shape {
double radius;

Circle(this.radius);

@override
double calculateArea() {
return 3.14 * radius * radius;
}
}

void main() {
Shape rectangle = Rectangle(5, 10);
print('The area of the rectangle is ${rectangle.calculateArea()}');

Shape circle = Circle(7);
print('The area of the circle is ${circle.calculateArea()}');
}

In the example above, the abstract class Shape defines the calculateArea method, which is implemented by its subclasses Rectangle and Circle. Each subclass encapsulates the calculation logic specific to its shape, while the common behavior is inherited from the base class. This approach enables you to reuse the calculateArea method and avoid code duplication.

Code Example 3: Utilizing Constants

When you have constant values used in multiple places within your code, it’s recommended to define them as constants rather than repeating the same value multiple times. This improves readability, allows for easier maintenance, and avoids inconsistencies.

class Constants {
static const double MAX_WIDTH = 100.0;
static const double MAX_HEIGHT = 200.0;
}

void main() {
double width = 50.0;
double height = 150.0;

if (width > Constants.MAX_WIDTH) {
print('Width exceeds the maximum allowed value');
}

if (height > Constants.MAX_HEIGHT) {
print('Height exceeds the maximum allowed value');
}
}

In this example, the maximum width and height are defined as constants within the Constants class. These constants can be accessed from anywhere in the code, ensuring consistency and eliminating the need to repeat the same values.

Code Example 4: Extracting Common Validation Logic

Consider a scenario where you need to validate user input in multiple parts of your application. Instead of duplicating the validation code, you can create a reusable function to handle the validation.

bool isValidEmail(String email) {
final RegExp emailRegex = RegExp(r'^[\w-]+(\.[\w-]+)*@[a-zA-Z\d-]+(\.[a-zA-Z\d-]+)*(\.[a-zA-Z]{2,})$');
return emailRegex.hasMatch(email);
}

void main() {
String email = 'test@example.com';

if (isValidEmail(email)) {
print('Email is valid');
} else {
print('Invalid email');
}
}

In this example, the isValidEmail function takes an email as a parameter and validates it using a regular expression. By encapsulating the validation logic into a function, you can reuse it across different parts of your application, avoiding code duplication and ensuring consistent validation.

Code Example 5: Extracting Common Functionality into Helper Methods

When you find yourself repeating similar logic in different methods, it’s beneficial to extract that common functionality into helper methods. This promotes code reuse and simplifies maintenance.

class StringUtil {
static String capitalize(String text) {
if (text.isEmpty) return '';
return text[0].toUpperCase() + text.substring(1);
}
}

void main() {
String name = 'john doe';

String capitalized = StringUtil.capitalize(name);
print(capitalized); // Output: John doe
}

In this example, the StringUtil class defines a capitalize static method that capitalizes the first letter of a given string. By extracting this functionality into a helper method, you can reuse it throughout your codebase, avoiding redundant code and enhancing maintainability.

Code Example 6: Abstracting Common UI Components

In user interface development, there are often instances where you need to create similar components with slight variations. By abstracting common components into reusable widgets or classes, you can achieve a cleaner and more maintainable codebase.

import 'package:flutter/material.dart';

class CustomButton extends StatelessWidget {
final String text;
final VoidCallback onPressed;

const CustomButton({required this.text, required this.onPressed});

@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onPressed,
child: Text(text),
);
}
}

void main() {
runApp(MaterialApp(
home: Scaffold(
body: Center(
child: CustomButton(
text: 'Click Me',
onPressed: () {
print('Button clicked');
},
),
),
),
));
}

In this Flutter example, the CustomButton widget encapsulates the common behavior and styling of a button with a customizable text and onPressed callback. By abstracting this component into a reusable widget, you can create multiple instances of the button with different texts and actions, reducing code duplication and enhancing code maintainability.

Code Example 7: Extracting Business Logic into Separate Classes

When dealing with complex business logic, it’s beneficial to extract it into separate classes or modules. This approach improves code organization, promotes code reusability, and enhances maintainability.

class Calculator {
int add(int a, int b) {
return a + b;
}

int subtract(int a, int b) {
return a - b;
}
}

void main() {
Calculator calculator = Calculator();

int result1 = calculator.add(5, 3);
print(result1); // Output: 8

int result2 = calculator.subtract(10, 4);
print(result2); // Output: 6
}

In this example, the Calculator class encapsulates the business logic for addition and subtraction operations. By abstracting this logic into a separate class, you can reuse it throughout your codebase, avoiding code duplication and promoting a modular structure.

Code Example 8: Using Inheritance to Share Common Functionality

Inheritance allows you to define a base class with common functionality and derive specialized classes that inherit and extend that functionality. This technique reduces redundancy and fosters code reuse.

abstract class Animal {
String name;

Animal(this.name);

void makeSound();
}

class Cat extends Animal {
Cat(String name) : super(name);

@override
void makeSound() {
print('Meow!');
}
}

class Dog extends Animal {
Dog(String name) : super(name);

@override
void makeSound() {
print('Woof!');
}
}

void main() {
Cat cat = Cat('Whiskers');
cat.makeSound(); // Output: Meow!

Dog dog = Dog('Buddy');
dog.makeSound(); // Output: Woof!
}

In this example, the Animal class serves as a base class with a common property name and method makeSound(). The Cat and Dog classes inherit from Animal and override the makeSound() method with their specific sound. By utilizing inheritance, you avoid duplicating the common functionality across different animal classes.

Benefits of the DRY Principle:

  1. Readability: By eliminating duplicate code, the DRY principle improves code readability. Developers can easily understand and maintain the codebase, as they only need to analyze and modify a single implementation.
  2. Maintainability: Code duplication leads to redundancy and makes maintenance more challenging. The DRY principle promotes modular and reusable code, reducing the effort required for bug fixes, enhancements, and refactoring.
  3. Bug Reduction: Repeating code increases the chances of introducing bugs. By centralizing logic into reusable components, you minimize the risk of inconsistencies or errors introduced by duplicate code.
  4. Code Size: Eliminating redundancy reduces the overall code size, making the application more efficient in terms of storage and execution.
  5. Flexibility: Reusable code can be easily adapted and extended, enhancing the flexibility of your software architecture.

Conclusion:

The DRY principle serves as a guiding principle for developers striving to write clean, efficient, and maintainable code. By avoiding code duplication and promoting code reusability, you can improve the readability, maintainability, and flexibility of your Flutter applications. Applying the DRY principle results in leaner codebases, reduced bugs, and a more efficient development process, ultimately leading to higher-quality software products.

--

--