SOLIDifying For Robust and Scalable Code

Alya Azhar Agharid
20 min readMar 5, 2023

--

Photo by AltumCode on Unsplash

Today’s programming industry has seen tremendous growth in the complexity of software. We should enhance our code by applying the best programming practices to increase its quality and maintainability and adhere to high industry standards.

Among the various concepts aimed at helping software developers improve their software development are Object-Oriented (OO) and SOLID principles. Let’s now discuss them both!

Object-Oriented (OO) Principles

In software development, the OO paradigm has been used extensively and has grown in popularity. A collection of recommendations known as the “OO Principles” aids programmers in building modular, scalable, and maintainable systems. Let’s talk about the guiding concepts!

1 — Encapsulation

This principle aims to hide the internal state of objects from the outside with a clear separation between the internal state of objects and external behavior.

It can be done by defining the object’s internal state as private and it can be displayed only by calling a public method. The process of calling and changing state objects is only done with public methods, not by direct access to the state object.

Encapsulation makes the interface clean and well-defined to interact with other objects, preventing unwanted changes in object states, and also safe and great maintenance.

class JobVacancy:
def __init__(self, position):
self.__position = position
self.__numApp = 0 #the number of applicants

def get_position(self):
return self.__position

def get_num_applicants(self):
return self.__numApp

def apply(self):
self.__numApp += 1
print(“Apply Success”)
# create a new job vacancy
new_job_vacancy = JobVacancy(“Data Scientist”)

# the code below is trying to access the number
# of applicants directly (which is encapsulated)
print(new_job_vacancy.__numApp) # will raise an AttributeError

# access the private attributes by public methods
print(new_job_vacancy.get_position()) # will print “Data Scientist”

# try to apply to the job which is encapsulated by accessing public methods
new_job_vacancy.apply() # this will print "Apply Success"
print(new_job_vacancy.get_num_applicants()) # will show 1 applicants

2 — Abstraction

By omitting aspects that are unrelated to a specific class, this principle concentrates on the generic properties of objects. The goal of abstraction is to make representations more understandable and practical.

Abstraction can be achieved by creating abstract classes or interfaces that a class can’t be instantiated directly but can provide base classes for other classes to inherit.

Abstract Class
The class has a shared set of properties and methods from which its descendants can develop their unique functionality.

Interface
An interface, as opposed to a set of methods, must be implemented by a class that is part of the interface.

Abstraction increases code reuse (same functionality between classes), loose coupling, and code is easier to maintain and update later.

from abc import ABC, abstractmethod

class JobVacancy(ABC):
def __init__(self, title, company):
self.title= title
self.company = company

@abstractmethod
def job_requirement(self):
pass

class DataScientist(JobVacancy):
def job_requirement(self):
str_reqs = f"{self.title} at {self.company}"
return str_reqs
Example of Abstraction

By the use of abstraction, we may design a common structure for classes that share a common behavior while still allowing for unique implementation details in each concrete subclass, as seen in the example.

3 — Inheritance

This principle permits objects to acquire attributes and behavior from another class. Creating a derived class (child class) allows for the addition or modification of derived characteristics and behavior to the needs of the class.

It also carries polymorphism, which classes can form in various forms. And inheritance also helps to make our code less duplication, improve code reuse, flexible and adaptable code, and increase the scalability

class News:
def __init__(self, title, content):
self.__title= title
self.__content = content
def get_title(self):
return self.__title
def get_content(self):
return self.__content

class JobVacancyNews(News):
def __init__(self, title, content, company):
super().__init__(title, content)
self.__company = company
def get_company(self):
return self.__company
# more job vacancy news
Example of Inheritance

4 — Polymorphism

This approach enables objects to take on a variety of shapes and show a variety of behaviors. Without knowing the actual type, different types of objects can be handled as if they were of the same type.

Method Overloading/Compile-time Polymorphism/Static Polymorphism
It allows a class with multiple methods with the same name but different parameters in a class.

Method Overriding/Runtime Polymorphism/Dynamic Polymorphism
The derived class overrides methods from the base class

The flexibility, modularity, and generality of the code are all increased by polymorphism. Generally speaking, code is simpler to maintain and more responsive to requirements changes.

