SOLID: Single Responsibility Principle(SRP)
A software entity should have only one responsibility or have only one reason to change.
Have you ever get confused or scratching your head 🤔, to find a proper name for a class, or method, etc.
If yes, It is the clear indication of the entity doing more than it is intended to do or have multiple responsibilities. aka Violating SRP guideline.
For Example, There is a ProductListScreen class have a function which should render product catalogue.
class ProductListScreen {
func makeProductList() {
// 1. Data Loading
let productsArray = APIService.getProducts()
// 2. Prepare UI element
let productListView = ProductListView()
productListView.data = productsArray
// 3. Show UI element to user
productsArray.makeVisibleToUser()
}
}
Can you spot that ?
There should be only one reasons to change a software entity..…Only ☝🏻
But if you notice the function makeProductList here, is having 3 responsibilities.
Or we can also say that there would be at least three reason which may lead a change in makeProductList.
- If data loading changes
- If products to be shown as Carousel like component rather than list.
- Not showing the list to the user at all
This means this code will lead to rigidity or fragility later on if any of this change will happen.
So, how to solve this ?
Solution 💊
In SRP phase we determine the what an entity should be doing, defining a clear boundary of encapsulation. One way to get this done is using Decomposition.
class ProductListScreen {
// 3. Show UI element to user
func presentProductList() {
let productsArray = loadAPIProducts()
let productListView = makeProductUI(with: productsArray)
productListView.makeVisibleToUser()
}// 1. Data Loading from API
func loadAPIProducts() -> [String] {
// Do some API call and return an array of products
return APIService.getProducts()
}// 2. Prepare UI element
func makeProductUI(with data: [String]) -> ProductListView {
let productListView = ProductListView()
productListView.data = productsArray
return productListView
}}
This way we can have single responsibility or a single reason to change for each functions.
Again, this are not the strict rules one should follow, these are the guidelines one should follow for better coding.
Sometimes there could be entities having more than one reason to change if the reasons are not going to change forever.
Let’s see one real time example,
(I would be using Swift language in iOS context but you can try with any OOP language.)
class ProductListScreen: UIViewController {
@IBOutlet weak var tableView: UITableView! //1
override func viewDidLoad() {
super.viewDidLoad()
// Table Layout
tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
// 2. Table DataSetUp
let productsTableViewAdapter = ProductsTableViewAdapter(data: ["Chocolate", "Coffee", "Tea", "Noodles"]) {
(selectedIndexPath) in
print("Clicked At: \(selectedIndexPath)")
}
tableView.dataSource = productsTableViewAdapter
tableView.delegate = productsTableViewAdapter
// 3. Some view configuration after it is loaded (view did load)
view.backgroundColor = UIColor.red
}}
private class ProductsTableViewAdapter: NSObject, UITableViewDataSource, UITableViewDelegate {
let data: [String]
let onClickHandler: (IndexPath) -> Void
init(data: [String], onClickHandler: @escaping (IndexPath) -> Void) {
self.data = data
self.onClickHandler = onClickHandler
}func tableView(_ tableView: UITableView, numberOfRowsInSection section: Int) -> Int {
return data.count
}func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
return UITableViewCell() // Dummy
}func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
onClickHandler(indexPath)
}}
You may have seen this kind of code a lot of times, viewDidLoad is a bit messy with 3 reasons to change namely.
viewDidLoad have to be changed If we,
- Change the UITableView to UICollectionView (Carousel like comp as stated above)
- Use other kind of Adapter or have Model or CoreData instead of plain String data.
- Need the screen to be black color
Ah three reasons, lets decompose this
class ProductListScreen: UIViewController {
@IBOutlet weak var tableView: UITableView!
override func viewDidLoad() {
super.viewDidLoad()
setUpTableViewLayout()
setUpTableViewData()
}private func setUpTableViewData() {
let productsTableViewAdapter = makeTableViewAdapter(with: ["Chocolate", "Coffee", "Tea", "Noodles"])
tableView.dataSource = productsTableViewAdapter
tableView.delegate = productsTableViewAdapter
}private func setUpTableViewLayout() {
tableView.topAnchor.constraint(equalTo: view.topAnchor).isActive = true
tableView.bottomAnchor.constraint(equalTo: view.bottomAnchor).isActive = true
tableView.leftAnchor.constraint(equalTo: view.leftAnchor).isActive = true
tableView.rightAnchor.constraint(equalTo: view.rightAnchor).isActive = true
}private func makeTableViewAdapter(with data: [String]) -> UITableViewDataSource & UITableViewDelegate {
return ProductsTableViewAdapter(data: data) {
(selectedIndexPath) in
print("Clicked At: \(selectedIndexPath)")
}
}}
This looks better, No?
Here,If you have not noticed one thing here, let me explain, ProductsTableViewAdapter for managing UITableView rendering related code is following the SRP.
Hope you understand the concept here. Please feel free to comment your questions below. Give it a try to find some violations in your code and employ SRP.
Let’s move to our next topic. Open Close Principle