[go: up one dir, main page]

0% found this document useful (0 votes)
70 views26 pages

325 Assignment NOTES LECTURE 2

The document discusses topics covered by different students in a group assignment for CSE 325 Information System Design. It lists the student IDs and names of six students in the group and the specific topics each student covered in the video lecture, including Refactoring, Open Closed Principle, Composition over Inheritance, SRP Violation in Logging and Caching, SOLID principles, and Refactoring. It also provides the date and details of the assignment.

Uploaded by

islam2059
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
70 views26 pages

325 Assignment NOTES LECTURE 2

The document discusses topics covered by different students in a group assignment for CSE 325 Information System Design. It lists the student IDs and names of six students in the group and the specific topics each student covered in the video lecture, including Refactoring, Open Closed Principle, Composition over Inheritance, SRP Violation in Logging and Caching, SOLID principles, and Refactoring. It also provides the date and details of the assignment.

Uploaded by

islam2059
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 26

CSE 325 Information System Design

Assignment

Student IDs:
1805016, 1805035, 1805045, 1805048, 1805051, 1805056

Section: A2
Department: CSE, BUET
Group: 05
Lecture no: 02

Topics Covered:
1805016: REFACTORING(0:00 to 38:00)

1805035:OPEN CLOSED PRINCIPLE AND Initial understanding of


COMPOSITION OVER INHERITANCE (video covered
from 1:21:00 to 2:15:00)

1805045:Pros and cons of composition and inheritance,when to use


composition over inheritance(1:45:00 to 20:15:00)

1805048: Storage, SRP Violation in Logging & Caching (video covered


from 38:00 to 1:21:00)

1805051: SOLID, Refactoring, Logging, Caching (video covered from 0:00


to 38:00 minutes)

1805056:
Date: 10.03.2023

SOLID:
SOLID is a set of principles that were introduced by Robert C. Martin in the early 2000s as a
guide for object-oriented software design. The principles are meant to help software developers
create software that is easy to understand, maintain, and modify.
Here's what each letter in the acronym SOLID stands for and a brief explanation of the principle:
Single Responsibility Principle (SRP): A class should have only one reason to change. This
means that a class should have only one responsibility or job. If a class has more than one
responsibility, it becomes harder to understand, maintain, and modify.
Open-Closed Principle (OCP): A software entity (class, module, function, etc.) should be open
for extension but closed for modification. This means that you should be able to add new
functionality to a software entity without changing the existing code.
Liskov Substitution Principle (LSP): Subtypes must be substitutable for their base types. This
means that any instance of a base class should be able to be replaced by an instance of a
subclass without affecting the correctness of the program.
Interface Segregation Principle (ISP): Clients should not be forced to depend on interfaces they
do not use. This means that you should create small, focused interfaces that contain only the
methods that a client needs, rather than creating large, general-purpose interfaces.
Dependency Inversion Principle (DIP): High-level modules should not depend on low-level
modules. Both should depend on abstractions. Abstractions should not depend on details.
Details should depend on abstractions. This means that you should use interfaces or abstract
classes to define how modules interact with each other, rather than relying on concrete
implementations.
By following these principles, software developers can create software that is more
maintainable, flexible, and extensible.
Modification Starting for 1805016

Refactoring:
Refactoring is the process of improving the structure and quality of existing code without
changing its external behavior. It involves making small, incremental changes to the code that
improve its readability, maintainability, and performance.

The goal of refactoring is to make the code easier to understand, modify, and extend. It helps
reduce technical debt and makes it easier to add new features to the software without
introducing bugs or unintended side effects.

Activities involved in refactoring:


Refactoring can involve a wide range of activities. Here are some examples of activities involved
in refactoring:
1. Identifying areas for improvement: Before refactoring, developers must identify the
areas of the codebase that need improvement. This can involve reviewing the codebase,
analyzing performance data, or receiving feedback from users and stakeholders.
2. Analyzing the existing code: Once areas for improvement have been identified,
developers must analyze the existing code to understand its internal structure and
dependencies. This can involve reviewing code documentation, examining code
comments, or using automated tools to visualize code complexity.
3. Planning and prioritizing changes: Based on the analysis of the existing code,
developers must plan and prioritize the changes they want to make. This can involve
creating a refactoring plan, estimating the effort required for each change, and
prioritizing changes based on their impact on the software system.
4. Making changes: Once a refactoring plan is in place, developers can start making
changes to the codebase. This can involve reorganizing code modules, renaming
variables and functions, simplifying complex code, or improving code comments and
documentation.
5. Testing: After making changes to the codebase, developers must test the software to
ensure that its external behavior has not changed. This can involve writing automated
tests, running manual tests, or using code analysis tools to detect errors or
inconsistencies.
6. Review and feedback: Developers must review their changes and seek feedback from
other team members to ensure that the changes are consistent with the overall goals of
the project. This can involve reviewing code changes, seeking feedback from peers, or
holding code review meetings.

Importance of refactoring:
Refactoring is an important part of the software development process, and it is often done as
part of code reviews or when preparing the codebase for new features or changes. Refactoring
is a necessary practice in software development for several reasons:

1. Maintainability: As software systems evolve, they become more complex and difficult to
maintain. Refactoring can help simplify and organize the codebase, making it easier for
developers to understand, modify, and extend over time.
2. Scalability: Refactoring can help ensure that software systems are scalable, meaning
they can handle increasing amounts of data and users without slowing down or crashing.
By improving the internal structure of the codebase, developers can make it easier to
add new features and functionality without introducing new bugs or performance issues.
3. Technical debt: Technical debt is the accumulation of software design flaws that can
make it more difficult and expensive to maintain and enhance a codebase in the future.
Refactoring can help reduce technical debt by addressing these design flaws as they
arise, making it easier to maintain the codebase over time.
4. Collaboration: Refactoring can help improve collaboration among developers by making
the codebase more understandable and consistent. This can help reduce confusion and
misunderstandings, leading to more efficient and effective teamwork.
5. Quality: Refactoring can help improve the overall quality of software systems by
identifying and addressing issues with the codebase. This can help improve reliability,
security, and performance, making the software more valuable to users and
stakeholders.
Doing refactoring efficiently:
Refactoring requires careful planning, testing, and documentation to ensure that the changes
are made correctly and do not introduce new issues. Thus Doing refactoring efficiently is an
important part of software development. Following steps can be maintained in this regard:
1. Having clear goals: Before starting to refactor, we should have a clear understanding of
the goals we want to achieve.We have to identify the specific areas of the codebase that
need improvement and have a plan in place for how we will make those improvements.
2. Using automated testing: Automated testing can help ensure that our changes do not
introduce new bugs or break existing functionality.We have to make sure to write tests
before making changes to the code and run them frequently as we make changes.
3. Refactoring in small increments: Instead of trying to refactor the entire codebase at
once, we should focus on making small incremental changes. This can help us catch
errors early and ensure that our changes do not introduce new bugs.
4. Keeping track of changes: We should use version control software to keep track of
changes we make during refactoring. This can help us revert changes if necessary and
keep track of what has been changed over time.
5. Getting feedback: We should get feedback from other members of our team as we
refactor. This can help us catch errors early and ensure that our changes are consistent
with the overall goals of the project.
6. Continuous improvement: Refactoring is an ongoing process, and we should strive to
continuously improve the codebase. Regularly review the code to identify areas that
need improvement and prioritize those changes based on their impact on the project.

Modification ended for 1805016

Logging:
Below a sample declaration for a class named FileStore is given.
public class FileStore
{
private string _path;

public FileStore(string path);

public DirectoryInfo WorkingDirectory();

public void Save(string id, string message);

public Maybe<string> Read(string id);


public FileInfo GetFileInfo(string id);
}

Note that the Maybe<string> type represents a value that may or may not be present. In this
case, the Read() method returns a Maybe<string> type, which can be either a Some<string>
with the value of the message if the file exists, or a None<string> if the file does not exist.