class JobVacancy():
def __init__(self, title, content, company):
self.title= title
self.content = content
self.company = company
def display(self):
pass

class JobBriefDesc(JobVacancy):
def display(self):
str_display = f"{self.title} in {self.company} tells {self.content}"
print(str_display)
return str_display

class JobFullDesc(JobVacancy):
def display(self):
str_display = f"Title:{self.title}\nCOmpany:{self.company}\nDescription:{self.content}"
print(str_display)
return str_display

The code above shows us that we can create either JobBriefDesc or JobFullDesc and call the “display()” method, but it can behave depending on what type of instance we called. So, this allows us to treat different types of JobVacancy as they were in the same type but still provides the appropriate information depending on the type.

5 — Composition

To create more complex objects, some items can be combined using a composition which means combining simpler objects. Composition objects, or components, are composite objects of several objects. Components manage their objects, so components can be removed independently.

Composition is loose-coupling, in contrast to inheritance which is high coupling.

class Company:
def __init__(self, name, location):
self.name = name
self.location = location
self.job_vacancies = []
def add_job_vacancy(self, job):
self.job_vacancies.append(job)
def delete_job_vacancy(self, job):
self.job_vacancies.remove(job)

class JobVacancy():
def __init__(self, title, content, company):
self.title= title
self.company = company

We can see in the code above that the Company object represents a company that also has a list of another single object called JobVacancy that the Company offers. So, with composition, the code has a relationship between JobVacancy and Company, the Company object is the one called component.

Software developers gain benefits from OO standards by reducing code duplication, maximizing code reuse, and improving the project’s modularity and scalability. It offers a structure for well-planned, modular, and reusable software programs.

What’s The Correlation Between OO and SOLID Principle?

Object-Oriented Principles are foundations for designing and implementing software systems in an object-oriented paradigm. Nowadays, as the age goes on, additional principles to limit and challenge the OO design have been developed and it’s called SOLID Principles, a set of five guidelines to improve the maintainability, scalability, and flexibility of software systems, building upon the foundations of OO principles.

S.O.L.I.D Principles

Source: https://www.boxpiper.com/

Robert J. Martin (well known as Uncle Bob), in 2000 introduced SOLID principles. SOLID principles itself is a set of five principles of Object-Oriented class design (OO-Principles earlier).

SOLID is a set rule of best practices to make code more scalable, maintainable, and easier to understand. The five points of principles give an understanding of software architecture in general and each of the points focuses on a specific aspect of software development. If it’s applied together, they will have comprehensive guidelines to create more robust code.

SOLID principles have the same purpose as the clean code, OO architecture, and design patterns are:
“To create understandable, readable, and testable code that many developers can collaboratively work on.”

S — Single Responsibility Principle (SRP)

The class should do one thing and only have one single reason to change. It’s emphasized that each class should only have a single responsibility/job and have to do it well.

The purpose is to avoid classes having multiple responsibilities and ending up harder to understand, test, maintain, and modify. It can help code simple and easy to maintain, organized for better understanding, and easy to modify and test. SRP can make the system more robust and reliable.

A common fallacy that happened in SRP is assuming the function of the class is too narrow, it effects creating too many classes or methods, which is inefficient, harder, and more complex. Also, it is wrong to assume that a class only can have a single/one method to make sure it has only one job, nope, that’s wrong. Look out at this example:

class UserProfile:
def __init__(self, name, npm):
self.__name = name
self.__npm = npm
def get_name(self):
return self.__name
def get_npm(self):
return self.__npm
def set_name(self, new_name):
self.__name = new_name
def set_npm(self, new_npm):
self.__npm = new_npm
def apply_job(self, job):
# implement code to apply for a job
def fill_questionnaire(self, quest):
# impelement code to fill a questionnaires

The code above is not a good example of SRP because the class UserProfile has no single responsibility to save data of user profile only, but also applying for a job and filling out questionnaires. So, the best practice that follows SRP is as follows:

class UserProfile:
def __init__(self, name, npm):
self.__name = name
self.__npm = npm
def get_name(self):
return self.__name
def get_npm(self):
return self.__npm
def set_name(self, new_name):
self.__name = new_name
def set_npm(self, new_npm):
self.__npm = new_npm

