Category

tutorial

Every Developer MUST know SOLID Principles

photo by Frantzou Fleurine on Unsplash

SOLID will help you make your code extendable, more understandable, maintainable and encapsulated with a simple application of what you will see in this article.

NOTE: The code samples are written with Kotlin. The samples I use in this article are simple to make the principles easy to understand. It is not necessarily directly applicable in the real world. What i am trying to do here is explaining the 5 principles in the simplest manner. Let us dive in now.

What S.O.L.I.D stands for?

  • S: Single Responsibility Principle
  • O: Open-Closed Principle
  • L: Liskov Substitution Principle
  • I: Interface Segregation Principle
  • D: Dependency Inversion Principle

1- SRP: Single Responsibility Principle

A class should have one and only one reason to change, meaning that a class should only have one job.

This principle is applicable not only for classes, but also for functions, software components, and microservices. The idea of this principle is Decoupling and Cohesion the components from each other. So that change in one component must not cause a change in another.

CODE:

class Car {
constructor(name: String) { /. . ./ }
fun getCarName() { /. . ./ }
fun saveCarToDB(newCar: Car) { /. . ./ }
}

The Car class violates the SRP. Why?

The constructor and getCarName are both for properties management. But the saveCarToDB function manage the Storage of the new car. which add another responsibility for the Car class. The SRP version of this example is to split the class into two with ONE responsibility for each.

BETTER CODE:

class Car {
constructor(name: String) { /. . ./ }
fun getCarName() { /. . ./ }
}
class CarDB {
fun saveCarToDB(newCar: Car) { /. . ./ }
fun getCarFromDB(carName: String) : Car { /. . ./ }
}

Applying this principle in every and each component makes the code Cohesive and Decoupled.

2- OCP: Open-Closed Principle

Software objects or entities (Classes, modules, functions) should be open for extension, but closed for modification.

That means A class should have one and only one reason to change, meaning that a class should only have one job.

Let’s continue with the Car‘s example to iterate thorough cars and count their doors count:

CODE:

class Car {
var name : String = ""
constructor(name: String) {
this.name = name
}
}
fun main(args: Array) {
var cars = arrayOf(Car("BMW"), Car("MINI"), Car("VW"))
printCarDoorCount(cars)
}
fun printCarDoorCount(cars: Array) {
cars.forEach { car ->
var doorsCount = 0
if (car.name == "BMW") {
doorsCount = 4
} else if (car.name == "VW") {
doorsCount = 4
} else if (car.name == "MINI") {
doorsCount = 2
}
println("${car.name} has $doorsCount")
}
}

The function printCarDoorCount violates the opened-closed principle. Because it cannot be closed against a new kinds of cars!

Adding new Car will force to add new condition in the if-else in printCarDoorCount function. That was quit a simple example. If you have many functions like this in your application, unfortunately, you will have to modify them all. If you miss one of them, most probably you will have your app returning unexpected results.

BETTER CODE:

abstract class Car {
var name : String = ""
constructor(name: String) {
this.name = name
}
abstract fun doorsCount() : Int
}
class Bmw() : Car("BMW") {
override fun doorsCount() : Int {
return 4
}
}
class MiniCooper() : Car("MiniCooper") {
override fun doorsCount() : Int {
return 2
}
}
class Volkswagen() : Car("Volkswagen") {
override fun doorsCount() : Int {
return 4
}
}
class Fiat() : Car("Fiat") {
override fun doorsCount() : Int {
return 3
}
}
fun main(args: Array) {
var cars = arrayOf(Bmw(), MiniCooper(), Volkswagen(), Fiat())
printCarDoorCount(cars)
}
fun printCarDoorCount(cars: Array) {
cars.forEach { car ->
println("${car.name} has ${car.doorsCount()}")
}
}

Car now has an abstract function, it may be called virtual method in some languages. We have each new car extends from our abstract Car class.

Each and every new Car will have to decide on how many doors will it have. printCarDoorCount will iterate on the Cars array and only call the car’s doorsCount().

