ad_google

sábado, 26 de dezembro de 2020

SOLID principles in software development




Since i started programming i always found it something difficult to do. It didn't seem natural and i lacked good practices to write the code. Most of the times I would be lost on where to start.To be honest, doing code, is an hard task and i don't believe who says otherwise. It requires a mindset. Not everyone has the reasoning to do these kind of tasks. It takes years of knowledge and experience to be good at it.

As a specie, humans give names to everything, because thats how we communicate to others about some object, concept or whatever it is.We like to categorize, to organize things, in programming we do the same with the principles we use for good code practices.What happens with these patterns is that when we identify what kind of task we have to do we will identify the pattern and apply it. 

Next i will talk about the SOLID principles, this is an acronym that we should be remembering, it comes from the early 2000.

It means the following: Single-responsability principle; Open/Closed principle; Liskov substitution principles; Inversion segregation principles; Dependency-inversion principle.

Let's now see what this fancy words mean individually.

Single-responsability principle

A class should have only one reason to change.

Why? Because having a class that does many different things, will make a mess and it will have to change frequently.

An example using a User class

class User

{

    void CreatePost(Database db, string postMessage)

    {

        try

        {

            db.Add(postMessage);

        }

        catch (Exception ex)

        {

            db.LogError("An error occured: ", ex.ToString());

            File.WriteAllText("\LocalErrors.txt", ex.ToString());

        }

    }

}


The method CreatePost() in the class User does many different things.

For example it logs the error on a file and a database if an exception occurs.

To comply with single responsability we add another class that will be responsible for logging the errors.


class Post

{

    private ErrorLogger errorLogger = new ErrorLogger();


    void CreatePost(Database db, string postMessage)

    {

        try

        {

            db.Add(postMessage);

        }

        catch (Exception ex)

        {

            errorLogger.log(ex.ToString())

        }

    }

}


class ErrorLogger

{

    void log(string error)

    {

      db.LogError("An error occured: ", error);

      File.WriteAllText("\LocalErrors.txt", error);

    }

}


Open/Closed principle

Software entities (classes, modules, funcions, etc) should be open for extension but closed for modifications.

For the code to comply with this principle, we may use inheritance and interfaces, they will enable to polymorphically substitute classes.

For example, we may want to add to different database tables a message, if we do all on the same class the following can happen:

class Post

{

    void CreatePost(Database db, string postMessage)

    {

        if (postMessage.StartsWith("#"))

        {

            db.AddAsTag(postMessage);

        }

        else

        {

            db.Add(postMessage);

        }

    }

}

What we have above is horrible, at least creating new methods would be a bit better.

Applying the principle, the class Post will be closed for modifications but open for extensions, we will have the much better code:

class Post

{

    void CreatePost(Database db, string postMessage)

    {

        db.Add(postMessage);

    }

}


class TagPost : Post

{

    override void CreatePost(Database db, string postMessage)

    {

        db.AddAsTag(postMessage);

    }

}



Liskov substitution principle

It means that subtypes must be substitutable for their base types.

By other words, instances of a class should be able easily replaced by child classes without altering the correctness of the program.

class Post

{

    void CreatePost(Database db, string postMessage)

    {

        db.Add(postMessage);

    }

}


class TagPost : Post

{

    override void CreatePost(Database db, string postMessage)

    {

        db.AddAsTag(postMessage);

    }

}


class MentionPost : Post

{

    void CreateMentionPost(Database db, string postMessage)

    {

        string user = postMessage.parseUser();


        db.NotifyUser(user);

        db.OverrideExistingMention(user, postMessage);

        base.CreatePost(db, postMessage);

    }

}


class PostHandler

{

    private database = new Database();


    void HandleNewPosts() {

        List<string> newPosts = database.getUnhandledPostsMessages();


        foreach (string postMessage in newPosts)

        {

            Post post;

            if (postMessage.StartsWith("#"))

            {

                post = new TagPost();

            }

            else if (postMessage.StartsWith("@"))

            {

                post = new MentionPost();

            }

            else {

                post = new Post();

            }

            post.CreatePost(database, postMessage);

        }

    }

}

We can observe above how the call of CreatePost() in the case of a subtype MentionPost won't do what it is supposed to do; notify the user and override existing mention. 

Since the CreatePost() method is not overriden in MentionPost the CreatePost() call will simply be delegated upwards in the class hierarchy and call CreatePost() from it's parent class.

Let's correct that mistake.

class MentionPost : Post

{

    override void CreatePost(Database db, string postMessage)

    {

        string user = postMessage.parseUser();


        NotifyUser(user);

        OverrideExistingMention(user, postMessage)

        base.CreatePost(db, postMessage);

    }


    private void NotifyUser(string user)

    {

        db.NotifyUser(user);

    }


    private void OverrideExistingMention(string user, string postMessage)

    {

        db.OverrideExistingMention(_user, postMessage);

    }

}


Interface segregation principle

Clients should not depend upon methods it does not use.

It states, that we should not add new functionality to an interface in use and instead create a new one if needed.

An example of a violation of the principle:

interface IPost

{

    void CreatePost();

}


interface IPostNew

{

    void CreatePost();

    void ReadPost();

}


In this example, we first suppose that we have only a method CreatePost() on the interface IPost, but later we had a new method ReadPost() so that the interface becomes IPostNew.

The right way to do, is to create a new interface.

interface IPostCreate

{

    void CreatePost();

}


interface IPostRead

{

    void ReadPost();

}



Dependency-inversion principle

High level modules should not depend on low level modules. Both should depend on abstrations.

Abstration should not depend upon details. Details should depend upon abstrations.

This principle in software, means that we should decouple modules. To comply with this principle, a design pattern called dependency inversion pattern exists, that most often is solved by dependency injection.

Dependency injection is an huge topic, it is used by injecting the dependencies of a class through its constructor as input parameters.

Let's take a look at an example:

class Post

{

    private ErrorLogger errorLogger = new ErrorLogger();


    void CreatePost(Database db, string postMessage)

    {

        try

        {

            db.Add(postMessage);

        }

        catch (Exception ex)

        {

            errorLogger.log(ex.ToString())

        }

    }

}



Observe that we create the ErrorLogger instance from within the Post class. This is a violation of the dependency inversion principle. If we wanted to use a different kind of logger, we would have to modify the Post class. 

Let's fix this by using dependency injection.

class Post

{

    private Logger _logger;


    public Post(Logger injectedLogger)

    {

        _logger = injectedLogger;

    }


    void CreatePost(Database db, string postMessage)

    {

        try

        {

            db.Add(postMessage);

        }

        catch (Exception ex)

        {

            logger.log(ex.ToString())

        }

    }

}


By using dependency injection we no longer rely on the Post class to efine the specific type of logger.

Conclusion

The principles that exist actually may not be the answer to everything, because new and improved ways of doing things are being discovered constantly. Personally, i intend to keep on par with the actual knowledge. 

If we follow good practices our code will be more bullet proof. More extensible, scalable, modifiable and so on. 

The requirements may constantly change so we should code in a way that allows us and everyone that use that code to rapidly make the needed changes. 

In another article i will talk about the patterns that derive from the principles i talked here.