class UserApplicant:
def apply_job(self, job):
# implement code to apply for a job

class Questionnaire:
def fill_questionnaire(self, quest):
# impelement code to fill a questionnaire

Yay! The code is a lot easier to understand because the functionality of the class is already separated. And so if we want to modify the code for the questionnaire it’s don’t bother the UserProfile class.

O — Open/Closed Principle (OCP)

Software entities (classes, modules, functions, etc.) should be open for extension but closed to modification. This emphasizes that new functionality can be added (extension) without changing the existing code (modification).

Modifying the existing code means taking risk of potential bugs. It can be achieved by creating interfaces and abstract classes (look at the difference above in OO-Principle). OCP helps to make codes have better maintainability and flexibility to adapt to changing requirements.

A common fallacy that happened in OCP is assuming that the code is closed to modification, even when it’s not necessary. So that makes the code difficult to maintain and understand.

class JobApply:
def process_apply(self, company, title):
if company == "Google":
# Process the apply on Google
self.process_job_apply_google(title)
elif company == "Gojek":
# Process the apply on Google
self.process_job_apply_gojek(title)
elif company == "Tokopedia":
# Process the apply on Google
self.process_job_apply_tokopedia(title)

def process_job_apply_google(self, title):
# Code to process apply on Google

def process_job_apply_gojek(self, title):
# Code to process apply on Gojek

def process_job_apply_tokopedia(self, title):
# Code to process apply on tokopedia

Nope, the JobApplyclass violates the OCP principle, it is not closed for modification. If a new company was added, the conditional statement needs to be added (need to be modified), we would have to modify the process_apply method and introduce another conditional case. This violates the principle, as the class should be open for extension without modifying its existing code.

A better approach to adhere to the OCP would be to use abstraction and polymorphism. Here’s an updated version:

from abc import ABC, abstractmethod

class JobApply:
def process_apply(self, company: Company, title):
company.company_process_job_apply(title)

class Company(ABC):
@abstractmethod
def company_process_job_apply(self, title):
# Process the job apply

class Google(Company):
def company_process_job_apply(self, title):
# Process the job apply

class Tokopedia(Company):
def company_process_job_apply(self, title):
# Process the job apply

class Gojek(Company):
def company_process_job_apply(self, title):
# Process the job apply

See? In this revised code, the JobApply class accepts a Company object and calls its company_process_job_applyabstract method, without needing to know the specific implementation details. Now, if a new company needs to be added, you can simply create a new subclass of Company and implement the company_process_job_apply method without modifying the existing code in the JobApply class. This adheres to the OCP by allowing the class to be open for extension without modification.

L — Liskov Substitution Principle (LSP)

A class should be replaceable by any of its subclasses without affecting the correctness of the program, which also states that subclasses should be interchangeable with their base classes.

This is obvious, because, in inheritance,
the subclass may include or extend all of the parent class’s features.

LSP helps us to have our code greater flexibility and easier to modify, improve code reuse, and have better testing (more effective).

Thinking that inheritance always uses an “is-a” relationship is a common fallacy in LSP. Another fallacy is that violates the pre-and-post-conditions and also invariants the parent class. The example violates the conditions related to the parent class as follows:

class Job:
def __init__(self, title, salary):
self.title = title
self.salary = salary

def get_salary(self):
return self.salary

class FullTimeJob(Job):
def __init__(self, title, salary):
super().__init__(title, salary)

def get_salary(self):
return self.salary + 1000000

Uh-huh🚫🙅🏻. That’s not true.

The FullTimeJob subclass extends the Job class and overrides the get_salary method to add a fixed amount to the base salary. However, this violates the LSP because the behavior of the FullTimeJob subclass is not consistent with the behavior of the Job superclass.

According to the LSP, if you have code that expects a Job object and you substitute it with a FullTimeJob object, the code should still work correctly. However, in this case, calling get_salary on a FullTimeJob object will give a different result compared to calling it on a Job object.

We can change it as follows:

class SalaryCalculator:
def calculate_salary(self, base_salary):
return base_salary

class Job:
def __init__(self, title, salary):
self.title = title
self.salary = salary

def get_salary(self):
return self.salary