Now, if we add a new car, printCarDoorCount will not change. All you need to do is to add the new car to the cars array. You can see the Fiat class upside or you may add your favorite car too 🙂

Now printCarDoorCount applies and conforms the OCP principle. 💪🏻

3- LSP: Liskov’s Substitution Principle

A sub-class must be substitutable for its super-class

This means, simply, the Parent class should be able to be replaced by any of its Subclasses in any region of the code.

As a sign for violating this principle, checking the type of class and doing something depending on that (That also may break OCP).

Let us continue with our Car’s example. We will write a function that will print the Car’s engine capacity:

CODE:

fun printEngineCapacity(cars: Array){
for (car in cars){
if (car is Bmw) {
println("${car.name} has 3400 CC Engine")
} else if (car is Volkswagen) {
println("${car.name} has 2600 CC Engine")
} else if (car is MiniCooper) {
println("${car.name} has 3000 CC Engine")
} else if (car is Fiat) {
println("${car.name} has 1600 CC Engine")
}
}
}

The function printEngineCapacity will work fine and print each car’s engine capacity. But, we are here for writing a good code. This violates LSP (and OCP).

If we add any new Car’s type and send it with the array, the printEngineCapacity function will not work as expected. We have to add new else-if to our function. if the function accepts Car’s (Super-Class) as parameter, it should not check its Sub-classes inside that function.

BETTER CODE:

abstract class Car {
var name : String = ""
constructor(name: String) {
this.name = name
}
abstract fun doorsCount() : Int
abstract fun engineCapacity(): Int
}
class Bmw() : Car("BMW") {
override fun doorsCount() : Int {
return 4
}
override fun engineCapacity() : Int {
return 3400
}

}
class MiniCooper() : Car("MiniCooper") {
override fun doorsCount() : Int {
return 2
}
override fun engineCapacity() : Int {
return 3000
}

}
class Volkswagen() : Car("Volkswagen") {
override fun doorsCount() : Int {
return 4
}
override fun engineCapacity() : Int {
return 2600
}

}
class Fiat() : Car("Fiat") {
override fun doorsCount() : Int {
return 3
}
override fun engineCapacity() : Int {
return 1600
}

}
fun main(args: Array) {
var cars = arrayOf(Bmw(), MiniCooper(), Volkswagen(), Fiat())
printEngineCapacity(cars)
}
fun printEngineCapacity(cars: Array){
for (car in cars){
println("${car.name} has ${car.engineCapacity()} CC Engine Capacity")
}
}

Now the printEngineCapacity method doesn’t need to know the type of Car to know the Engin Capacity, it just calls the engineCapacity() method of the Car type because by contract (we decided in abstract Car class) a sub-class of Car class must implement the engineCapacity() function.

4- ISP: Interface Segregation Principle:

Clients should not be forced to depend upon interfaces that they do not use

In other words, that means many client specific interfaces are better than one general interface (Where user will be forced to implement methods that will not use).

This example will be a bit different. Lets check this code:

CODE:

interface ISport {
fun swim()
fun run()
fun longJump()
fun highJump()

fun getReady()
fun stop()
}
class Swimming : ISport {
var isReady = false
override fun swim() {
if (isReady) {
println("Swimming…")
} else {
println("Not Ready Yet")
}
}

override fun run() {}
override fun longJump() {}
override fun highJump() {}


override fun getReady() {
isReady = true
println("Ready to Swim")
}

override fun stop() {
println("Stopped Swimming")
}
}
fun main(args: Array) {
val swimmerFoo = Swimming()
swimmerFoo.getReady()
swimmerFoo.swim()
}

Interface segregation principle actually deals with the disadvantages of implementing big interfaces.

ISport interface knows more than it needs to know. The shared methods between all Sport classes (in our example) are getReady() and stop(). In Swimming class, we have implemented extra unused functions run(), longJump(), and highJump().

