Assignment 7
Assignment 7
Instructions: 1. For each plantUML code, replace it with the UML diagram it
creates and edit the connections between classes for accuracy as needed.
Do not remove the existing labels and explanations 2. For each scenario,
edit the comments on it by making reference to your UML.
SOLID principles are a set of design principles that promote maintainability, flexibility, and scalability in
object-oriented software development. These principles include Single Responsibility Principle (SRP),
Open/Closed Principle (OCP), Liskov Substitution Principle (LSP), Interface Segregation Principle
(ISP), and Dependency Inversion Principle (DIP). Here's how you can apply these principles in Java
while implementing business logic from a use case:
1. Single Responsibility Principle (SRP):
Identify the responsibilities of your use case. A use case typically involves multiple steps or
actions.
Create separate classes or methods for each responsibility. For example, separate classes
for input validation, business rules, and data persistence.
public class UseCase {
private Validator validator;
private BusinessLogic businessLogic;
private DataPersistence dataPersistence;
Design your classes and modules to be open for extension but closed for modification.
Use interfaces and abstract classes to define contracts that can be extended without
modifying existing code.
Ensure that derived classes can be substituted for their base classes without affecting the
correctness of the program.
Follow the contracts defined by interfaces and base classes.
public interface BusinessLogic {
void perform();
}
By applying these SOLID principles, you create a more modular, maintainable, and extensible
codebase for your business logic in Java.
class Payment {
-amount: double
-customerId: String
-paymentMethod: String
+validate(): boolean
+persist(): void
}
@startuml
interface PaymentValidator {
+validate(payment: Payment): boolean
}
interface PaymentPersister {
+persist(payment: Payment): void
}
class PaymentProcessor {
-validator: PaymentValidator
-persister: PaymentPersister
class Payment {
-amount: double
-customerId: String
-paymentMethod: String
}
interface PaymentPersister {
+persist(payment: Payment): void
}
interface PaymentMethodProcessor {
+processPayment(payment: Payment): void
}
class PaymentProcessor {
-validator: PaymentValidator
-persister: PaymentPersister
-methodProcessor: PaymentMethodProcessor
class Payment {
-amount: double
-customerId: String
-paymentMethod: String
}
@startuml
interface PaymentValidator {
+validate(payment: Payment): boolean
}
interface PaymentPersister {
+persist(payment: Payment): void
}
interface PaymentMethodProcessor {
+processPayment(payment: Payment): void
}
class PaymentProcessor {
-validator: PaymentValidator
-persister: PaymentPersister
-methodProcessor: PaymentMethodProcessor
class Payment {
-amount: double
-customerId: String
-paymentMethod: String
}
interface PaymentPersistence {
+persist(payment: Payment): void
}
interface PaymentMethodProcessor {
+processPayment(payment: Payment): void
}
class PaymentProcessor {
-validation: PaymentValidation
-persistence: PaymentPersistence
-methodProcessor: PaymentMethodProcessor
class Payment {
-amount: double
-customerId: String
-paymentMethod: String
}
interface PaymentPersistence {
+persist(payment: Payment): void
}
interface PaymentMethodProcessor {
+processPayment(payment: Payment): void
}
class PaymentProcessor {
-validation: PaymentValidation
-persistence: PaymentPersistence
-methodProcessor: PaymentMethodProcessor
class Payment {
-amount: double
-customerId: String
-paymentMethod: String
}
This refactoring adheres to SOLID principles, making the code more modular, maintainable, and
extensible. Each class or interface now has a single responsibility, and dependencies are inverted to
depend on abstractions.
Let's consider a simple scenario of a notification system that sends messages through different
channels. We'll start with a basic use case and then refactor it to adhere to SOLID principles using
for visual representation.
The system needs to send notifications through different channels (e.g., email, SMS).
@startuml
class NotificationService {
+sendNotification(message: String, channel: String): void
}
class EmailService {
+sendEmail(message: String): void
}
class SMSService {
+sendSMS(message: String): void
}
@enduml
@startuml
interface NotificationSender {
+sendNotification(message: String): void
}
@startuml
interface EmailSender {
+sendEmail(message: String): void
}
interface SMSSender {
+sendSMS(message: String): void
}
interface PushNotificationSender {
+sendPushNotification(message: String): void
}
@startuml
interface EmailSender {
+sendEmail(message: String): void
}
interface SMSSender {
+sendSMS(message: String): void
}
interface PushNotificationSender {
+sendPushNotification(message: String): void
}
The system needs to manage different types of content (e.g., articles, videos).
@startuml
class ContentManager {
+addContent(content: Content): void
}
class Content {
-type: String
-data: String
+display(): void
}
class ContentManager {
+addContent(content: Content): void
}
class Content {
-type: String
-data: String
}
@startuml
interface ContentDisplay {
+display(): void
}
interface Content {
// Common content properties and methods
}
class ContentManager {
+addContent(content: Content): void
}
@startuml
interface ContentDisplay {
+display(): void
}
interface Content {
// Common content properties and methods
}
class ContentManager {
+addContent(content: Content): void
}
@startuml
interface ContentDisplay {
+display(): void
}
interface ArticleDisplay {
+display(): void
}
interface VideoDisplay {
+display(): void
}
interface ImageDisplay {
+display(): void
}
interface Content {
// Common content properties and methods }
class ContentManager {
+addContent(content: Content): void
}
interface ArticleDisplay {
+display(): void
}
interface VideoDisplay {
+display(): void
}
interface ImageDisplay {
+display(): void
}
interface Content {
// Common content properties and methods }
class ContentManager {
+addContent(content: Content): void
}
27/50
This refactoring adheres to SOLID principles, making the code more modular, maintainable, and
extensible. Each class or interface now has a single responsibility, and dependencies are inverted to
depend on abstractions.
Let's consider a scenario of an online shopping system. We'll start with a basic use case and then
refactor it to adhere to SOLID principles using for visual representation.
@startuml
class ProductService {
+addProduct(product: Product): void
}
class OrderService {
+createOrder(cart: ShoppingCart): void
}
class ShoppingCart {
-items: List<Item>
+addItem(item: Item): void
+checkout(): void
}
class Product {
-name: String
-price: double
}
class Item {
-product: Product
-quantity: int
+calculateTotal(): double
}
@enduml
class OrderProcessor {
+createOrder(cart: ShoppingCart): void
}
class ShoppingCart {
-items: List<Item>
+addItem(item: Item): void
+checkout(): void
}
class Product {
-name: String
-price: double
}
class Item {
-product: Product
-quantity: int
+calculateTotal(): double
}
@enduml
@startuml
interface OrderProcessor {
+createOrder(cart: ShoppingCart): void
}
class StandardOrderProcessor implements OrderProcessor {
+createOrder(cart: ShoppingCart): void
}
class ShoppingCart {
-items: List<Item>
+addItem(item: Item): void
+checkout(orderProcessor: OrderProcessor): void
}
class Product {
-name: String
-price: double
}
class Item {
-product: Product
-quantity: int
+calculateTotal(): double
}
@enduml
@startuml
interface OrderProcessor {
+createOrder(cart: ShoppingCart): void
}
class Product {
-name: String
-price: double
}
class Item {
-product: Product
-quantity: int
+calculateTotal(): double
}
@enduml
@startuml
interface ProductManagement {
+addProduct(product: Product): void
}
interface OrderProcessing {
+createOrder(cart: ShoppingCart): void
}
interface ShoppingCartManagement {
+addItem(item: Item): void
+checkout(orderProcessor: OrderProcessing): void }
class Product {
-name: String
-price: double
}
class Item {
-product: Product
-quantity: int
+calculateTotal(): double
}
@enduml
interface OrderProcessing {
+createOrder(cart: ShoppingCart): void
}
interface ShoppingCartManagement {
+addItem(item: Item): void
+checkout(orderProcessor: OrderProcessing): void }
class Product {
-name: String
-price: double
}
class Item {
-product: Product
-quantity: int
+calculateTotal(): double
}
@enduml
This refactoring adheres to SOLID principles, making the code more modular, maintainable, and
extensible. Each class or interface now has a single responsibility, and dependencies are inverted to
depend on abstractions.
Let's consider a scenario involving authentication and authorization in a web application. We'll start
with a basic use case and then refactor it to adhere to SOLID principles using for visual
representation.
@startuml
class AuthenticationService {
+authenticate(username: String, password: String): boolean
+authorize(user: User, role: String): boolean
}
class User {
-username: String
-password: String
-roles: List<String>
}
class UserController {
+login(username: String, password: String): String
+checkAuthorization(user: User, role: String): boolean
}
@enduml
46/50
2. Notification System:
Each example demonstrates the application of SOLID principles to improve code structure,
maintainability, and flexibility.
SUMMARY
Here is a set of instructions to follow for each scenario example to refactor the code according to
SOLID principles:
2. Notification System:
Identify and separate the responsibilities related to different notification channels (e.g.,
email, SMS).
Introduce interfaces for notification channels.
Implement concrete classes for each notification channel.
Refactor the NotificationService to use the new interfaces.
3. Content Management System:
General Instructions:
For each refactoring, consider the Single Responsibility Principle (SRP), Open/Closed Principle
(OCP), Liskov Substitution Principle (LSP), Interface Segregation Principle (ISP), and
Dependency Inversion Principle (DIP).
Ensure that classes and interfaces have clear responsibilities and are loosely coupled.
Apply dependency injection to promote flexibility and adherence to DIP.
Update class relationships and dependencies in diagrams accordingly.
Examine the existing code and identify distinct responsibilities within the system, such as
validation, persistence, notification channels, content management, order processing, and
authentication.
2. Introduce Interfaces:
For each identified responsibility, introduce corresponding interfaces that define the
contract for the associated functionality. This promotes adherence to the Interface
Segregation Principle (ISP).
3. Implement Concrete Classes:
Create concrete classes that implement the introduced interfaces. Each concrete class
should encapsulate the specific behavior related to its responsibility. This supports the
Single Responsibility Principle (SRP).
4. Refactor Existing Code:
Modify the existing code to use the newly introduced interfaces and concrete classes.
Update dependencies to rely on abstractions rather than concrete implementations,
following the Dependency Inversion Principle (DIP).
5. Apply Dependency Injection:
Design the system to be open for extension but closed for modification. When adding new
functionality or modifying existing behavior, favor extending existing interfaces or
introducing new interfaces.
7. Ensure Liskov Substitution Principle (LSP):
Verify that concrete classes implementing interfaces can be substituted for instances of
those interfaces without affecting the correctness of the system. This ensures consistency
and extensibility.
8. Update Diagrams:
Reflect the changes made in the code by updating the diagrams. Clearly represent
the relationships, interfaces, and dependencies to visually communicate the design
improvements.
Appendix:
By following these generalized instructions, students can systematically apply SOLID principles
across various scenarios, ensuring the codebase becomes modular, maintainable, and extensible.
The principles of SOLID, when consistently applied, contribute to robust software design and
development.
Robert C. Martin, also known as "Uncle Bob," is a software engineer and one of the authors of the
SOLID principles. The SOLID principles are a set of five design principles that promote
maintainability, flexibility, and readability in object-oriented software design. Keep in mind that
there might be updates or changes beyond my last knowledge update in January 2022. As of
then, the SOLID principles are:
Definition: A class should have only one reason to change, meaning that a class should have only
one responsibility or job. If a class has more than one responsibility, it becomes more difficult to
understand, modify, and maintain.
Guidance: Design classes that have a single reason to change. If you can describe a class with
the word "and," it might be doing more than one thing.
Definition: Software entities (classes, modules, functions, etc.) should be open for extension but
closed for modification. This means that you should be able to add new functionality without
altering the existing code.
Guidance: Use abstraction and polymorphism to design systems that can be easily extended
without modifying existing code. This involves creating interfaces or abstract classes that can be
implemented or extended by new code.
Definition: Subtypes must be substitutable for their base types without altering the correctness of
the program. In other words, if a class is a subtype of another class, it should be able to replace
that class without affecting the program's behavior.
Guidance: Ensure that derived classes adhere to the contracts and expectations set by their base
classes. Subtypes should extend rather than contradict the behavior of the base types.
Interface Segregation Principle (ISP):
Definition: A class should not be forced to implement interfaces it does not use. This means that a
class should only be required to implement methods that are relevant to its functionality.
Guidance: Design fine-grained interfaces specific to the needs of clients. Avoid creating large,
monolithic interfaces that force implementing classes to provide methods they don't need.
Definition: High-level modules (e.g., business logic) should not depend on low-level modules (e.g.,
data access or specific implementations), but both should depend on abstractions. Abstractions
should not depend on details; details should depend on abstractions.
Guidance: Use dependency injection, abstractions, and inversion of control to achieve a flexible
and loosely coupled architecture. High-level modules should depend on abstract interfaces,
allowing them to be independent of the low-level implementation details
https://blog.cleancoder.com/uncle-bob/2020/10/18/Solid-Relevance.html
https://en.wikipedia.org/wiki/SOLID
https://sujoyu.github.io/-previewer/
1. Association:
● Arrow: By default, no arrowhead. You can use --> to add an arrowhead at one end, <-- for the other
end, or <-> for both ends.
2. Aggregation:
● Line Type: Solid line with an open diamond at the aggregate end.
● Arrow: No arrowhead.
3. Composition:
● Line Type: Solid line with a filled diamond at the composite end.
● Arrow: No arrowhead.
● Line Type: Solid line with a hollow triangle at the child end.
● Arrow: No arrowhead.
● Line Type: Dashed line with a hollow triangle at the implementing class end.
● Arrow: No arrowhead.
6. Dependency:
● Arrow: By default, an arrowhead pointing from the dependent to the independent element. You can use
..> for no arrowhead.
● Line Type: Solid line connecting three elements (two classes and an association class).
The arrow points from the dependent element (the element that relies on the other) to the independent
element.
Worked Example:
Scenario: File Management System
Original Use Case (Without SOLID):
Use Case: Process Different Types of Files
The system needs to handle different types of files, such as text files, image files, and audio files.
@startuml
class FileManager {
+processFile(file: File): void
}
class File {
-type: String
-data: byte[]
}
@startuml
interface FileDisplay {
+display(): void
}
class FileManager {
+processFile(file: FileDisplay): void
}
class File {
-type: String
-data: byte[]
}
@startuml
interface FileDisplay {
+display(): void
}
interface FileProcessor {
+processFile(file: FileDisplay): void
}
@startuml
interface FileDisplay {
+display(): void
}
interface FileProcessor {
+processFile(file: FileDisplay): void
}
interface FileProcessor {
+processFile(file: FileDisplay): void
}
@startuml
interface FileDisplay {
+display(): void
}
interface FileProcessor {
+processFile(file: FileDisplay): void
}