class FullTimeJob(Job):
def __init__(self, title, salary):
super().__init__(title, salary)
self.salary_calculator = SalaryCalculator()

def get_salary(self):
base_salary = super().get_salary()
return self.salary_calculator.calculate_salary(base_salary)


class SeniorFullTimeJob(FullTimeJob):
def __init__(self, title, salary):
super().__init__(title, salary)

def get_salary(self):
base_salary = super().get_salary()
return self.salary_calculator.calculate_salary(base_salary + 1000000)

Yay!🙌🏻 In this modified design, a new SalaryCalculator class is introduced to handle the salary calculation. The Job class remains unchanged, serving as the base class. The FullTimeJob class, instead of directly modifying the behavior of Job, uses the SalaryCalculator to calculate the salary based on the base salary. The SeniorFullTimeJob subclass can also utilize the SalaryCalculator for calculating a different salary with an additional amount.

By separating the salary calculation responsibility into a separate class, the design adheres to the Liskov Substitution Principle. The Job, FullTimeJob, and SeniorFullTimeJob classes can be used interchangeably, without any modifications to the base class behavior.

I — Interface Segregation Principle (ISP)

The word “Segregation” has meaning to keep things separated. This principle emphasizes of a small and focused interface only depends on the functionality that is necessary, and should not be implemented the interfaces that do not need it. ISP makes code has better organization, reduced coupling, and increased flexibility.

Interfaces need to be fine-grained and specific.

Common mistakes in ISP are the interfaces that mean to keep things separated too fine-grained or too large, so it creates unnecessary complexity of functionality.

from abc import ABC, abstractmethod

class Job:
def __init__(self, title, description, salary):
self.title = title
self.description = description
self.salary = salary

class JobManage(ABC):
@abstractmethod
def apply(self):
# Code to apply for the job
pass

class DisplayableJob(ABC):
@abstractmethod
def get_description(self):
# Code to display job description
pass

@abstractmethod
def get_salary(self):
# Code to display job salary
pass

class FullTimeJob(Job, JobManage, DisplayableJob):
def apply(self):
# Implementation for applying to a full-time job
pass

def get_description(self):
# Implementation for retrieving the full-time job description
pass

def get_salary(self):
# Implementation for retrieving the full-time job salary
pass

class FreelanceJob(Job, DisplayableJob):
def get_description(self):
# Implementation for retrieving the freelance job description
pass

def get_salary(self):
# Implementation for retrieving the freelance job salary
pass

class JobSearchApp:
def __init__(self, jobs):
self.jobs = jobs

def display_jobs(self):
for job in self.jobs:
print(f"{job.title}: {job.get_description()} - Salary: {job.get_salary()}")
if isinstance(job, JobManage):
job.apply()

Hmm, the JobManage interface contains the apply method, while the DisplayableJob interface contains the get_description and get_salary methods. The FullTimeJob class implements both interfaces since it needs to handle job application and display job details. However, the FreelanceJob class only implements the DisplayableJob interface because freelancers cannot apply through the JobManage interface.

The JobSearchApp class checks if a job object is an instance of JobManage before calling the apply method. This violates the ISP because the JobSearchApp depends on an interface that has multiple methods, even though it doesn't need to use all of them.

from abc import ABC, abstractmethod

class Job:
def __init__(self, title, description, salary):
self.title = title
self.description = description
self.salary = salary

class JobManage(ABC):
@abstractmethod
def apply(self):
# Code to apply for the job
pass

class DisplayableJob(ABC):
@abstractmethod
def get_description(self):
# Code to display job description
pass

@abstractmethod
def get_salary(self):
# Code to display job salary
pass

class FullTimeJob(Job, JobManage, DisplayableJob):
def apply(self):
# Implementation for applying to a full-time job
pass

def get_description(self):
# Implementation for retrieving the full-time job description
pass

def get_salary(self):
# Implementation for retrieving the full-time job salary
pass

class JobSearchApp:
def __init__(self, jobs):
self.jobs = jobs

def display_jobs(self):
for job in self.jobs:
self.display_job_info(job)

def display_job_info(self, job):
print(f"{job.title}: {job.get_description()} - Salary: {job.get_salary()}")
if isinstance(job, JobManage):
self.apply_to_job(job)