If we add another Sport, say Running, that extends from ISport, We will implement another 3 extra unused methods (swim(), longJump(), highJump()). So let’s see the better way of doing that.

BETTER CODE:

interface ISport {
fun getReady()
fun stop()
}


interface ISwimming : ISport {
fun swim()
}

interface IRunning : ISport {
fun run()
}

class Swimming : ISwimming {
var isReady = false
override fun swim() {
if (isReady) {
println("Swimming…")
} else {
println("Not Ready Yet")
}
}
override fun getReady() {
isReady = true
println("Ready to Swim")
}
override fun stop() {
println("Stopped Swimming")
}
}

class Running : IRunning {
var isReady = false
override fun run() {
if (isReady) {
println("Running…")
} else {
println("Not Ready Yet")
}
}
override fun getReady() {
isReady = true
println("Ready to Run")
}
override fun stop() {
println("Stopped Swimming")
}
}

fun main(args: Array) {
val swimmerFoo = Swimming()
swimmerFoo.getReady()
swimmerFoo.swim()

val runnerBoo = Running()
runnerBoo.getReady()
runnerBoo.run()
}

Now ISport contains only the methods that needs to be implemented in all inhereted classes. Only the Swimming will have to implement swim() in ISwimming interface.

NOTE: you can add start() method to ISport interface and implement the start() method in each class. But for the sake of explaining the ISP I added swim, run, highJump, and longJump methods.

5- DIP: Dependency Inversion Principle:

A. High-level modules should not depend on low-level modules. Both should depend on abstractions.
B. Abstractions should not depend on details. Details should depend on abstractions.

In another words: Dependency should be on abstractions not concretions

CODE:

open class Worker {
open fun work() {
println("General Worker is now working")
}
}

class Accountant : Worker() {
override fun work() {
println("Accountant is doing the calculations now")
}
}

class Manager {
var workers : ArrayList = arrayListOf()
fun addWorker(worker: Worker){
workers.add(worker)
}
fun manageWorkers() {
for (worker in workers) {
worker.work()
}
}
}

fun main(args: Array) {
val manager = Manager()
manager.addWorker(Worker())
manager.addWorker(Accountant())

manager.manageWorkers()
}

That was a bad example since dependency is on concretions not on abstractions.

BETTER CODE:

interface IWorker {
fun work()
}

interface IManager {
fun addWorker(worker: IWorker)
fun manageWorkers()
}

class GeneralWorker : IWorker{
override fun work() {
println("General Worker is now working")
}
}

class Accountant : IWorker {
override fun work() {
println("Accountant is doing the calculations now")
}
}

class Manager : IManager {
var workers : ArrayList = arrayListOf()
override fun addWorker(worker: IWorker)

override fun addWorker(worker: IWorker) {
workers.add(worker)
}

override fun manageWorkers() {
for(worker in workers){
worker.work()
}
}
}

fun main(args: Array) {
val manager = Manager()
manager.addWorker(GeneralWorker())
manager.addWorker(Accountant())

manager.manageWorkers()
}

In the better version code, both high-level modules and low-level modules depend on abstractions. Of course, this comes with overhead of writing additional code, the advantages that DIP provides outweigh the extra effort. So this principle is not to be applied for every and each class and module. For instance, If we have a class with functionality that is more likely to remain unchanged in the future there is no need to apply this principle.

Conclusion:

Those were the five SOLID principles which every software developer must know. I tried to use simple examples to make it as easy as possible for beginner developer to understand.

It might be hard at first to conform to all these principles at every and each software you write, but with consistent practice and devotion, it is going to become a part of your software and mentality of writing and will greatly have a huge impact on the maintainability and readability of our software or app.

NOTE: Since the code used in this article is Kotlin, You can run and change the example codes written here online on https://try.kotlinlang.org

تصميم واجهة لتطبيقات الايفون بدون الستوري بورد – Storyboard

بسم الله الرحمن الرحيم

