26.10.15 How do you write great software? Part of the atmosphere from Geekstone meetup in September 2015 In order to answer this question, it is necessary to firstly define what excellent software is: From a user's point of view, excellent software always does what the user has defined, and never leads to unexpected results, From the point of view of object-oriented programming – excellent software is the one that is consistent with paradigms of object-oriented programming, From the perspective of design – excellent software is the one that provides reusability, so that there is no need to make everything from scratch, but it is possible to use the existing elements of an application. There are three steps in software development, which can contribute to its quality: Make sure that software does what the user requires Application of object-oriented principles Design of sustainable and reusable software Software does what the user requires Users often do not know what they want, or it is difficult for them to define what they want. Therefore, during the design phase it is very important to well define their requirements in cooperation with them, and attempt to remove all doubts and potential misunderstandings. However, even if we create software that meets all the requirements of the user from the first attempt, we must be prepared that it will most certainly be changed. In time, the user will want to introduce some new functionalities, and remove or modify the existing ones. Changes are something we must always be prepared for. Application of object-oriented principles When we talk about object-oriented programming and its paradigms, there are several of them, but the one that is misunderstood quite often is encapsulation. Encapsulation is usually defined as information concealment, however, a more precise definition would be implementation concealment, but this is not the only thing included in the meaning of this term. Encapsulation is also a fail-safe mechanism, which can also be identified with poka-yoke design, introduced by Toyota – and is translated as “isolating faults”. Thus, encapsulation not only conceals implementation details, but it also takes care of isolating faults. This is accomplished through various protection clauses, in order to prevent the user from making unintentional mistakes. Some examples of violations of encapsulation principles are provided below, together with suggestions of solutions. 1. Temporal coupling Problem If we have an Initialize method in which we are setting something without which our code would not work later, the user may be unaware of that, and might mistakenly call the Initialize method without such parameter and thus create an error in the system. Figure 1. Calling the Initialize method without a corresponding parameter Solution Constructor injection – in this way we are certain that, during initialization of the object, the user will forward the parameter without which we cannot continue further. Figure 2. Correct setting of properties via constructor injection 2. Automatic properties Automatic fields are not under control and setting invalid data might occur often (Figure 3). A much better practice is to create a set and get method with built-in checks, as shown in Figure 4. Figure 3. Automatic properties Figure 4. Set methods with additional checks Designing sustainable and reusable software Various principles may be of assistance in the creation of sustainable and reusable design. One of them is the SOLID principle. Single Responsibility Principle – each object should be in charge of one function. This is one of the basic principles, and unless it is fulfilled, it is often quite difficult to fulfill other principles. It is very important since it ensures code reusability. Below is an example showing how Invoice class undermines this principle. However, the solution is not to extract all methods into particular classes – it is necessary to have classes separated by logical units, thus the example given shows that – CalculateTax and CalculateTotal – belong to one logical unit, but Print certainly belongs to another. Perhaps later we might decide not to print only invoices, but also payment orders, bills etc. By isolating the Print method, we allow the user to use that same method for all documents and thus avoid duplication of code. Figure 5. Single Responsibility example Open Close Principle – classes should be open for extension, and closed for modification. This is very important since studies have shown that developers spend much more time on reading and understanding of code, rather than writing code. In this way we do not force class user to understand all details of implementation of our class. Also, entering changes may often lead to introduction of errors in the existing code. The example below includes the InvoiceValidator class, which validates invoices. If we should add a new requirement, we would have to add new if/else branches in the lass, which would violate this principle. Calculation logic in the given example is very smple, but if we had a more complex calculation, the class would quickly become quite hard to grasp. A much more elegant solution would be to make a list of validators which would receive an abstract class of validator type and call the Validate method by passing through the loop. In this way we would not be forced to change the initial InvoiceValidator class by adding new classes that inherit the Validator, which means that it is closed for modification and yet open for extension by adding new validators. Figure 6.Example of undermining Open-close principle Figure 7. Example of following Open-close principle Liskov Substitution Principle – there are various definitions of this principle, but they seem to be quite abstract, but the problem which leads to undermining this principle is incorrect approach to abstraction. For example, if we try to model a car which has Drive and ShiftGear methods – and would like Tesla cars to subsequently inherit that car (which seems quite fair), we will encounter a problem, as Tesla is an electric car and does not have ShiftGear, we would have to throw an exception in this method, however this is considered as extremely bad practice, because it may occur that later someone else calls it and bring the system into an incorrect state. The correct solution would be to create a new IAuto interface which would only have a Drive method, which could be inherited by both "regular" gasoline engines and Tesla as a representative of electric cars. Interface Segregation Principle – class should not implement an interface that is not fully implemented. This principle indicates that it is better to have several smaller interfaces, rather than one containing everything, without any logical sense. Figure 8. Example of Interface Segregation principle Dependency Inversion Principle – should depend on abstractions rather than specific classes. In the example below, we have InvoicePrinter and use it in PrintingSystem, however, if over time we decide that we want a printer in HTML format, for example, and create a new class InvoicePrinterHTML, we must go through PrintingSystem and check which printer is the one in question, and call the appropriate one accordingly. We would also have to perform such action everywhere where the printer is used. It is much better to put an interface (which will implement both printers) instead of specific instance within the PrintingSystem, so that it is completely irrelevant to the PrintingSystem which printer is the one in question. Figure 9. Example of Dependency Inversion principle Autor: Ivana Kovacevic Ivana Kovačević is a Software Developer and she likes to use technology to improve the quality of the people's life. She use Microsoft technologies and she loves to share her knowledge and to spread her own.