def apply_to_job(self, job):
job.apply()

Hooray! The JobSearchApp class now separates the responsibilities of displaying job information and applying to jobs. The display_job_info method handles the display logic, calling the appropriate methods on the job object.

By separating the responsibilities and ensuring that the JobSearchApp class only depends on the relevant interfaces, we have resolved the violation of the Interface Segregation Principle (ISP).

D — Dependency Inversion Principle (DIP)

In Uncle Bob’s article (2000), Uncle Bob summarizes the DIP principle as follows:
“If the OCP states the goal of OO architecture, the DIP states the primary mechanism”.

DIP mentioned that the high-level modules should not depend on the low-level modules. but both should depend on abstractions.

What is High-level and low-level module?

1. High-level/Functional Module — high-level abstraction to a more extensive functionality.
2. Low-level/Utility Module — low-level abstraction and encompasses a limited set of functionalities.

This concept emphasizes the importance of depending upon interfaces or abstract classes instead of concrete classes. It is related to OCP Principle because it wants to class to be opened to extension. The DIP can help to reduce coupling, improve testability, and increase modularity so that easier to maintain.

A Common fallacy for DIP is the use of interfaces or abstractions that are too generic or too specific, it can make unnecessary implementation details or lack of flexibility and reuse. Look at this code below!

class DataSavedJob:
def __init__(self):
self.data = {}
def save(self, job):
# code here
def delete(self, id):
# code here

class JobVacancy:
def __init__(self, savedJob:DataSavedJob):
self.saved_job = savedJob
def save_the_job(self, job):
self.saved_job.save(self)
def delete_the_job(self, id):
self.saved_job(self.id)

Eum..No🙅🏻 That’s not good. Look at the code, the high-level functionality in JobVacancyclass depends on the low-level functionality called DataSavedJobclass. This violates the DIP principle, now, how to fix it? We should create instances so that the high-level module has no dependencies on low-level modules, better depending on an interface or abstract class, like this!

from abc import ABC, abstractmethod

class JobRepository(ABC):
@abstractmethod
def save(self, job):
pass

@abstractmethod
def delete(self, id):
pass

class DataSavedJob(JobRepository):
def __init__(self):
self.data = {}

def save(self, job):
# Code to save the job
pass

def delete(self, id):
# Code to delete the job
pass

class JobVacancy:
def __init__(self, saved_job: JobRepository):
self.saved_job = saved_job

def save_the_job(self, job):
self.saved_job.save(self)

def delete_the_job(self, id):
self.saved_job.delete(id)

Yay! JobRepository interface is an abstraction. Both the DataSavedJob class and the JobVacancy class depend on this interface rather than the concrete implementation. The DataSavedJob class implements the JobRepository interface, providing the specific implementation details for saving and deleting jobs.

The JobVacancy class now receives an instance of the JobRepository interface in its constructor, allowing it to work with any class that implements the interface. This decoupling of dependencies adheres to the Dependency Inversion Principle.

Advantages & Backwards Of Using SOLID Principle

Advantages:

  1. Maintainability
    SOLID principles promote code that is easy to understand, modify, and maintain. By adhering to these principles, we create code that is modular, loosely coupled, and follows good design practices. This makes it easier to make changes or extend the system without introducing unexpected side effects.
  2. Testability
    SOLID principles encourage code that is easier to test. By applying principles such as Dependency Inversion, you can easily substitute dependencies with mock objects or test doubles during unit testing. This allows for more isolated and effective testing, leading to better code quality.
  3. Flexibility and Extensibility
    SOLID principles make your code more flexible and extensible. Through the use of abstraction, interfaces, and dependency injection, you can easily introduce new implementations, swap dependencies, and extend functionality without modifying existing code. This reduces the risk of introducing bugs and improves the overall maintainability of the system.
  4. Code Reusability
    SOLID principles promote modular and reusable code. By separating concerns, following the Single Responsibility Principle, and adhering to the Open-Closed Principle, you create code components that can be easily reused in different parts of the system or even in other projects. This saves development time and effort by avoiding redundant code and promoting a more efficient and scalable codebase.

