How to Write a SOLID Code

The most crucial thing software engineers must remember is that others will view their code. In addition, other engineers would like to build other software applications using your code. Therefore, you need to write neat code that allows your code to be reused with minimal changes. Let's see how to write a SOLID code that is easy to understand and spot bugs.

Updating a code is very costly, and frequent changes can lead to the death of the code base. In addition, this does not facilitate easy integration with new additions.

Robert Martin initially identified these principles in the late 1990s as a standard for writing code. Software engineers have followed these principles of SOLID formally ever since.

Five Principles of Writing a SOLID code

Before you understand how to write a SOLID code, you must understand its principles. These five principles are designed to create simple codes that are easy to understand, maintain, and expand. SOLID principles become essential when a group works collectively on a codebase consisting of thousands of lines of code.

Single Responsibility Principle (SRP)

According to the first principle, a class should have only one straightforward job and responsibility. Therefore, there will be only one reason why a class needs to be changed or improved. On the other hand, if your class has numerous responsibilities, you must change them often.

You need to ask yourself a simple question before you make any changes to your code: What is the responsibility of my class? If your answer includes "and," you're breaking the single responsibility principle.

To effectively implement this principle, you must divide the responsibilities into different classes. Therefore, this principle makes it easier to enforce software while preventing any unexpected side effects after any changes in the future. If you want to increase of chances of abiding by SRP, the easiest way is to avoid writing large and complex classes for your codebase.

class Album:
    def __init__(self, name, artist, songs) -> None:
        self.name = name
        self.artist = artist
        self.songs = songs    def add_song(self, song):
        self.songs.append(song)    def remove_song(self, song):
        self.songs.remove(song)     def __str__(self) -> str:
        return f"Album {self.name} by {self.artist}\nTracklist:\n{self.songs}"    # breaks the SRP
    def search_album_by_artist(self):
        """ Searching the database for other albums by same artist """
        pass

The developer has created a class Album in the above example, which stores the album name, artists, and tracklist. The class can manipulate the data by adding or deleting songs. However, if the developer decides to add a search function, then they would end up breaking SRP.

Open-Closed Principle (CR)

Robert labelled this an essential object-oriented design principle; it states that all software entities should be open for extension but closed for modifications. Since the requirements can change over time, the goal of the Open-Closed Principle is to create a code that does not need to be changed every time there is a change in requirements.

Moreover, CR also states that even if the requirements change, you should incorporate these changes by adding new lines to the codebase instead of changing the old code. Therefore, CR will allow you to write a SOLID code that is robust, easily maintainable, and reusable.

There are two main ideas in CR that every software developer should know:

  • First, the Open idea states that adding a sub-class should not induce change in the base class and interfaces.
  • The Closed idea states that the coder needs to be confident that the base classes or interfaces will not be modified later.
#after
class SearchBy:
    def is_matched(self, album):
        pass
      
class SearchByGenre(SearchBy):
    def __init__(self, genre):
        self.genre = genre    def is_matched(self, album):
        return album.genre == self.genre
    
class SearchByArtist(SearchBy):
    def __init__(self, artist):
        self.artist = artist    def is_matched(self, album):
        return album.artist == self.artist
    
class AlbumBrowser:
    def browse(self, albums, searchby):
        return [album for album in albums if searchby.is_matched(album)]

Instead of writing new functions, you can define a base class that operates with a common interface to meet your specifications. Later, you can specify the subclasses of each specification to inherit the interface from the base class. This will allow you to extend your search to another class whenever required.

Liskov Substitution Principle (LSP)

LSP requires subTypes to replace its BaseType, which means you can substitute the objects present in a subclass with objects of their superclass. However, the subclass objects need to behave similarly to their superclasses.

Moreover, the Liskov Substitution Principle is related to inheritance and is an extension of CR as it focuses on the behaviour of a superclass and its subtypes. If a superclass from your SOLID code can do a job, then the subclass should also be able to do it.

class Person():
    def walkNorth(meters):
        pass
    def walkSouth(meters):
        passclass Prisoner(Person):
    def walkNorth(meters):
        pass 
    def walkSouth(meters):
        pass

In this example, a person from the superclass can easily walk around but keeping a prisoner as a subclass is wrong and breaks LSP. Since the prisoner is not free to move arbitrary distances, you should not allow walk methods for the class.

Interface Segregation Principle (ISP)

ISP states that there is no need to force a client to depend upon interfaces that are not necessary. This simply means you need to split large interfaces into smaller specific ones while writing a SOLID code. Therefore, classes should only have methods that make sense for each function, and the subclasses should inherit these functions.

Software engineers use the abstract method to create an interface within a base class without implementation. Instead, the software engineer can force the implementation of these interfaces on every subclass present in the base class.

from abc import ABCMeta
class PlaySongsLyrics:
    @abstractmethod
    def sing_lyrics(self, title):
        pass
class PlaySongsMusic:
    @abstractmethod
    def play_guitar(self, title):
        pass    @abstractmethod
    def play_drums(self, title):
        pass
class PlayInstrumentalSong(PlaySongsMusic):
    def play_drums(self, title):
        print("Ba-dum ts")    def play_guitar(self, title):
        print("*Soul-moving guitar solo*")
class PlayRockSong(PlaySongsMusic, PlaySongsLyrics):
    def play_guitar(self):
        print("*Very metal guitar solo*")    def sing_lyrics(self):
        print("I wanna rock and roll all night")    def play_drums(self, title):
        print("Ba-dum ts")

Let's supposed you used an abstract method to create an interface for your base class that has no implementation. However, it forces you to implement it in every subclass that inherits the interface from the base class. To make things easier, you can create a separate class for singing and music or split them even more by instrument.

Dependency Inversion Principle (DIP)

According to the last SOLID principle, abstractions should not depend on details; the details should depend on abstractions. In simple terms, a high-level module should not rely on a low-level module.

DIP is closely related to CR and LSP because both principles depend on interfaces. Moreover, if your code contains well-defined abstract interfaces, your code will not break if you decide to change the internal implementation of one class.

Initially, Robert built a code metric for DIP that would compare the Abstractness and Instability of a code base. For example, you can consider any code that contains numerous abstract classes and interfaces with few concrete types as an abstract. Similarly, you can consider a code stable if it's challenging to modify or change.

class GeneralAlbumStore:
    @abstractmethod
    def filter_by_genre(self, genre):
        passclass MyAlbumStore(GeneralAlbumStore):
    albums = []    def add_album(self, name, artist, genre):
        self.albums.append((name, artist, genre))    def filter_by_genre(self, genre):
        if album[2] == genre:
            yield album[0]class ViewRockAlbums:
    def __init__(self, album_store):
        for album_name in album_store.filter_by_genre("Rock"):
            print(f"We have {album_name} in store.")

If your albums are stored in a tuple, your code can break if you decide to change their order. So, you can add an abstract interface to the AlbumStore that hides the details that other classes can call.

Conclusion

The first step is to build your understanding of writing a SOLID code with its principles. Then, you can build an intuition of the problems or solutions described by the principles. SOLID is the basics of programming, and you need to follow them at all costs to keep your code robust and expandable.

Offensive360 provides an all-in-one source code analysis that will allow you to see if your codebase meets the security best practices. Just upload the code to O360, and our AI-driven tool will struc

Discover more from O360

Subscribe now to keep reading and get access to the full archive.

Continue reading