S.O.L.I.D Design Principles

Mushtaque Ahmed
The Startup
Published in
10 min readNov 4, 2019

Design Principles for Mobile Platform Architecture (iOS, Swift)

If your apps are working that doesn’t mean you have good design. Design and architecture of a mobile app are very much ignored in this era of quick development and immediate release.

Few of the Signs of Bad Design are
1. Rigidity : When a design is rigid, any change in one part of code will have a cascading effect and other parts needs to be changed.
2. Fragility : Fixing new parts/ bugs will produce to more bugs
3. Immobility : Hard to reuse components because of tight coupling.

In order to overcome this issues in your software, the known and tested way to architect your apps well in advance. And the one principle that can help to put the foundation of a good architecture is SOLID.

SOLID :
1. Single Responsibility
2. Open / Closed Principle
3. Liskov substitution
4. Interface Segregation
5. Dependency Inversion

  1. Single Responsibility : It states that each type should have one well defined responsibility. Instead of developing big heterogeneous components we can divide it to smaller homogenous components focused to do single task.
  2. Open / Closed Principle : The software entities must be open for extension but closed for modification. So in brief the core classes or APIs should be extended if a new features is to be introduced, but the core functionality which is already available, should not be changed. This principle can be achieved by OOPS concept such as Inheritance, Compositions and Polymorphism.
  3. Liskov substitution: Software should replace references to base class with objects of derived classes. To explain in details, always try to create and use the object of a derived class rather then base class. Base class interface should be used to create new derived classes, not to implement functionality. That can be done on the newly created derived classes.
  4. Interface segregation : It deals with the problem of ‘fat’ interfaces. A fat or polluted interfaces means, it exposes too many types and properties. Because of this the client have to import or depends on types and properties that they don’t even use or require.
  5. Dependency Inversion Principle : It focus on reusability of the systems / components. It tries to decouple classes who are very closely knit with the use of OOPS concept called Abstraction.
    Ways of achieving that are:
    1. High Level Module should not depend on Low level Module implementation. Both should depends on abstraction.
    2. Abstraction should not depend on details. Details should depend on abstraction.

Let’s analyse all the above principles in a real use case example.

Lets consider we develop a e-commerce application where
1. User can log in to the application if he/she has an account
3. Once the user log in he can browse/search different section (Mens , Women , Kids) and
4. Search categories (Shirts/Tops , Bottoms/Skirts , Shoes and Bags).
5. He can select any items and add to his cart
6. Edit and Checkout.

Search Categories
  1. Single Responsibility. As explained above single responsibility emphasises on focus on single task for a class. In a login page we can have view event, business logic and calling the backend for authentication in one View Controller. This gives rise to massive view controller problem as shown below.
class LoginViewController {@IBOutlet var username
@IBOutlet var password
@IBOutlet loginbutton
func viewDidLoad() {
//View event
}
func isemailvalid() -> Bool{
// Business logic
}
func ispasswordvalid() -> Bool{
// Business logic
}
@IBAction func Login(){
// Call backend API for authentication
}
func doLogin(){
//Create request and get back data
}
func parsedataToModel() {
// Business logic
}
func showError() {
//View event
}
func viewDidDissappear() {
//View event
}
}

So in order to follow the SOLID principle you have to divide the whole ViewController classes into granule classes with each classes handling single reponsibility

class LoginViewController {@IBOutlet var username
@IBOutlet var password
@IBOutlet loginbutton
lazy var viewmodel = ViewModel()
func viewDidLoad() {
//View event
}
func showError() {
//View event
}
@IBAction func Login(){
// Call viewmodel to request backend API for authentication
viewmodel.callLoginApi()
}
func viewDidDissappear() {
//View event
}
}
--------------------------------------------------------------------class ViewModel {
lazy networkmanager = Networkmanager()
func isemailvalid() -> Bool{
// Business logic
}
func ispasswordvalid() -> Bool{
// Business logic
}
func callLoginApi() {
// Networks calls can be made in view model and a call back to VC once the data is available.
}}
--------------------------------------------------------------------
class NetworkManger {
func doLogin(){
//Create request and get back data
}
func parsedataToModel() {
// Business logic
}
}--------------------------------------------------------------------
class LoginModel {

var authtoken
var refreshtoken
// etc
}