Backward:

  1. Increased Complexity:
    Additional complexity to the codebase. Applying these principles often requires introducing abstractions, interfaces, and additional layers of indirection, which can make the code harder to understand for developers who are not familiar with the principles. It may also increase the initial development time as more thought and design considerations are required.
  2. Over-Engineering:
    In some cases, strictly following SOLID principles without careful consideration may lead to over-engineering. It’s important to strike a balance between adhering to SOLID principles and practicality. Sometimes, the added abstractions or complexity introduced to satisfy the principles may not be necessary for the specific context, resulting in unnecessary overhead and reduced efficiency.
  3. Learning Curve:
    SOLID principles require developers to have a good understanding of object-oriented design principles and best practices. Applying SOLID principles effectively may require additional training, knowledge, and experience. It may take time for developers to grasp the concepts and apply them correctly in real-world scenarios.

The Best Time to Apply OOP & SOLID Principle

When?

  1. Complex Systems
    OOP and SOLID principles are well-suited for building complex systems where modularity, maintainability, and scalability are crucial. When dealing with intricate business requirements and multiple interacting components, OOP helps in organizing code into manageable and reusable units, while SOLID principles ensure the codebase remains flexible, extensible, and easy to maintain.
  2. Large Development Teams
    OOP and SOLID principles are advantageous when multiple developers are working on the same codebase. By adhering to a common set of design principles and patterns, OOP and SOLID promote code consistency, readability, and collaboration. This enables team members to understand and modify each other’s code more easily.
  3. Long-Term Projects
    OOP and SOLID principles are beneficial in long-term projects where the codebase is expected to evolve and grow over time. By following SOLID principles, you can create code that is more resilient to change, allowing for easier maintenance and future enhancements. OOP’s encapsulation and abstraction help in managing complexity and isolating changes to specific components.

When OOP and SOLID may not be suitable?

  1. Small, Simple Projects
    For small and straightforward projects with limited complexity and short development cycles, strict adherence to OOP and SOLID principles may introduce unnecessary overhead and complexity. Applying SOLID principles to such projects may be over-engineering and can lead to diminished productivity. It’s important to strike a balance between the benefits of OOP and SOLID and the specific needs of the project.
  2. Rapid Prototyping
    During the initial stages of prototyping and experimentation, flexibility and quick iterations may be more important than strictly adhering to OOP and SOLID principles. Rapid prototyping often involves exploratory coding, quick modifications, and disposable code. In such cases, a more flexible and iterative approach may be preferred over upfront design and adherence to SOLID principles.

SOLID Vs. Performance Matters

Source: Educative.io

“Do SOLID design principles make code slow?” 🤔🤔

Positive Ways:

  1. Code Organization
    Encourage modular code organization, making it easier to understand and navigate the codebase. By separating concerns and enforcing clear boundaries between components, SOLID principles promote code that is more readable and maintainable.
  2. Single Responsibility
    Advocates that a class should have only one reason to change. This helps in creating smaller, focused classes that are easier to comprehend and reason about. Understanding the purpose and behavior of a class becomes more straightforward when it has a single responsibility.
  3. Loose Coupling
    SOLID principles promote loose coupling between components through techniques like Dependency Injection (DI) and Inversion of Control (IoC). Loose coupling reduces the interdependencies between modules, making it easier to understand and modify individual components without affecting others.

Negative Ways:

  1. Method Call Overhead
    Method call overhead refers to the extra time and resources required to resolve and execute a method when using interfaces and abstraction layers. In SOLID principles, we often define interfaces or abstract classes to represent the behavior of objects, and concrete implementations provide the actual functionality.
  2. Indirection
    SOLID principles often introduce an additional layer of indirection, as code interacts with abstractions rather than concrete implementations. This indirection can lead to reduced performance due to the extra levels of indirection and memory access required.
  3. Over-Engineering
    Over-engineering refers to the act of adding more complexity, features, or abstractions to a system than necessary. In the context of SOLID principles and performance considerations, over-engineering can occur when we strictly adhere to SOLID principles without carefully evaluating their impact on performance. SOLID principles promote good code design practices and help create maintainable and flexible systems. However, blindly following these principles in situations where performance is crucial can lead to unnecessary complexity and decreased performance without significant benefits.

