Single Responsibility Principle
A module should have one, and only one, reason to change. A module should be responsible to one, and only one, user or stakeholder. A module should be responsible to one, and only one, actor(a group of people) like members vs admin members. Members are consumers or customers and admins are admin users. A class should have one and only one responsibility. In our application level code, we define entity classes to represent real time entities such as User, Employee, Account etc. Most of these classes are examples of SRP principle because when we need to change the state of a person, only then we will modify the User class, and so on. When I am in doubt of whether a function abides by the SRP. I try to explain what that function does? If I have to use the conjunctions “and” or “or” then it is likely that the function doesn’t abide by the SRP.
SRP is applied to the database(DAO), service(business logic), and the controller layer(endpoint) even to the UI. An entity class should focus on just one database table, UserService should only focus on business methods that work with data in User class. Saving and Reading from the database should be delicated to the UserDAO interface. UserController focuses on endpoints that work with UserService business logic. AdminUserController has logic that is different from UserController’s logic.
@Data @AllArgsConstructor @NoArgsConstructor @JsonInclude(value = Include.NON_NULL) public class User implements Serializable { private static final long serialVersionUID = 1L; private Long id; private String firstName; private String lastName; private LocalDate dateOfBirth; private String email; private String password; }
public interface UserService { User updateEmail(String newEmail); }
@Service @Slf4j public class UserServiceImp implements UserService { /* * This UserServiceImp focuses on logic that are related to User */ @Override public User updateEmail(String newEmail) { /** * 1. look up if newEmail already exists in db<br> * 2. change email to newEmail in db */ return null; } }
public interface UserAuthService { User signUp(User user); User login(String email, String password); } @Service @Slf4j public class UserAuthServiceImp implements UserAuthService { /* * User authentication only */ @Override public User signUp(User user) { log.info("signUp(..)"); return null; } @Override public User login(String email, String password) { log.info("login(..)"); return null; } }
Here we have a User entity class and two service classes that demo SRP. The UserAuthService interface focuses on authenticating the user where as the UserService interface focuses on User business logic. There is only one reason why we should make a change to these services.
Note that the service class should not contain utility global functions related to the module. It is better to separate global utility methods in another globally accessible class like UserUtils. This will help in maintaining the class for that particular purpose, and we can decide the visibility of that class to a specific module only. This means that UserUtils may or may not be accessible outside of its package.
Benefits of SRP
The class is easier to read, understand, maintain, and is more reusable.
It is easier to test functions that are performing one basic operation.
It makes version control easier. For example, say we have a DAO class that handles database operations, and we see a change in that file in the git commits. By following the SRP, we will know that it is related to storage or database-related stuff.
Merge conflicts will appear less as files will have a single reason to change and conflicts that do exist will be easier to resolve.
Note that it is inevitable that new business requirements will come up or updates to your code base will happen. Therefore, code that is decoupled will make your life easier when implementing the change. The last thing you want to do is to make one change and create a cascading effect to the rest of the system.
Open-Closed Principle
A software artifact, entity, or class should be open for extension but closed for modification.
Open for extension means that the behavior of the module can be extended. As the requirements of the application change, we are able to extend the module with new behaviors that satisfy those changes. In other words, we are able to change what the module does. It’s called software for a reason that it should be easy to change.
Closed for modification means the behavior of a module does not result in changes to the source or binary code of the module. The binary executable version of the module, whether in a linkable library, a DLL, or a Java .jar, remains untouched.
Behavior and functionality can be easily changed by just extending what is already present – instead of modifying the present code.
If a behavior coded in a class needs to be changed, a way of doing that is to create a subclass and override methods as necessary. No change to the superclass is necessary, just new code in the subclass.
We still need some kind of toggle mechanism to switch between the original and extended behaviour, which could require modification of the present code, and the design needs to support the particular extension that we want to make – we cannot design our code in a way that ANY modification is possible without touching it.
The one exception to the rule is when fixing bugs in existing code. So, we should modify our class only at the time of bug fixing.
Design and code should be done in a way that new functionality should be added with minimum or no changes in the existing code. When needs to extend functionality – avoid tight coupling, don’t use if-else/switch-case logic, do code refactoring as required.
Benefits of OCP
Code is easier to maintain. Interfaces in code offers an additional level of abstraction which in turn enables loose coupling. The implementations of an interface are independent of each other and don’t need to share any code. Hence, you can easily maintain your code with client’s keep changing requirements.
Flexibility to extend functionalty. Further, if any change request arises in future, your code will be more flexible to extend.
public interface UserService { User updateEmail(String newEmail); }
public class UserServiceImp implements UserService { /* * This UserServiceImp focuses on logic that are related to User */ @Override public User updateEmail(String newEmail) { /** * 1. look up if newEmail already exists in db<br> * 2. change email to newEmail in db<br> * 3. send email to new email for verification */ return null; } }
public class SpouseService implements UserService { @Override public User updateEmail(String newEmail) { /** * 1. look up if newEmail already exists in db<br> * 2. change email to newEmail in db<br> * 3. send email to new email for verification<br> * 4. send email to Primary user that spouse email was change */ return null; } }
Here we are extending the fuctionality of UserService as for SpouseService. Notice that there an extra logic when a spouse changes their email.
The LISKOV Substitution Principle
if we substitute a superclass object reference with an object of any of its subclasses, the program should not break.
The LSP is applicable when there’s a supertype-subtype inheritance relationship by either extending a class or implementing an interface. We can think of the methods defined in the supertype as defining a contract. Every subtype is expected to stick to this contract. If a subclass does not adhere to the superclass’s contract, it’s violating the LSP.
How can a method in a subclass break a superclass method’s contract? There are several possible ways:
LSP violations are a design smell. We may have generalized a concept prematurely and created a superclass where none is needed. Future requirements for the concept might not fit the class hierarchy we have created.
If client code cannot substitute a superclass reference with a subclass object freely, it would be forced to do instanceof
checks and specially handle some subclasses. If this kind of conditional code is spread across the codebase, it will be difficult to maintain.
Every time we add or modify a subclass, we would have to comb through the codebase and change multiple places. This is difficult and error-prone.
It also defeats the purpose of introducing the supertype abstraction in the first place which is to make it easy to enhance the program.
The LSP is a very useful idea to keep in mind both when developing a new application and when enhancing or modifying an existing one.
When designing the class hierarchy for a new application, the LSP helps make sure that we are not prematurely generalizing concepts in our problem domain.
When enhancing an existing application by adding or changing a subclass, being mindful of the LSP helps ensure that our changes are in line with the superclass’s contract and that the client code’s expectations continue to be met.
Benefits or LSP
) Code Reusability
2) Easier Maintenance
3) Reduced Coupling
public interface UserNotificationService { boolean sendEmail(User user); } @Service @Slf4j public class UserNotificationServiceImp implements UserNotificationService { @Override public boolean sendEmail(User user) { /** * loop through and send email */ return false; } }
public boolean notifyAllUsersOfDayOff() { List<User> users = new ArrayList<>(); users.add(new User()); users.add(new AdminUser()); /** * call the UserNotificationService.sendEmail(...) to send the notification */ for (User user : users) { // User and AdminUser userNotificationService.sendEmail(user); } return false; }
Interface Segregation Principle
Interface segregation simply means that we should break larger interfaces into smaller ones. Thus ensuring that implementing classes need not implement unwanted methods.
Clients should not be forced to depend upon interfaces that they do not use. The goal of this principle is to reduce the side effects of using larger interfaces by breaking application interfaces into smaller ones. It’s similar to the Single Responsibility Principle , where each class or interface serves a single purpose.
Precise application design and correct abstraction is the key behind the Interface Segregation Principle. Though it’ll take more time and effort in the design phase of an application and might increase the code complexity, in the end, we get a flexible code.
public interface UserService { User updateEmail(String newEmail); }
public interface AdminUserService { boolean addClient(AdminUser adminUser, User user); boolean notifyAllUsersOfDayOff(); }
Even though we have combined these two interfaces into one, it is better long term to separate them into smaller interfaces.
Benefits of ISP
Dependency Inversion Principle
Conventional application architecture follows a top-down design approach where a high-level problem is broken into smaller parts. In other words, the high-level design is described in terms of these smaller parts. As a result, high-level modules that gets written directly depends on the smaller (low-level) modules.
What Dependency Inversion Principle says is that instead of a high-level module depending on a low-level module, both should depend on an abstraction.
@Data @AllArgsConstructor @NoArgsConstructor @JsonInclude(value = Include.NON_NULL) public class Client implements Serializable { private static final long serialVersionUID = 1L; private Long id; private String firstName; private String lastName; private LocalDate dateOfBirth; private String email; private String password; /** * private User user;<br> * 1. schedule appointment on calendar of user.<br> * 2. send email to user as a reminder<br> * 3. charge client appointment fee<br> * * Note: instead of doing this logic here(bad idea). we moved it to the ClientService * interface */ }
@Service public class ClientServiceImp implements ClientService { @Override public boolean bookAppointment(Client client, User user) { /** * 1. schedule appointment on calendar of user.<br> * 2. send email to user as a reminder<br> * 3. charge client appointment fee<br> */ return false; } }
Here our entity classes or lower level classes don’t dependent on each other, they dependent on abstraction(ClientService interface)
Dependency Inversion is more focused on the structure of your code, its focus is keeping your code loosely coupled. On the other hand, Dependency Injection is how the code functionally works. When programming with the Spring Framework, Spring is using Dependency Injection to assemble your application. Dependency Inversion is what decouples your code so Spring can use Dependency Injection at run time.
Benefits of DIP
Software documentation is a crucial part of working software. Whether it’s API documentation, release notes, or customer-facing help content, you can’t afford to skip over the docs when it comes to developing or shipping out software. Software without documentation is difficult to work with and costs more to maintain.The main goal of effective documentation is to ensure that developers and stakeholders are headed in the same direction to accomplish the objectives of the project.
The documentation types that the team produces and its scope depending on the software development approach that was chosen. Agile is the main development approach most organizations are using.
Here are the different types of documentation.
Product documentation describes the product that is being developed and provides instructions on how to perform various tasks with it. In general, product documentation includes requirements, tech specifications, business logic, and manuals.
Process documentation represents all documents produced during development and maintenance that describe… well, the process. The common examples of process-related documents are standards, project documentation, such as project plans, test schedules, reports, meeting notes, or even business correspondence.
The main difference between process and product documentation is that the first one records the process of development and the second one describes the product that is being developed.
System documentation provides an overview of the system and helps engineers and stakeholders understand the underlying technology. It usually consists of the requirements document, architecture design, source code, validation docs, verification and testing info, and a maintenance or help guide. It’s worth emphasizing that this list isn’t exhaustive.
A product requirement document or PRD provides information about system functionality. Generally, requirements are the statements of what a system should do. It contains business rules, user stories, use cases, etc. This document should be clear and shouldn’t be an extensive and solid wall of text. It should contain enough to outline the product’s purpose, its features, functionalities, maintenance, and behavior.
The best practice is to write a requirement document using a single, consistent template that all team members adhere to. The one web-page form will help you keep the document concise and save the time spent on accessing the information. An example of this is a confluence page.
User experience design begins at the requirements stage and proceeds through all the stages of development, including the testing and post-release stages. The process of UX design includes research, prototyping, usability testing, and the actual designing part, during which lots of documentation and deliverables are produced.
The UX documentation can be divided into stages. The research stage includes:
User Personas are created and documented during the research stage. The information gathered during user interviews and surveys is compiled into functional user persona documents. User personas represent the key characteristics of real users, focusing on behavior, thought patterns, and motivation.
A user scenario is a document that describes the steps a user persona will take to accomplish a specific task. User scenarios focus on what a user will do, rather than outlining the thought process. The set of scenarios can be either visual or narrative, and describe the existing scenarios or future functionality.
Scenario maps are used to compile the existing user scenarios into a single document. Scenario maps show all possible scenarios available at a given moment. The main purpose of a scenario map is to depict all the possible scenarios for every single function, as well as intersecting scenario steps.
A user story map is formed from the backlog of the product. This type of document helps to arrange the user stories into future functions or parts of the application. A user story map can be a scheme, or a table of user stories grouped in a particular order to denote the required functions for a certain sprint.
The UX style guide is a document that includes the design patterns for the future product. It also describes all possible UI elements and content types used, defining the rules of how they should be arranged and work with each other
On the stage of prototyping and designing, a UX designer often works with the deliverables and updates documentation on par with other team members, including product owner, UI designers, and development team. The most common documents produced at these stages are:
User flow or user journey schemes help the team to map the steps a user should take through the whole product. The main task of a user flow scheme is to depict the possible steps a user may take, going from page to page. Usually, the scheme includes all the pages, sections, buttons, and functions they provide to show the logic of user movement.
Wireframes are the blueprints for future UI. Basically, wireframes are the schemes that show how to arrange the elements on the page and how they should behave. But, wireframes don’t depict what those elements should look like.
A mock-up is the next product design stage, showing the actual look and feel of a product. Basically, mock-ups are static images representing the final product design.
A prototype is a mock-up that you can interact with: click some buttons, navigate between different pages, and so on. A prototype can be created in a prototyping tool like Sketch or MockFlow . Using templates, UX designers can create interactive mock-ups on the early stages of development to be employed for usability testing.
A usability testing report is a short-form feedback document created to communicate the results of usability testing. The report should be as short as possible, with visual examples prevailing over text.
Software architecture design documents, sometimes also called technical specifications, include the main architectural decisions made by the solution architect . Unlike the product requirement document mentioned above that describes what needs to be built, the architecture design documentation is about how to build it. It has to describe in what way each product component will contribute to and meet the requirements, including solutions, strategies, and methods to achieve that. So, the software design document gives an overview of the product architecture, determines the full scope of work, and sets the milestones, thus, looping in all the team members involved and providing the overall guidance.
A source code document is a technical section that explains how the code works. While it’s not necessary, the aspects that have the greatest potential to confuse should be covered. The main users of the source code documents are software engineers.
Source code documents may include but are not limited to the following details:
Try to keep the document simple by making short sections for each element and supporting them with brief descriptions.
As the name suggests, user documentation is created for product users. However, their categories may also differ. So, you should structure user documentation according to the different user tasks and different levels of their experience
The documentation created for end-users should explain in the simplest way possible how the software can help solve their problems. Such user instructions can be provided in the printed form, online, or offline on a device. Here are the main types of the user documents:
The quick start guide provides an overview of the product’s functionality and gives basic guidelines on how to use it.
The complete manual includes exhaustive information and instructions on how to install and operate the product. It lists the hardware and software requirements, detailed description of the features and full guidelines on how to get the most out of them, example inputs and outputs, possible tips and tricks, etc.;
The troubleshooting guide gives end-users information on how to find and resolve possible issues that might arise when using the product.
System administrators’ documents don’t need to provide information about how to operate the software. Usually, administration docs cover installation and updates that help a system administrator with product maintenance. Here are standard system administrators documents:
Process documentation covers all activities surrounding product development. The value of keeping process documentation is to make development more organized and well-planned. This branch of documentation requires some planning and paperwork both before the project starts and during the development. Here are common types of process documentation:
Plans, estimates, and schedules. These documents are usually created before the project starts and can be altered as the product evolves.
Reports and metrics. Reports reflect how time and human resources were used during development. They can be generated on a daily, weekly, or monthly basis. Consult our article on agile delivery metrics to learn more about process documents such as velocity chats, sprint burndown charts, and release burndown charts.
Working papers. These documents exist to record engineers’ ideas and thoughts during project implementation. Working papers usually contain some information about an engineer’s code, sketches, and ideas on how to solve technical issues. While they shouldn’t be the major source of information, keeping track of them allows for retrieving highly specific project details if needed.
Standards. The section on standards should include all coding and UX standards that the team adheres to along the project’s progression.
The majority of process documents are specific to the particular moment or phase of the process. As a result, these documents quickly become outdated and obsolete. But they still should be kept as part of development because they may become useful in implementing similar tasks or maintenance in the future. Also, process documentation helps to make the whole development more transparent and easier to manage.
The main goal of process documentation is to reduce the amount of system documentation. In order to achieve this, write the minimal documentation plan. List the key contacts, release dates, and your expectations with assumptions.
public interface Employee { /** * @return the name of the employee */ String getName(); /** * @param e add this employee to the list of employees */ void add(Employee e); /** * @param e remove this employee from the list of employees */ void remove(Employee e); /** * @return the list of employees */ List<Employee> getEmployees(); /** * This method estimates the costs in ManDays for the given project. Managers * delegate this request to their employees, developers return an estimate. * * @param projectDescription * @return */ int estimateProject(String projectDescription); }
public class VP extends Manager { public VP(String name) { super(name); } @Override public String toString() { return "I am " + getName() + ", VP"; } /** * VP doubles the estimated amount. */ @Override public int estimateProject(String projectDescription) { System.out.println("I am " + getName() + ", the VP, and calling for an estimate..."); final int projectEstimate = super.estimateProject(projectDescription); System.out.println("Original estimate: " + projectEstimate); return Math.toIntExact(Math.round(projectEstimate * 2)); } } public class TeamLeader extends Manager { public TeamLeader(String name) { super(name); } @Override public String toString() { return "I am " + getName() + ", Team Leader"; } } public abstract class Manager implements Employee { List<Employee> employees = new ArrayList<>(); String name; public Manager(String name) { this.name = name; } @Override public List<Employee> getEmployees() { return this.employees; } @Override public void add(Employee e) { if (e != null) { this.employees.add(e); } } @Override public void remove(Employee e) { if (e != null) { this.employees.remove(e); } } @Override public int estimateProject(String projectDescription) { if (this.employees.isEmpty()) { return 0; } return Math.round(this.employees.stream().mapToInt(e -> { System.out.println(e); return e.estimateProject(projectDescription); }).sum() / this.employees.size()); } @Override public String getName() { return this.name; } } public class Developer implements Employee { String name; public Developer(String name) { this.name = name; } @Override public String getName() { return this.name; } @Override public void add(Employee e) { } @Override public void remove(Employee e) { } @Override public List<Employee> getEmployees() { return null; } @Override public int estimateProject(String projectDescription) { return new Random().nextInt(24); } @Override public String toString() { return "I am " + getName() + ", Developer"; } } public static void main(String... args) { final Developer d1 = new Developer("Jack"); final Developer d2 = new Developer("Jill"); final Developer d3 = new Developer("Brian"); final Developer d4 = new Developer("Bob"); final Manager m1 = new TeamLeader("Marc"); final Manager m2 = new TeamLeader("Christian"); final Manager m3 = new TeamLeader("Phil"); m1.add(d3); m1.add(d2); m2.add(d1); m3.add(d4); final VP vp = new VP("Joseph"); vp.add(m1); vp.add(m2); System.out.println("Our estimate is: " + vp.estimateProject("New exotic feature")); }November 26, 2019
The Hash table data structure stores elements in key-value pairs where
The key is sent to a hash function that performs arithmetic operations on it. The result (commonly called the hash value or hash) is the index of the key-value pair in the hash table.
1. Hash function
As we’ve already seen, the hash function determines the index of our key-value pair. Choosing an efficient hash function is a crucial part of creating a good hash table. You should always ensure that it’s a one-way function, i.e., the key cannot be retrieved from the hash. Another property of a good hash function is that it avoids producing the same hash for different keys.
2. Array
The array holds all the key-value entries in the table. The size of the array should be set according to the amount of data expected.
A collision occurs when two keys get mapped to the same index. There are several ways of handling collisions.
1. Linear probing
If a pair is hashed to a slot which is already occupied, it searches linearly for the next free slot in the table.
2. Chaining
The hash table will be an array of linked lists. All keys mapping to the same index will be stored as linked list nodes at that index.
3. Resizing the hash table
The size of the hash table can be increased in order to spread the hash entries further apart. A threshold value signifies the percentage of the hash table that needs to be occupied before resizing. A hash table with a threshold of 0.6 would resize when 60% of the space is occupied. As a convention, the size of the hashtable is doubled. This can be memory intensive.
Operation | Average | Worst |
---|---|---|
Search | O(1) | O(n) |
Insertion | O(1) | O(n) |
Deletion | O(1) | O(n) |
Space | O(n) | O(n) |
public class MyHashMap<K, V> { private int size = 10; private MapNode<K, V>[] array = new MapNode[size]; public void put(K key, V value) { int arrayIndex = hash(key); putVal(key, value, arrayIndex); } private int hash(K key) { int a = 20; int b = 30; int primeNumber = 101; int m = size; int h = key.hashCode(); return ((a * h * b) % primeNumber) % m; } public V get(K key) { int arrayIndex = hash(key); MapNode<K, V> head = array[arrayIndex]; if (head == null) { return null; } MapNode<K, V> currentNode = head; while (null != currentNode && !currentNode.getKey().equals(key)) { currentNode = currentNode.getNext(); } if (currentNode == null) { return null; } else { return currentNode.getValue(); } } public void putVal(K key, V val, int arrayIndex) { MapNode<K, V> head = array[arrayIndex]; if (head == null) { array[arrayIndex] = head = new MapNode<>(key, val); } else { MapNode<K, V> currentNode = head; MapNode<K, V> prevNode = head; while (null != currentNode && !currentNode.getKey().equals(key)) { prevNode = currentNode; currentNode = currentNode.getNext(); } if (currentNode == null) { currentNode = new MapNode<>(key, val); prevNode.setNext(currentNode); } else { currentNode.setValue(val); } } } }
@AllArgsConstructor @ToString @Data public class MapNode<K, V> { private K key; private V value; private MapNode<K, V> next; public MapNode(K key, V value) { this(key, value, null); } }