The implementation of Save and Read methods are given below where messages are logged:
public void Save(string id, string message) {

_logger.LogInformation($"Saved message with id {id} to {filePath}");


var filePath = Path.Combine(_path, $"{id}.txt");
File.WriteAllText(filePath, message);
}
public string Read(string id) {
var filePath = Path.Combine(_path, $"{id}.txt");

_logger.LogInformation($"Read message with id {id} from {filePath}");


if (!File.Exists(filePath)) {

// This is a process to check if the object is null or not.

// This pattern is called NULL Object Pattern


throw new FileNotFoundException($"File {id}.txt not found in {_path}");
}
return File.ReadAllText(filePath);
}

Logging is a useful tool for developers because it helps them understand the behavior of their
code and diagnose issues that may arise during runtime. Here are a few reasons why logging is
important:
Debugging: When something goes wrong with an application, logging can provide valuable
information to help diagnose the issue. By logging important events and variables at runtime,
developers can trace the path of execution and identify the root cause of errors.
Performance Monitoring: Logging can also help monitor the performance of an application by
tracking important metrics, such as the duration of database queries or HTTP requests. By
analyzing this data, developers can identify bottlenecks and optimize the performance of their
application.
Auditing: Logging can be used to track user actions and detect security breaches. By logging
user activity, developers can identify malicious behavior and take steps to prevent future
attacks.
Compliance: Logging can also help ensure compliance with regulations and standards, such as
GDPR or HIPAA. By logging access to sensitive data, developers can demonstrate that they are
following best practices for data protection and access control.
Overall, logging provides developers with valuable insights into the behavior of their code and
helps them improve the quality, reliability, and security of their applications.

In general developers have access to only Development and Testing Environment. Log files can
be a powerful tool for developers to communicate with the production environment because they
provide a common language for discussing issues and diagnosing problems. Here are a few
ways that log files can help developers communicate with the production environment:
Reproducibility: By logging important events and variables at runtime, developers can reproduce
issues that occurred in production and diagnose the root cause. This helps developers to
communicate effectively with the production environment because they can point to specific log
entries and provide concrete examples of the behavior that needs to be addressed.
Collaboration: Log files can be shared with other developers, testers, and support personnel to
facilitate collaboration and troubleshooting. By sharing log files, developers can provide a
common starting point for discussions and ensure that everyone is working with the same
information.
Feedback: Log files can also provide valuable feedback to developers about the behavior of
their code in the production environment. By analyzing log files, developers can identify patterns
and trends that may indicate opportunities for improvement or optimization.
Documentation: Log files can serve as a form of documentation for the production environment
by recording important events, changes, and incidents. This documentation can be used to track
the evolution of the environment over time and to provide a historical record of issues and
resolutions.
Overall, log files can help developers communicate effectively with the production environment
by providing a common language, facilitating collaboration, providing feedback, and serving as
documentation. By leveraging log files, developers can ensure that they are working closely with
the production environment and delivering high-quality software that meets the needs of users.

Caching:
Here is an updated version of the Save and Read method with caching.
public void Save(string id, string message) {
var filePath = Path.Combine(_path, $"{id}.txt");
File.WriteAllText(filePath, message);
logger.LogInformation($"Saved message with id {id} to {filePath}"); // Add message to cache
cache.Set(id, message);
logger.LogInformation($"Added message with id {id} to cache");
}

public Maybe<string> Read(string id){


// Try to get message from cache
if (_cache.TryGetValue(id, out string message)){
_logger.LogInformation($"Retrieved message with id {id} from cache");
return Maybe.Some(message);
}
var filePath = Path.Combine(_path, $"{id}.txt");
if (!File.Exists(filePath)){
_logger.LogWarning($"File {filePath} not found");
return Maybe.None<string>();
}
message = File.ReadAllText(filePath);
_logger.LogInformation($"Retrieved message with id {id} from {filePath}");
// Add message to cache
_cache.Set(id, message);
_logger.LogInformation($"Added message with id {id} to cache");
return Maybe.Some(message);
}

Caching can be an efficient way to improve the performance of an application because it