Balancing SOLID and Performance:

Balancing SOLID principles and performance involves finding the right trade-off between code design (SOLID) and optimization for efficient execution (Performance):

  1. Identify Performance-Critical Areas
    Focus performance optimization efforts on the critical areas of the codebase, where performance gains have the most significant impact. Consider optimizing those areas while maintaining SOLID principles in less critical parts.
  2. Performance Profiling
    Conduct thorough performance profiling to identify the actual bottlenecks in the system. This helps target optimizations to areas that provide the most significant performance improvements.
  3. Selective Application of SOLID Principles
    In performance-sensitive areas, selectively apply SOLID principles, relaxing certain aspects if necessary, to optimize for performance. Maintain SOLID principles in other parts of the codebase to preserve code maintainability and understandability.
  4. Micro-Optimizations
    Employ micro-optimizations and performance-tuning techniques to mitigate any performance overhead introduced by SOLID principles. However, be cautious not to compromise code readability or maintainability in pursuit of performance gains.

Therefore, as we’ve seen, this is one possibility given the potential rise in indirection levels and the quantity of layers of abstraction. To put it another way, we might be compelled to use additional method calls and combine the data we require from various tiny objects.

The best advice is to go with a minimal, yet clean, design that emphasizes the separation of interests. If there are any performance issues, runtime tools like a profiler should be used to measure them. The initial design shouldn’t be changed to make the code faster unless a clear bottleneck is found.

SOLID Implementation in PPL C06

In the example below is the implementation of the function in views.py in the JobSeeker application. SOLID implementation is in the SRP or Single Responsibility Principle in this DetailLowonganView.

The functions in the class below have implemented their respective independent functions, for example:

  1. Method form_valid to handle storing the results of the application form in the database
  2. Method lamaran_is_validto see if the jobvacancy object you want to apply for is valid.
  3. Method post to respond to the request form to apply by first checking the role of the user who will apply for the job
  4. Method get_context_data to retrieve data from the corresponding vacancy details
  5. Method test_func to test that the job details are accessible if the user is authenticated
class DetailLowonganView(FormMixin,UserPassesTestMixin,DetailView):
model = Lowongan
form_class = LamarForm
template_name = 'detail_lowongan.html'
context_object_name = 'detail_lowongan'
login_url = LOGINURL
success_url = '/riwayat-lamaran/'


def form_valid(self, form):
form.save()
return super().form_valid(form)

def lamaran_is_valid(self):
lowongan = Lowongan.objects.get(id=self.kwargs.get('pk'))
already_lamar = Lamar.objects.filter(users_id=self.request.user.id,lowongan_id=self.kwargs.get('pk')).exists()
return not already_lamar and lowongan.is_open

def post(self, request, *args, **kwargs):
if request.user.role_id == 1:
self.object = self.get_object()
form = self.get_form()
if form.is_valid() and self.lamaran_is_valid():
return self.form_valid(form)
else:
return self.form_invalid(form)
else:
messages.info(request, "Anda tidak memiliki akses.")
return redirect(LOGINURL)

def get_context_data(self, **kwargs):
context = super().get_context_data(**kwargs)
context['is_alumni'] = self.request.user.role_id == 1
already_lamar = Lamar.objects.filter(users_id=self.request.user.id,lowongan_id=self.kwargs.get('pk')).exists()
context['already_lamar'] = already_lamar
return context

def test_func(self):
return self.request.user.is_authenticated

Here, Let’s Sum Up!

Object-Oriented (OO) and SOLID Principles are widely used as two of the best practice in software development. The OO-Principle paradigm tends to the construction of objects that can hold data and methods for manipulating that data. SOLID Principles are a set of five guidelines for creating extendable, scalable, and maintainable code.

OO-Principle helps a lot with organized code so we as developers can easily manage and modify different parts of the system without disrupting the code that has been written before. And of course, it helps a lot for the reusability of code. It completes with SOLID Principles that helps developer to create code that is easier to maintain, extend, and scale. By understanding and implementing those two principles, software development can be more maintainable, extendable, and scalable.

--

--

Alya Azhar Agharid
Alya Azhar Agharid

Written by Alya Azhar Agharid

girl who likes to read, write, and tell.

No responses yet