Development

Solid principles for solid developers: 4 steps on the way to strong architecture

Under modern requirements for the software development process, the developers have so much info, requirements, patterns and principles to follow, so sometimes it is hard to keep everything in mind. So senior developers formed particular patterns and behavioural habits that make the developer’s life easier and final code clearer. These principles we are going to share with you in this article. 

We briefly focus on the next principles of the development process composition:

  • SOLID
  • KISS
  • DRY
  • YAGNI

To demonstrate what is what, we will be building a real CSV files generation system below. Understanding the basics with the principles allows their proper application in live projects. 

A brief explanation of the development principles 

Let’s briefly explain the theory on the principles we defined above. Basically, they differ and sometimes may deal with different entities, though their application with real projects allows the introduction of higher control over the Development Life Cycle and code quality. 

SOLID

Each letter of the word SOLID leads to a principle component that should be followed in the development process: S – Single Responsibility Principle; O – Open Closed Principle; L – Liskov Substitution Principle; I – Interface Segregation Principle; D – Dependency Inversion Principle.

Single Responsibility Principle. The principle claims that each class must have only one purpose to exist.

Open Closed Principle. The idea is the class must be closed for some internal changes but it must be open for extensions.

Liskov Substitution Principle. Assume the situation: there is a system, which uses let’s say class A. If we create a new class B and inherit from class A, changing class A with class B must not cause a system failure.

Interface Segregation Principle. Then planning the usage of interfaces, the ones must be as thin as possible (avoid thick interfaces).

Dependency Inversion Principle. The higher level must be related to abstraction instead of some instance.

KISS (Keep it simple stupid)

The whole system must be as simple as possible. 

DRY (Don’t repeat yourself) 

There must not be duplicates of the same code logic in several places.

YAGNI (You are not gonna need it)

There is no need to write some code that will be activated in future.

The purpose of the principles’ usage is to be able to maintain and expand the code logic in the fastest and easiest way. In our example, we will be focusing on several principles only. In real life programmers from their own experience know where the principle should be working, where it must be working and where it is not needed at all. Nevertheless, for applying the principles to bigger projects, the developers should practice with an overall understanding of whether to apply the particular principle or not. 

Practical demonstration of principles application

Below we are sharing the example of principles application in practice within the code and selection of what is really needed for project serviceability. 

The technology stack.  There was a project written on PHP 7.*, Symfony 4.*, Mysql, Doctrine ORM, Twig, different bundles and API’s etc. 

The task. There were three actions in controllers, where CSV file generation was used. The code logic was like this:

{
	// Create new random temporary file
	$fh = fopen("php://temp", 'r+');

	 // In real project headers were generated using input data
	$headers = ['header 1', 'header 2'];

	// Loop through headers list, change it encoding and translate it
	$headers = array_map(function($el) use($translator) {
            	return @iconv('UTF-8', 'ISO-8859-1', $translator->trans($el));
           }, $headers);
	
	// Add headers to csv file
	fputcsv($fh, $headers, ';');
	
	// Gather all the required data (in real project there were some logic to collect data)
	$data = ['item 1', 'item 2'];

	// Loop through data list, change it encoding
	$data = array_map(function($col) {
                return @iconv('UTF-8', 'ISO-8859-1', $col);
            }, $data);

	// Loop through data lists and add it to csv file
	foreach($data as $datum)
    {
	    fputcsv($fh, $datum, ';');
    }

	// In real project footers were generated using input data
	$footers = ['footer 1', 'footer 2'];

	// Loop through footers list, change its encoding and translate it
	$footers = array_map(function($el) {
            	return @iconv('UTF-8', 'ISO-8859-1', $translator->trans($el));
           }, $footers);

	// Add footer to csv file
	fputcsv($fh, $footer, ';');
}

So basically it is a clean PHP to generate CSV files. Input data are different in each controller, but the core logic is the same. So it is pretty clear what principles are violated: DRY, KISS and Single Responsibility Principles.

DRY. The code part is repeated in three controllers.

KISS. Because we have the code to generate CSV files in three places, it could be not quite simple to figure out what CSV generation code is working. Especially when the programmer dives into the project for the first time or after a long break period. And if we will continue duplicating the same code, it is going to be a mess.

Single Responsibility Principle. Controllers are responsible for controlling the application logic and act as the coordinator between the View and the Model. Generation of CSV files is a bit different task, so it must be moved to some other class.

Except for the refactoring, we had to think, what if we will require CSV files generation in some other controller or method. How could we handle that? How could it be easily and fast to maintain and extend the CSV generation logic?

Refactoring. It is a bit clearer what to do when we know what principles were violated. So we required:

  • to remove CSV files generations from all the controllers
  • to push the CSV generation logic into one place

So basically it is possible to create some service for CSV generation that will take different input data and generate CSV using that data. 

For example:

{
	class CSVGenerator
	{
		private $data;
        private $translator;
        private $intl;
	    public function __construct(TranslatorInterface $translator, IntlExtension $intl)
        {
	        $this->translator = $translator;
            $this->intl = $intl;
        }
		public function init($data)
        {
	        $this->data = $data;
            return $this;
        }
		public function generate()
		{
		    // Here we can put generation of csv generation logic
        }
	}
}

So now we can inject the service into some action of some controller and it will be working. And we’ve successfully bypassed all three violations:

DRY – there are no repeats;

KISS, Single responsibility – we know the code logic is located in just one place.

So we partially reached the goal. But suddenly we’ve broken another principle:

Dependency Inversion Principle. If we inject the service into the controller, the latter will be based on concrete instances instead of an abstraction. So if we are required to change CSV generation exactly for one controller, it will be hard to do that.

YAGNI. It could be required to prepare data based on the entity type, so some unnecessary code will be written.

Basically, input data are different: for one action it could be one quantity of data, for the second action – the other. Also, the data itself could mean different things, there are different headers, footers etc. So we need to prepare the data per different CSV generations. Also, we should consider extending the system: so there could be other controllers which will require generating another type and quantity of data.

Solution. To solve the issues we followed the next steps:

  • changed CSVGenerator to be abstract
  • separated all the logic, which could be different to be abstract
  • created new classes to generate concrete entities, that extend the base CSVGenerator class and implemented all the abstract staff

So as a final result we had the base abstract class:

{
	abstract class CSVGenerator
	{
		protected $data;
    	protected $translator;
        protected $intl;
	    public function __construct(TranslatorInterface $translator, IntlExtension $intl)
        {
	        $this->translator = $translator;
            $this->intl = $intl;
        }

		public function init($data)
        {
	        $this->data = $data;
            return $this;
        }

		public function generate()
		{
            // Create new random temporary file
		    $handle = fopen("php://temp", 'r+');

            // Gather headers
            $headers = $this->getHeaders();
        	fputcsv($handle, $headers, ';');

            // Add data
            $this->pushDataToCSV($handle);

            // Prepare response
            $response = new Response(stream_get_contents($handle, -1, 0), Response::HTTP_OK, array(
                'Content-Type'  => 'text/csv'
            ));

           // Close the opened file
           fclose($handle);

           // Return response
	       return $response;
        }

        abstract protected function getHeaders();
        abstract protected function pushDataToCSV($csvHandle);
	}
}

And there are inherited classes like:

{
	class UserCSVGenerator extends CSVGenerator
    {
        protected function getHeaders()
        {
	        // Prepare headers
            $headers = ['Header 1', 'Header 2'];
    	    return $headers;
        }

        protected function pushDataToCSV($csvHandle)
        {
	        // … prepare data as array list
	        $data = [['data 1', 'data 2']];
	
	        foreach($data as $datum)
	        {
		        // .. Could be some additional data preparation
		        fputcsv($csvHandle, $datum, ';');	
	        }
        }
    }
}

Then we can write the action of the controller like:

{
	class UserController extends AbstractController
	{
		private $usersCSVGenerator;
        public function __construct(CSVGenerator $usersCSVGenerator)
        {
            $this->usersCSVGenerator = $usersCSVGenerator;
        }

        public function getUsersCsvFile() 
        {
	        // .. gathering users' data from repository, API etc
	        $usersData = [];

	        // Generate csv
	        return $this->usersCSVGenerator->init($usersData)->generate();
        }
    }
}

And in config we should set up substitution:

services:
    App\Services\CSV\CSVGenerator:
        abstract:  true
        arguments: ~
        autoconfigure: false

    App\Services\CSV\UserCSVGenerator:
        parent: App\Services\CSV\CSVGenerator
        public: true
        autowire: true
        autoconfigure: false
 
    App\Services\CSV\CSVGenerator $userCSVGenerator: '@App\Services\CSV\UserCSVGenerator'

The last configuration is completely related to Symfony, but for sure there is a Dependency Injection Mechanism in all modern frameworks.

Conclusion. Now we can check if the code satisfies some principles and does not violate the others.

First of all, we fixed violations: DRY, KISS and Single Responsibility Principles. Also, we followed:

Open Closed Principle. The core logic of CSV files generation is located in the base abstract class. And we can prepare the data and add them to CSV by extending the base class.

Liskov Substitution Principle. We can inject the base abstract class and substitute it on the fly via Dependency Injection. The system will still be stable.

Dependency Inversion Principle. Now controllers are based on abstract staff instead of concrete instances.

YAGNI. We don’t write code that is not required: we extend from the base class and write only what is required for current CSV file generation.

So practising the application of principles allows for making code clearer for the next developers or introducing changes and scaling. The most common principles are SOLID, KISS, DRY, and YAGNI; though it does not mean that they should be applied each time simultaneously. The developers with getting more experience find the proper ways how to make code optimized and principled with strong architecture without overwhelming it. Though it is impossible to underestimate the benefits they bring to the project and their ability to mage it with proper application. 

Wish to learn more about the documentation or technical tips that can empower your development flow? Read the articles from the development and e-commerce sections.

Write A Comment