2. Open / Closed Principle : As per this principle the component should be extensible. If there is are few component which has quite some commonality then the they should have a base class and they should extend over it. As per our e-commerce use case we have men, women and child shoes section. There we need to provide the shoes base class which defines its size where as type and prices can be extended.

Class Shoes {
private var size : Int

init(size:Int) {
self.size = size
}
}
--------------------------------------------------------------
Class Women : Shoes {
var heelsSize
var price
var type : Type
init(size:Int , with heelsSize:Int) {
self.size = size
self.heelsSize = heelsSize
}
func hasHeels() -> Bool {}
func shoeType() -> Type {
switch type {
case heels
case flats
case sandals
case flipflops
}
}
--------------------------------------------------------------
Class Men : Shoes {
var price
@override init(size:Int) {
self.size = size
}
func shoeType() -> Type {
switch type {
case sports
case formal
case sandals
case flipflops
}
}
--------------------------------------------------------------
Class Kids : Shoes {
var price
var type : Type
init(size:Int , type: Type) {
self.size = size
self.type = type
}
func discountOnType () -> price {
switch type {
case school ,
price = 20% discount
case playground
price = 10% discount
}
}
func shoeType() -> Type {
swicth type {
case school
case playground
case sandals
case flipflops
}
}

So as you can see Shoes is the base class and it is extended with the help of OOPS Inheritance concept. Any class can inherit its base properties and create other features around it, like in Women class it has an exclusive property heel size and different type of shoes based on heel size or in Kids class discount is based on the type of shoes.

3. Liskov substitution: This principle complements the above Open/Closed principle. As per Liskov we should always try to use the derived classes rather then base class. Consider one use case where the user wants to add various shoes for his family in the shopping cart. So we should implement it like this

Class ShoppingCart {var items = [Shoes]()func addItems(shoes:Shoes) {
addItems.append(shoes)
}
}
let menshoe = Men(size:7)
let womenshoe = Women(size:6 , hasheels:true)
let kidsShoe = Kids(size:6 , type:school)
let cart = ShoppingCart()
cart.items.append(menshoe)
cart.items.append(womenshoe)
cart.items.append(kidsShoe)

Here we created different object for different derived classes rather then the base shoe class and appended in the item arrays. *Ofcourse the items array can not be only of Shoes type , it has to be more generic entity, but for a ease of understanding consider we are only selling shoes in the app :P

4.Interface segregation : This principles suggest to keep the interface small and simple so that the classes which adopt these interface don’t have to implement all the methods and computed properties that they don’t require . In swift this can be achieved using protocol composition. Look at the below example of a fat interface

@protocol ShoesFeature {var numberOfShoes : Int {get set}
var hasHeels : Bool {get set}
var type : Type {get}
var leatherquality
var lacetype
init(size:Int)
func discountOnSchoolShoes() -> Int
func shoeType(with heelSize:Int)
func warrantyOnLeather() -> Int // Years
func freePolishWithShoes() -> Bool // Free if its a school shoes
}// So all Men , Women and Kids which adopt these protocols has to implement all of them
--------------------------------------------------------------------
class Men:Shoes, ShoesFeature {
// Implement all methods and properties
}
class Women:Shoes, ShoesFeature {
// Implement all methods and properties
}
class Kids:Shoes, ShoesFeature {
// Implement all methods and properties
}

Rather break these protocol into small protocols (its called protocol composition)

protocol ShoesFeature {
var size : Int {get set}
var numberOfShoes : Int {get set}
init(size:Int)

}
--------------------------------------------------------------------
protocol WomenShoesFeature {
var hasHeels : Bool {get set}
var type : Type {get}
func shoeType(with heelSize:Int)
}
--------------------------------------------------------------------
protocol MenShoesFeature {
var leatherquality
func warrantyOnLeather() -> Int
}
--------------------------------------------------------------------
protocol KidsShoesFeature {
var type : Type {get}
var lacetype
func discountOnSchoolShoes() -> Int
func freePolishWithShoes() -> Bool
}

These protocols will be adopted by the Men , Women and Kids classes as follows:

class Women :Shoes, ShoesFeature, WomenShoesFeature 

var size : Int
var numberOfShoes : Int
var hasHeels : Bool
var type : Type
required init(size:Int) {
self.size = size
}

func shoeType(with heelSize:Int) { //Do Something }
}
--------------------------------------------------------------------
class Men :Shoes, ShoesFeature, MenShoesFeature {
var size : Int
var numberOfShoes : Int
var leatherquality
required init(size:Int) {
self.size = size
}

func warrantyOnLeather() -> Int { //Do Something }
}
--------------------------------------------------------------------
class Kids : Shoes , ShoesFeature, KidsShoesFeature {
var size : Int
var numberOfShoes : Int
var type : Type {get}
var lacetype
required init(size:Int) {
self.size = size
}
func discountOnSchoolShoes() -> Int { //Do Something }
func freePolishWithShoes() -> Bool { //Do Something }
}

So Classes adopt relevant methods and properties rather then unnecessary methods and properties.

5. Dependency Inversion : As the name it self suggest that this principle wants to loose the dependency that classes create among each other (mostly Higher Level classes on lower level class) . Because of this there is a tight coupling among both these level of classes and any change in the implementation in lower level class will have to be made in higher level class. For example in our e-commerce app, the ShoppingCart class is a higher level class which depends on the lower level class of EditingShoppingCart which helps user to edit the shopping cart before final checkout as below

class ShoppingCart {
private var editCartObj = EditShopingCart()
func editCart() {
editCartObj.editItems(edittype.decrement , 1, "ShoeId" )
}func checkout() {
if (editCartObj.editingDone) {
// Do Checkout
}
}
}
-------------------------------------------------------------------
class EditShopingCart {
var items = [Items]
editingDone = bool
func editItems(edittype : EditType* , count: Int , itemid : Int) {
switch edittype {
case increment ://increment the item by count in items array
case decrement ://decrement the item by count in items array
case remove://remove the item in the items array
}
}
}
}
* Where EditType is a emum whether to remove , increase or decrease the count of an added item.

Here in this ShoppingCart is a higher level class which depends on functionality of a lower level class EditShoppingCart. So any change in the EditShoppingCart implementation( change in methods or including an new important property) has to be modified in ShoppingCart class too. This is what is called as tight coupling of entities. So in order to avoid this we need to use a protocol to which the EditShoppingCart conforms to and ShoppingCart class uses the property and method of this protocol.

class ShoppingCart {
private var editing : Editing
// We pass the editing object through the initialiser
init(editing : Editing ) {
self.editing = editing
}
func editCart() {
editing.editCartObj.editItems(edittype.decrement , 1, "ShoeId" )
}func checkout() {
if (editing.editingDone) {
// Do Checkout
}
}
}
-------------------------------------------------------------------
@protocol Editing {
editingDone : Bool { get set}
func editItems(edittype : EditType* , count: Int , itemid : Int)
}-------------------------------------------------------------------
class EditShopingCart : Editing {
var items = [Items]
editingDone : Bool = false
func editItems(edittype : EditType* , count: Int , itemid : Int) {
switch edittype {
case increment ://increment the item by count in items array
case decrement ://decrement the item by count in items array
case remove://remove the item in the items array
}
editingDone = true
}
}
}
  1. This approach let us extract the ShoppingCart class in any other project beside
  2. we can instantiate ShoppingCart object with any other EditShoppingCart which conform to Editing protocol.
  3. It’s good for unit testing and increase your test coverage.
  4. This abstraction should define the higher level components needs, it’s not the lower level component which dictates how the protocol look like.

PS : The above Programming type to achieve Dependency Inversion is the advanced Protocol Oriented Programming variant which is very commonly used today in the developer community. Check my detailed article here https://medium.com/@mushtaque87/protocol-oriented-programming-2c448013b96a

So do try to follow this SOLID principles in your next swift development (Frontend or Server side) to design your app to be more scalable , robust and testable.

Moving Next I will write about various common design patterns that can be used in Swift,iOS development.

Happy Coding !!!

Leave a few claps if you like the topic
Follow me to access more advanced topics in 2020.

--

--

Mushtaque Ahmed
The Startup

Mobile Architect | iOS | Swift | RxSwift | GRPC | VIPER | | Protocol Oriented Programming | Realm | cocoapods | Appium | HTML | React.js | React Native