As you start learning to program, it's important to understand some key design principles that will help you write better, cleaner code. Let's look at three important principles: DRY, KISS, and SOLID.
Avoids code duplication, promoting cleaner and more maintainable code.
Bad example (repeating code):
def greet_john
puts "Hello, John!"
puts "How are you today?"
end
def greet_mary
puts "Hello, Mary!"
puts "How are you today?"
end
Better example (using a method):
def greet(name)
puts "Hello, #{name}!"
puts "How are you today?"
end
greet("John")
greet("Mary")
Emphasizes simplicity and clarity in code design, making it easier to understand and modify.
Here's an example with a mistake that can be optimized following KISS:
def is_weekend?(day)
if day == "Saturday" || day == "Sunday"
puts "It's the weekend!"
return true
else
puts "It's a weekday."
return false
end
end
# Usage (assuming today is Wednesday)
day = "Wednesday"
is_weekend?(day)
This code works, but it's a bit verbose and repetitive. It prints messages within the function which might not be always desired.
KISS Optimization:
def is_weekend?(day)
day == "Saturday" || day == "Sunday"
end
# Usage
day = "Wednesday"
if is_weekend?(day)
puts "It's the weekend!"
else
puts "It's a weekday."
end
Here's how it follows KISS:
This optimized code achieves the same functionality (checking if it's the weekend) with fewer lines and avoids unnecessary complexity within the function.
Single Responsibility, Open/Closed, Liskov Substitution, Interface Segregation, Dependency Inversion: A set of five principles that promote well-structured, maintainable, and flexible object-oriented code.
Each part of your code should do one thing well.
Bad example (multiple responsibilities):
class User
attr_reader :name
def initialize(name)
@name = name
end
def save_to_database
# Code to save user to database
end
def generate_report
# Code to generate a report
end
end
Better example:
class User
attr_reader :name
def initialize(name)
@name = name
end
end
class UserDatabase
def save_user(user)
# Code to save user to database
end
end
class ReportGenerator
def generate_user_report(user)
# Code to generate a report
end
end
Your code (classes, modules, functions, etc.) should be open for extension but closed for modification. This means you should be able to extend a class's behavior without modifying its existing code.
class Shape
def area
raise NotImplementedError, "Subclass must implement abstract method"
end
end
class Rectangle < Shape
def initialize(width, height)
@width = width
@height = height
end
def area
@width * @height
end
end
class Circle < Shape
def initialize(radius)
@radius = radius
end
def area
Math::PI * @radius**2
end
end
Shape
class is abstract and defines an interface with the area
method.Rectangle
and Circle
classes extend Shape
and implement their own area
methods.Shape
without modifying existing code.This structure allows you to add new shapes without changing the existing Shape
, Rectangle
, or Circle
classes.
Subclasses should be able to replace their parent classes without breaking the code. In other words, a subclass should be able to do everything its parent class can do.
class Bird
def fly
raise NotImplementedError, "Subclass must implement abstract method"
end
end
class Sparrow < Bird
def fly
puts "Sparrow flying"
end
end
class Ostrich < Bird
def fly
raise "Ostriches can't fly" # This violates LSP
end
end
In the given code:
Bird
class defines a fly
method that's meant to be implemented by subclasses.Sparrow
class implements fly
correctly.Ostrich
class violates LSP because it can't fulfill the fly
behavior expected from a Bird
.The problem here is that the Bird
class assumes all birds can fly
, which isn't true in reality. To adapt this code to follow LSP, we need to redesign our class hierarchy to better represent the capabilities of different types of birds.
Here's how we can modify the code to adhere to LSP:
class Bird
def move
raise NotImplementedError, "Subclass must implement abstract method"
end
end
class FlyingBird < Bird
def move
fly
end
def fly
raise NotImplementedError, "Subclass must implement abstract method"
end
end
class NonFlyingBird < Bird
def move
run
end
def run
raise NotImplementedError, "Subclass must implement abstract method"
end
end
class Sparrow < FlyingBird
def fly
puts "Sparrow flying"
end
end
class Ostrich < NonFlyingBird
def run
puts "Ostrich running"
end
end
In this revised version:
Bird
class with a generic move
method.FlyingBird
and NonFlyingBird
.FlyingBird
implements move
as fly
, while NonFlyingBird
implements it as run
.Sparrow
inherits from FlyingBird
and implements fly
.Ostrich
inherits from NonFlyingBird
and implements run
.Now, this hierarchy respects LSP because:
move
, maintaining the base class contract.fly
, and non-flying birds implement run
.Bird
subclass where a Bird
is expected, and the move
method will work correctly for all of them.Many specific interfaces are better than one general interface.
Scenario: Imagine a library management system with different types of media (books, audiobooks, movies). We want to represent them with classes and define functionalities for borrowing and returning items.
Without ISP:
module Borrowable
def borrow
raise NotImplementedError, "Classes must implement borrow method"
end
def return_item
raise NotImplementedError, "Classes must implement return_item method"
end
end
class Book
include Borrowable
def borrow
puts "Borrowing a book"
end
def return_item
puts "Returning a book"
end
end
class Audiobook
include Borrowable
def borrow
puts "Borrowing an audiobook (download or physical copy)"
end
def return_item
puts "Returning an audiobook"
end
end
class Movie
include Borrowable
def borrow
puts "Borrowing a movie (rental or streaming)"
end
def return_item
puts "Returning a movie"
end
end
This code works, but it violates ISP because:
Borrowable
module forces all classes to implement return_item
, even though movies might not have a physical item to return (depending on rental or streaming).With ISP:
module Borrowable
def borrow
raise NotImplementedError, "Classes must implement borrow method"
end
end
module PhysicalItem
def return_item
raise NotImplementedError, "Classes must implement return_item method"
end
end
class Book
include Borrowable
include PhysicalItem
def borrow
puts "Borrowing a book"
end
def return_item
puts "Returning a book"
end
end
class Audiobook
include Borrowable
def borrow
puts "Borrowing an audiobook (download or physical copy)"
end
# No return_item method since it might not be applicable
end
class Movie
include Borrowable
def borrow
puts "Borrowing a movie (rental or streaming)"
end
# No return_item method since it might not be applicable
end
This approach adheres to ISP by:
Borrowable
handles borrowing, and PhysicalItem
handles physical item return (optional for some classes).Book
includes both modules, while Audiobook
and Movie
only include Borrowable
.This improves code maintainability and flexibility. You can add new media types (e.g., ebooks) with specific borrowing functionalities without modifying existing classes.
Depend on abstractions, not concrete implementations. Dependency Inversion Principle (DIP) says high-level parts of your code shouldn't rely on specific details of lower-level parts. Instead, they should both rely on a common "blueprint" (interface). This lets you change or swap lower-level parts without affecting the higher-level ones, keeping your code flexible and easier to maintain.
Scenario: Imagine a notification system that can send different types of notifications (email, SMS, push notification). We want to decouple the notification sender from the specific notification type.
Without DIP:
class NotificationSender
def initialize(recipient)
@recipient = recipient
end
def send_email_notification(message)
puts "Sending email notification to #{@recipient} with message: #{message}"
end
def send_sms_notification(message)
puts "Sending SMS notification to #{@recipient} with message: #{message}"
end
end
# Usage
sender = NotificationSender.new("[email protected]")
sender.send_email_notification("Welcome message!")
This code works, but it violates DIP because:
NotificationSender
class is tightly coupled to specific notification types (email, SMS). Adding a new notification type (e.g., push) requires modifying this class.With DIP:
interface Notification
def send(recipient, message)
raise NotImplementedError, "Subclasses must implement send method"
end
end
class EmailNotification
include Notification
def send(recipient, message)
puts "Sending email notification to #{recipient} with message: #{message}"
end
end
class SMSNotification
include Notification
def send(recipient, message)
puts "Sending SMS notification to #{recipient} with message: #{message}"
end
end
# Add more notification classes like PushNotification (similar structure)
class NotificationSender
def initialize(notification)
@notification = notification
end
def send_notification(recipient, message)
@notification.send(recipient, message)
end
end
email_notification = EmailNotification.new
sms_notification = SMSNotification.new
sender = NotificationSender.new(email_notification)
sender.send_notification("[email protected]", "Welcome message!")
# Use the sender with different notification types by injecting them
sender = NotificationSender.new(sms_notification)
sender.send_notification("+84877698945", "Your order has shipped!")
How it adheres to DIP:
NotificationSender
): Depend on abstractions (Notification
).EmailNotification
, SMSNotification
): Implement the abstraction.This approach offers several benefits:
NotificationSender
.NotificationSender
in isolation by mocking different notification implementations.By following DIP, you create a more adaptable and maintainable notification system.
Remember, these principles are guidelines, not strict rules. Use your judgment to find the right balance based on your project's complexity and requirements. These principles are guidelines to help you write better code. As you practice, they'll become more natural to apply.