reduces the time and resources needed to perform certain operations. Here are a few ways that
caching can be efficient:
Reduced Data Access Time: When data is stored in a cache, it can be accessed much more
quickly than if it had to be retrieved from a database or other data source. This is because the
data is already in memory and doesn't need to be loaded from disk or over the network.

Reduced Network Traffic: When data is stored in a cache, it can reduce the amount of network
traffic needed to retrieve it. This can be especially beneficial in applications that make frequent
requests to remote data sources or APIs, as it can reduce latency and improve response times.

Reduced Database Load: By caching frequently accessed data, we can reduce the number of
requests made to the underlying database. This can help to reduce the load on the database
and improve overall system performance.
Improved Scalability: Caching can also improve the scalability of an application by reducing the
number of resources needed to handle a large volume of requests. By caching frequently
accessed data, we can reduce the load on the system and ensure that it can handle high
volumes of traffic without becoming overwhelmed.

Edited by:
student ID: 1805051, time: 6:53pm, 6/3/2023 (BST)

Storage

Common SRP violation in Logging:

As it has been mentioned, the SRP states that each class should have a single
responsibility, and that responsibility should be entirely encapsulated by that class.
When it comes to logging, it's common to see logging statements scattered throughout
business logic classes, which can violate the SRP if those logging statements are considered
a separate responsibility from the main purpose of the business logic.
One problem with scattering logging statements throughout the codebase is that it can make
it difficult to change the logging behavior in the future. For example, if we decide to change
the format of our logs or switch to a different logging library, we would need to update every
single logging statement in every single class. This is a maintenance nightmare, and it can be
very error-prone.

To address this issue, we can create a separate storage class that is responsible for handling
all of the logging functionality. This class, which we can call something like Logger or
StoreLogger, can contain methods for various types of log messages, such as logInfo(),
logWarning(), logError(), and so on. These methods would take parameters to specify the
message text, severity level, and any other relevant information.

By centralizing all of the logging functionality in a single class, we can avoid the need to
scatter logging statements throughout our codebase. Instead, our business logic classes can
simply call the appropriate method on the Logger class when they need to log something.
This can simplify our code and make it easier to maintain in the long run.

In addition to simplifying maintenance, using a separate Logger class can also make it easier
to switch to a different logging library or change the format of our logs. Instead of updating
every logging statement in every class, we would only need to update the Logger class to use
the new library or format. This makes it easier to keep our codebase up to date with changing
requirements and technologies.

Here is an example of how we would typically implement logging without a StoreLogger


class:
import logging

class BusinessLogic:
def __init__(self):
self.logger = logging.getLogger(__name__)
self.logger.setLevel(logging.DEBUG)
self.file_handler = logging.FileHandler('business_logic.log')
self.file_handler.setLevel(logging.DEBUG)
self.logger.addHandler(self.file_handler)

def do_something(self):
# Some business logic here...
self.logger.info('Something happened')

def do_something_else(self):
# More business logic here...
self.logger.warning('Something unexpected happened')

if __name__ == '__main__':
bl = BusinessLogic()
bl.do_something()
bl.do_something_else()

In this example, the BusinessLogic class includes logging functionality directly inside its
methods. Specifically, it creates a logger object and file handler in its constructor, and uses
those objects to log messages using logger.info and logger.warning methods in its methods.

The problem with this approach is that if we need to change the log messages or the logging
library, we would have to go through every method in BusinessLogic and update the code.
This violates the SRP principle because the class has multiple responsibilities - it's
responsible for both business logic and logging.

Here's an example of how we could implement logging with a StoreLogger class to address
this issue:

import logging

class StoreLogger:
def __init__(self, log_file_path):
self.logger = logging.getLogger(__name__)
self.logger.setLevel(logging.DEBUG)
self.file_handler = logging.FileHandler(log_file_path)
self.file_handler.setLevel(logging.DEBUG)
self.logger.addHandler(self.file_handler)

def log_info(self, message):


self.logger.info(message)
def log_warning(self, message):
self.logger.warning(message)

def log_error(self, message):


self.logger.error(message)

class BusinessLogic:
def __init__(self, logger):
self.logger = logger

def do_something(self):
# Some business logic here...
self.logger.log_info('Something happened')

