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.