عندما بدأ بكتابة تطبيقات للايفون كنت استخدم الستوري بورد لاصغر تفاصيل تصميم الواجهات. كان من السهل رؤية المكونات و العلاقة بينها و ربط المكونات ببعضها. لكن لم يطل بي الى ان وجدت ان ليس كل شيء سهل هو الأفضل. عندما تعمل مع فريق على تطبيق ايفون قد يكلفك اضافة زر على نفس الصفحة التي قد عمل عليها مبرمج اخر ساعات من محاولة دمج الكود الذي تم تعديله. ايضا واجهتني مشاكل كثيرة عندما حاولت نقل كود مع الواجهة المتعلقة به من مشروع الى اخر. لم ياخذ البحث عن حل اكثر من بحث على جوجل لاجد انه بامكانك انشاء و ربط مكونات الواجهات ببعضها عن طريق الكود. و بحكم خبرتي القليل ببرمجة تطبيقات الايفون عندما بدأت اقرأ المقالات وجدت اكواد مخيفة و طويلة لكتابة واجهات بسيطة.

 

لا تخف هناك حل بسيط للغاية و قد سهل حياتي و اختصر على الوقت و عدت لاحب كتابة مشاريع للايفون مرة اخرى 🙂
الحل هو مكتبة SnapKit – Autolayout DSL Library

للبدأ بكتابة تطبيقات باستخدام هذه المكتبة يجب اولا اضافة كوكوبودس ( الذي يسهل اضافة المكاتب للمشروع) ستجد كيفية اضافته هنا

باضافة كوكوبودس للمشروع و اضافة مكتبو السنابكيت سيتم انشاء ملف اضافي للمشروع صيغته ..xcworkspace

الان سنفتح المشروع و اول خطوة نقوم بها هي حذف الستوري بورد لنبدأ صفحة جديدة في برمجة تطبيقات الايفون 💪🏻

لا تنسى حذف كلمة Main كما مبين في الصورة التي في الاعلى

نقطة البداية في التطبيق يتم تحديدها من الستوري بورد لكن بعد ان حذفناه يجب انشائها برمجياً ايضاً من ملف

AppDelegate.swift

الان التطبيق سيعمل و الشاشة الرئيسية ستكون الملف الذي حددناه كما في الصورة اعلى النص

لنبدأ بالقسم الممتع الان باضافة بعض مكونات الواجهات. لننتقل الى ملف الشاشة التي جعلناها شاشة رئيسية الان

ViewController.swift

١- اضافة مكتبة السنابكيت لنستطيع استدعاء دالاتها

٢- انشاء مكونة الواجهة (وهذا يقابل اضافة مكونات الواجهة بالسحب و الافلات على الستوري بورد)

٣- تغيير لون الشاشة لنتأكد من اضافة المكون الرئيسي

٤- اضافة المكون الذي تم انشائه الى الشاشة

٥- استخدام السنابكيت لجعل المكونة الرئيسية تملأ كل الشاشة

الآن لنصعب الامور قليلاً. سنقوم باضافة مكونة واجهة اخرى لتملأ نصف الشاشة العلوي و اضافة زر ليتوضع تحت المكونة التي ستملأ نصف الشاشة

في المربع الاحمر قد انشأت مكونة النصف العلوي و تمت اضافتها الى المكونة الرئيسية

الان لنضيف زر يتوضع تحت النصف العلوي مباشرةً

ليتوضع الزر تحت مكونة النصف العلوي تم ربط الحافة العلية للزر بالحافة السفلية للنصف العلوي بالسطر التالي:

يمكنك الان تجريب خيارات اخرى و تصميم واجهات اكثر تعقيداً باستخدام سنابكيت

في حال لديكم اي سؤال يمكنكم ان تكتبوا تعليق او تتواصلوا معلي عن طريق وسائل التواصل المذكورة في الصفحة الرئيسية للموقع.

 

قد تكون هنالك اخطاء باللغة و تسمية المكونات و لذلك ارجو منكم مساعدتي في اصلاحها ايضاً 🙂

بارك الله فيكم