def do_something_else(self):
# More business logic here...
self.logger.log_warning('Something unexpected happened')

if __name__ == '__main__':
logger = StoreLogger('business_logic.log')
bl = BusinessLogic(logger)
bl.do_something()
bl.do_something_else()

In this example, a separate StoreLogger class is used for logging, which encapsulates all of
the logging functionality. The BusinessLogic class takes an instance of the StoreLogger
class as a parameter in its constructor, so it can use the logging functionality without being
responsible for it.

This way, if we need to change the log messages or the logging library, we only need to
update the StoreLogger class, and all of the code that uses it will automatically use the
updated functionality. This conforms to the SRP principle because each class has a single
responsibility.
Overall, using a separate storage class for logging is a good example of encapsulation and
adhering to the SRP in software development. It simplifies maintenance, improves code
organization, and makes it easier to adapt to changing requirements.
Common SRP violation in Caching:

When we use in-memory caching in our application, it can work well in small-scale
environments. However, if we need to scale our application by adding multiple servers, it
becomes incompatible as each server will have its own cache, which can lead to inconsistent
data across the servers.

To address this issue, we may need to use distributed caching, such as Redis or Memcached,
which can work across multiple servers. However, this creates a problem of SRP violation, as
we may need to change the caching mechanism in multiple places within our codebase, which
is error-prone and time-consuming.

To solve this problem, we can create a StoreCache class, which will encapsulate the caching
functionalities and hide the implementation details from other classes. This class can use a
distributed caching library, and provide a set of caching functions that can be used by other
classes as needed.

By using the StoreCache class, we can centralize the caching mechanism and make it easier
to switch to a different caching system if needed. We can simply update the StoreCache
class with the new caching library and our application will continue to work without the need
to change code in multiple places.

Code without StoreCache class, or without encapsulation:


class ProductService {
public Product getProductById(int productId) {
Product product = null;
// check if product is in cache
if (Cache.hasKey("product_" + productId)) {
product = Cache.get("product_" + productId);
} else {
// if not in cache, fetch from database
product = Database.getProductById(productId);
// add to cache
Cache.put("product_" + productId, product);
}
return product;
}
}
In this example, the ProductService class is responsible for fetching a product by its ID. It
checks if the product is in the cache and if not, it fetches the product from the database and
adds it to the cache. The Cache class is a static class that handles in-memory caching.

The problem with this approach is that if we need to switch to a distributed caching system,
we would need to update this code in multiple places throughout our application, which
violates SRP.

Code with StoreCache class or with encapsulation:


class ProductService {
private StoreCache cache;

public ProductService() {
// initialize the cache
cache = new StoreCache();
}

public Product getProductById(int productId) {


Product product = null;
// check if product is in cache
if (cache.hasKey("product_" + productId)) {
product = cache.get("product_" + productId);
} else {
// if not in cache, fetch from database
product = Database.getProductById(productId);
// add to cache
cache.put("product_" + productId, product);
}
return product;
}
}

class StoreCache {
private DistributedCache cache;

public StoreCache() {
// initialize the distributed cache
cache = new RedisCache();
}
public boolean hasKey(String key) {
return cache.hasKey(key);
}

public Object get(String key) {


return cache.get(key);
}

public void put(String key, Object value) {


cache.put(key, value);
}
}

interface DistributedCache {
boolean hasKey(String key);
Object get(String key);
void put(String key, Object value);
}

class RedisCache implements DistributedCache {


// implementation for Redis caching
}

In this example, we have created a StoreCache class, which encapsulates the caching
functionality and uses a distributed caching library. The ProductService class now uses an
instance of the StoreCache class to fetch the product by ID. If we need to switch to a
different caching system, we can simply update the StoreCache class to use a different
distributed caching library, and our application will continue to work without the need to
update code in multiple places.

By using the StoreCache class, we have separated the caching functionality from the
ProductService class, which improves SRP and makes our application more maintainable and
scalable.

