325 Assignment NOTES LECTURE 2
325 Assignment NOTES LECTURE 2
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)
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.
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.
Logging:
Below a sample declaration for a class named FileStore is given.
public class FileStore
{
private string _path;
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) {
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");
}
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
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.
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)
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.
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.
public ProductService() {
// initialize the cache
cache = new StoreCache();
}
class StoreCache {
private DistributedCache cache;
public StoreCache() {
// initialize the distributed cache
cache = new RedisCache();
}
public boolean hasKey(String key) {
return cache.hasKey(key);
}
interface DistributedCache {
boolean hasKey(String key);
Object get(String key);
void put(String key, Object value);
}
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
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
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
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
Inheritance cannot handle this design because it will violate and we also
and unnecessary
Composition :We can achieve composition through adding new classes as
private variables and give them through dependency injection or just normally
preferred
Then we can use these components to extend our functionality
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.
Edited by:
Student ID: 1805045
Name: Md.Hasanul Islam.
Time: 11:00 am
Date: 9/3/2023