In summary, by using the StoreCache class, we can avoid SRP violation related to caching
and improve the scalability and maintainability of our application.
Edited by:
Student ID: 1805048
Name: Asif Ihtemadul Haque
Time: 1:17 AM
Date: 8/3/2023

Modification Starting for 1805035


OPEN CLOSED PRINCIPLE:

● OPEN FOR EXTENSION


● CLOSED FOR MODIFICATION

The Principle is simple: we will design classes such that they can be
extended, that is we can have more variation in behavior but the
internal structure of the source code will not change; rather we will
extend it to more classes.

The Behaviour are OPEN for extension and can be modified to have

variation within themselves.


But the internal code base cannot be modified.They are CLOSED for
modification
This can be achieved in two ways:

1. Inheritance

2. Composition

Inheritance : We can create a base class which will have the all the

functionality that our desired class should have then we will modify those

functionality by extending that particular class.

Now an example from class of how inheritance can be used:


Though only one is shown the rest of the implementation are the same

We can now make another class which can handle Database Storage
We can now create a new class with the functionality

But the problem with inheritance is it is not flexible and robust to other

changes:
Classes and objects created through inheritance are tightly coupled

because changing the parent or superclass in an inheritance relationship

risk breaking the code

Inheritance cannot handle this design because it will violate and we also

want it to add Found functionality which might be redundant in others

and unnecessary
Composition :We can achieve composition through adding new classes as

private variables and give them through dependency injection or just normally

assigning though we have normally assigned but dependency injection is

preferred
Then we can use these components to extend our functionality

So composition is preferred over inheritance


Edited by:
Student ID: 1805035
Name: Saleh Sakib Ahmed
Time: 1:17 pM
Date: 8/3/2023

Modification Starting for 1805045

COMPOSITION OVER INHERITANCE PROS and CONS:

Composition and inheritance are two common ways of


achieving code reuse in object-oriented programming.
Both have their strengths and weaknesses, and the
choice between them depends on the specific needs of a
project.

Composition design pattern:

Pros:
1.Composition allows for more flexible and modular
code. By creating objects that consist of other
objects, you can create complex systems with
smaller, reusable components.
2.Composition promotes code reuse without creating
complex class hierarchies. It is much easier to add
or remove functionality by modifying the
composition of objects than by changing an
inheritance hierarchy.
3.Composition allows us to change the behavior of an
object at runtime. We can dynamically add or remove
components from an object to alter its behavior
without changing its interface
4.Composition can help to reduce coupling between
classes. By relying on interfaces and delegation,
we can create more loosely coupled systems that are
easier to maintain and modify.

Cons:
1.Composition can lead to increased memory usage and
performance overhead, as objects may contain many
other objects that need to be created and managed.
2.Composition can result in more verbose code, as we
need to create and manage objects explicitly.
3.Composition can be more difficult to understand
than inheritance, as it requires a different way of
thinking about objects and their relationships.

Inheritance:
Pros:
1.Inheritance provides a clear and simple way to
create classes that share common functionality. By
defining a base class and deriving subclasses from
it, we can create a hierarchy of classes that share
common behaviors and interfaces.
2.Inheritance can help to reduce code duplication. By
defining common functionality in a base class, we
can avoid writing the same code in multiple places.
3.Inheritance promotes code reuse by allowing
subclasses to inherit behavior from their parent
classes.
4.Inheritance can be easier to understand than
composition, as it is a more intuitive way of
organizing classes and their relationships.
Cons:
1.Inheritance can lead to tight coupling between
classes, making it more difficult to modify and
maintain code.
2.Inheritance can result in complex class hierarchies
that are difficult to understand and navigate.
3.Inheritance can make it difficult to change the
behavior of an object at runtime, as it is defined
by its class hierarchy.
4.Inheritance can introduce hidden dependencies, as
changes to a base class can affect all subclasses
that inherit from it.

Overall, while inheritance can be useful in certain


situations, composition is generally considered to be a
more powerful and flexible design pattern, particularly
in larger and more complex projects where
maintainability and scalability are critical factors.

Edited by:
Student ID: 1805045
Name: Md.Hasanul Islam.
Time: 11:00 am
Date: 9/3/2023

You might also like