Protocol-Oriented Programming (POP) is a programming paradigm introduced in Swift that emphasizes the use of protocols as a way to define and enforce common behavior for multiple types. POP is a powerful tool for designing and organizing code, and can be used to achieve many of the same goals as object-oriented programming, but with greater flexibility, modularity, and safety.

The core idea behind Protocol-Oriented Programming is that protocols should be used to define the behavior that multiple types should have in common. For example, a protocol might define a set of methods or properties that must be implemented by any type that conforms to the protocol. This allows for the creation of abstractions that can be reused and composed in many different ways.

One of the key benefits of Protocol-Oriented Programming is that it encourages the use of composition over inheritance. Instead of relying on inheritance to share behavior between types, POP allows for the creation of protocols that can be adopted by multiple types. This leads to a more flexible and modular codebase, as protocols can be adapted and reused in many different contexts.

Another advantage of Protocol-Oriented Programming is that it helps to enforce the Single Responsibility Principle. By breaking down code into smaller, focused protocols, it becomes easier to understand and maintain code, and to identify and isolate bugs. This makes it easier to write reliable and maintainable code.

In addition, Protocol-Oriented Programming makes it easier to write generic and reusable code. By using protocols to define common behavior, it becomes possible to write functions and algorithms that work with multiple types, regardless of their specific implementation. This can greatly reduce the amount of duplicated code and make code more maintainable.

There are a few best practices that developers should follow when using POP in Swift. Firstly, it’s important to define protocols with clear and concise intent. Protocols should be small and focused, and should only define the minimum necessary behavior required by types that conform to the protocol.

It’s also important to use extensions to add default implementations of methods and properties to protocols. This can help to reduce the amount of code that needs to be written and make it easier to write generic and reusable code.

Finally, it’s important to take advantage of Swift’s type system and to use generics where appropriate. By using generics, it becomes possible to write algorithms and functions that work with multiple types, regardless of their specific implementation.

In conclusion, Protocol-Oriented Programming is a powerful programming paradigm that can be used to achieve many of the same goals as object-oriented programming, but with greater flexibility, modularity, and safety. By breaking down code into smaller, focused protocols, it becomes easier to write reliable and maintainable code, and to create abstractions that can be reused and composed in many different ways. By following best practices and taking advantage of Swift’s type system, developers can create powerful and reusable code with Protocol-Oriented Programming .

Best practice and an example of Protocol-Oriented Programming (POP) in Swift:

// Define a protocol with a property and a method
protocol Shape {
    var area: Double { get }
    func describe() -> String
}

// Implement the protocol for a struct
struct Square: Shape {
    var sideLength: Double
    
    var area: Double {
        return sideLength * sideLength
    }
    
    func describe() -> String {
        return "I am a square with side length \(sideLength) and area \(area)."
    }
}

// Implement the protocol for a class
class Circle: Shape {
    var radius: Double
    
    var area: Double {
        return Double.pi * radius * radius
    }
    
    func describe() -> String {
        return "I am a circle with radius \(radius) and area \(area)."
    }
}

// Use the protocol to create an array of shapes
let shapes: [Shape] = [Square(sideLength: 2.0), Circle(radius: 1.0)]

// Iterate over the array and call the describe method on each shape
for shape in shapes {
    print(shape.describe())
}

In this example, the Shape protocol defines a common interface for types that represent shapes. The Square struct and the Circle class both conform to the Shape protocol and implement the required properties and methods.

The use of protocols in this example allows us to create an array of Shape objects, regardless of their specific implementation, and call the describe method on each object in a uniform manner. This demonstrates the benefits of POP in terms of flexibility, abstraction, and reusability.

More knowledge keyword on Protocol-Oriented Programming

Object-Oriented Design (OOD)
Procedural programming

the big idea about Functional Reactive Programming (FRP) and Imperative Programming : which one to use?

The choice between using Functional Reactive Programming (FRP) and Imperative Programming often depends on the particular problem being solved and the specific requirements of the project.

Here are some general guidelines for when to use FRP and when to use Imperative Programming:

Functional Reactive Programming:

For handling asynchronous and event-driven systems, such as UI updates, network requests, and user interactions. FRP is well-suited for these scenarios because it allows the programmer to model and manipulate streams of values that change over time in a declarative way.
For managing complex and large data flows. FRP can help simplify the management of complex data flows by modeling them as streams of values that can be transformed and combined in a functional manner.
When concurrency and parallelism are important considerations. FRP libraries often provide built-in support for concurrency and parallelism, making it easier to write and maintain concurrent and parallel code.
Imperative Programming:

For solving problems that require low-level control over the execution flow, such as system-level programming, device drivers, and performance-critical algorithms. Imperative Programming provides the programmer with fine-grained control over the flow of execution, which can be important in these scenarios.
For small, simple problems that do not require complex data flows or asynchronous and event-driven systems. Imperative Programming can be a simpler and more straightforward approach for small problems.
When performance is the primary concern and there is no need for handling complex data flows or asynchronous and event-driven systems. Imperative Programming can be faster and more efficient than FRP in some scenarios because it provides fine-grained control over the execution flow.
Best Practices:

When using FRP, it’s important to understand the contract of the FRP library you are using, including the types and operations it provides.
Avoid overloading the system with too many streams, as this can lead to complex and difficult-to-maintain code.
Use FRP for modeling and transforming streams of values that change over time, and use Imperative Programming for low-level control over the execution flow and performance-critical code.
Keep in mind the trade-off between the simplicity and expressiveness of FRP and the fine-grained control and efficiency of Imperative Programming, and choose the approach that best fits the specific requirements of your project.

Functional Reactive Programming (FRP) is a programming paradigm that has gained popularity among iOS developers due to its ability to handle asynchronous events and its emphasis on immutability, composability, and purity. FRP allows iOS developers to write clean, maintainable code that is easy to understand and to scale.

In traditional iOS development, handling asynchronous events can be complex and error-prone. For example, updating the user interface in response to a network request can be difficult to manage in an imperative programming style. In FRP, we represent asynchronous events as streams, and we use functional operations to manipulate these streams. This makes it easier to manage asynchronous events, and to avoid the kind of bugs that can arise when dealing with asynchronous events in imperative programming.

Another advantage of FRP is that it emphasizes immutability, which is a key feature of Swift. In FRP, we treat values as streams that change over time. This makes it easier to reason about how values change over time, and to avoid the kind of bugs that can arise when dealing with mutable state in imperative programming.

FRP also makes it easier to build responsive user interfaces. User interfaces are often event-driven, with many different events happening simultaneously. In traditional iOS development, it can be difficult to manage these events, especially when multiple events are happening at the same time. In FRP, we can represent these events as streams, and we can use functional operations to combine and manipulate these streams. This makes it easier to build responsive user interfaces that react to events in real-time.

There are several FRP libraries available for iOS development, including ReactiveSwift and RxSwift. These libraries provide a set of functional operations for working with streams, and they make it easier to write code that is concise, composable, and easy to understand. For example, ReactiveSwift provides functional operations like map, filter, and reduce, which make it easy to manipulate streams of data. RxSwift provides a similar set of functional operations, and it also provides a convenient syntax for working with streams.

One of the key advantages of using an FRP library is that it makes it easier to write clean, maintainable code. In traditional iOS development, it can be difficult to manage the complexity of asynchronous events, especially when multiple events are happening at the same time. In FRP, we can represent these events as streams, and we can use functional operations to combine and manipulate these streams. This makes it easier to manage the complexity of asynchronous events, and to write code that is easy to understand and to maintain.

Another advantage of using an FRP library is that it makes it easier to write code that is scalable. Reactive programming is designed to handle large amounts of data, and FRP extends this capability to the realm of functional programming. This makes it easier to build scalable applications that can handle large amounts of data, and that can be easily maintained and extended over time.

In conclusion, FRP is a powerful programming paradigm that is well-suited for iOS development. It makes it easier to handle asynchronous events, to build responsive user interfaces, to write clean, maintainable code, and to build scalable applications. If you’re an iOS developer looking for a way to build robust, scalable, and responsive applications, then Functional Reactive Programming is definitely worth considering. With the help of libraries like ReactiveSwift and RxSwift, you can start incorporating FRP into your iOS development workflow today

Some samples and best practise for Functional Reactive Programming

Here is asimple example of using the ReactiveSwift library to perform a network request and update the user interface in response.

 

import ReactiveSwift
import Result
import UIKit

class ViewController: UIViewController {

  @IBOutlet weak var label: UILabel!

  override func viewDidLoad() {
    super.viewDidLoad()

    // Create a SignalProducer that performs a network request
    let producer = SignalProducer { observer, lifetime in
      let request = URLRequest(url: URL(string: "https://example.com/data")!)
      URLSession.shared.dataTask(with: request) { data, response, error in
        if let error = error {
          observer.send(error: error)
        } else {
          guard let data = data,
            let string = String(data: data, encoding: .utf8) else {
              observer.send(error: NSError(domain: "", code: 0, userInfo: nil))
              return
          }
          observer.send(value: string)
          observer.sendCompleted()
        }
      }.resume()
    }

    // Use the SignalProducer to update the label with the response
    producer
      .startWithResult { result in
        switch result {
        case let .success(value):
          DispatchQueue.main.async {
            self.label.text = value
          }
        case let .failure(error):
          print(error)
        }
      }
  }
}


In this example, we create a SignalProducer that performs a network request using URLSession. The SignalProducer is started using the startWithResult method, which takes a closure that is called with the result of the network request. If the request is successful, the closure updates the label with the response. If the request fails, the closure prints the error.

This is a simple example of how FRP can make it easier to handle asynchronous events and update the user interface. By representing the network request as a SignalProducer, we can manipulate the response using functional operations, making it easier to manage the complexity of the asynchronous event.

Here are some best practices for using FRP in iOS and Swift development:

Represent values that change over time as streams: Use Signal or SignalProducer to represent values that change over time, and use functional operations to manipulate these streams.
Emphasize immutability: Use let instead of var when possible, and avoid using mutable state as much as possible. This will make your code easier to understand and maintain.
Write composable code: Use functional operations like map, filter, and reduce to write code that is easy to understand and to maintain.
Use error handling: Use Result or Try to handle errors in a functional way. This will make your code easier to understand and to maintain.
Test your code: Use unit tests to verify that your code works as expected. FRP makes it easier to write testable code, so take advantage of this by writing tests for your code.
These are just a few best practices for using FRP in iOS and Swift development.
Happy reading

To perform explicit SSL bumping with Squid, you need to perform the following steps:

Generate a SSL certificate and key: You can either generate a self-signed certificate or obtain one from a certificate authority. The certificate and key will be used by Squid to encrypt and decrypt the traffic.
Install and configure Squid: Squid is a popular open-source proxy server that can be used to perform explicit SSL bumping. You need to install Squid on the system and configure it to listen on the desired port.
Configure SSL bumping: In the Squid configuration file, enable SSL bumping by adding the following lines:


ssl_bump bump all
ssl_cert_file /path/to/cert.pem
ssl_key_file /path/to/key.pem

Specify the destination servers: In the Squid configuration file, specify the destination servers to which the encrypted traffic will be forwarded. You can also specify a list of domains for which SSL bumping will be performed.
Start Squid: Start Squid with the new configuration. Once started, Squid will begin decrypting and encrypting the SSL/TLS traffic based on the configuration.
Here is an example Squid configuration that demonstrates explicit SSL bumping:

http_port 3128
ssl_bump bump all
ssl_cert_file /path/to/cert.pem
ssl_key_file /path/to/key.pem

acl SSL_ports port 443
acl Safe_ports port 80          # http
acl Safe_ports port 21          # ftp
acl Safe_ports port 443         # https
acl Safe_ports port 70          # gopher
acl Safe_ports port 210         # wais
acl Safe_ports port 1025-65535  # unregistered ports
acl Safe_ports port 280         # http-mgmt
acl Safe_ports port 488         # gss-http
acl Safe_ports port 591         # filemaker
acl Safe_ports port 777         # multiling http
acl CONNECT method CONNECT

http_access allow CONNECT SSL_ports
http_access deny !Safe_ports
http_access deny CONNECT !SSL_ports

always_direct allow all

Basic guide About Explicit SSL bumping

Explicit SSL bumping also known as “SSL interception,” is a feature of some reverse proxies and security appliances that allows the proxy to decrypt, inspect, and re-encrypt SSL/TLS encrypted traffic.

The proxy acts as a man-in-the-middle, decrypting incoming SSL/TLS traffic and re-encrypting it before forwarding it to the destination server. This allows the proxy to inspect and filter the encrypted traffic based on various security policies, such as filtering out malicious traffic, monitoring and controlling data transfers, and enforcing content filtering.

Explicit SSL bumping is typically used in enterprise networks to provide additional security and control over network traffic. However, it can also introduce security risks and violate the privacy of encrypted communication. It’s important to carefully consider the use of explicit SSL bumping and weigh the security benefits against the privacy risks before deploying it in a network.

To enable SSL bump in HAProxy, follow these steps:

Obtain a SSL certificate and key. You can either generate a self-signed certificate or obtain one from a certificate authority.
Create a new HAProxy frontend and backend configuration. In the frontend configuration, specify the SSL certificate and key with the crt and key options, and enable SSL by using the ssl option. In the backend configuration, specify the target servers to which HAProxy will forward requests.
In the frontend configuration, add a reqadd directive to insert a header indicating that SSL bumping is in effect. This header can be used by the backend servers to determine if a request has been SSL-bumped or not.
In the backend configuration, configure the backend servers to validate the header inserted by HAProxy. This can be done by using the http-request set-header directive to add a header indicating that SSL bumping has taken place.
Start or reload HAProxy with the new configuration.
Here’s an example HAProxy configuration that demonstrates SSL bumping:

 
global
        log 127.0.0.1 local0 notice
        maxconn 4096
        user haproxy
        group haproxy

defaults
        log global
        mode http
        option httplog
        option dontlognull
        retries 3
        option redispatch
        timeout connect 5000
        timeout client 50000
        timeout server 50000

frontend http-in
        bind *:80
        mode http
        default_backend servers

frontend https-in
        bind *:443 ssl crt /path/to/cert.pem key /path/to/key.pem
        mode http
        reqadd X-Forwarded-Proto:\ https
        default_backend servers

backend servers
        mode http
        balance roundrobin
        server server1 192.168.1.100:80 check
        server server2 192.168.1.101:80 check
        http-request set-header X-SSL-Bumped true if { hdr(X-Forwarded-Proto) -i https }


Functional Reactive Programming (FRP) is a programming paradigm that combines the functional programming style with reactive programming. Reactive programming is a programming paradigm that deals with asynchronous data streams and the propagation of change. It’s a way of handling events that occur in the user interface, network requests, or other event-driven systems. In FRP, the focus is on treating values as streams of data that change over time, and functions that react to these changes. This makes FRP a powerful tool for building user interfaces and handling complex event-driven systems.

Functional programming is a programming paradigm that emphasizes immutability, purity, and composability. It’s a style of programming that focuses on writing functions that have no side effects and always return the same output for the same inputs. FRP extends these principles to the realm of reactive programming by allowing us to express time-varying values as streams and to compose these streams using functional operations.

In FRP, we represent values that change over time as streams. A stream is an abstract data structure that represents a time-varying value. Just as arrays represent sequences of values, streams represent sequences of values that change over time. Streams can be manipulated using functional operations like map, filter, and reduce. This allows us to write code that is concise, composable, and easy to understand.

One of the key advantages of FRP is that it makes it easier to reason about time-varying values. In traditional imperative programming, it’s often difficult to keep track of how values change over time. This can lead to subtle bugs and make it difficult to maintain and scale an application. In FRP, we represent time-varying values as streams, and we use functional operations to manipulate these streams. This makes it easier to reason about how values change over time, and to avoid the kind of bugs that can arise when dealing with time-varying values in imperative programming.

Another advantage of FRP is that it makes it easier to write responsive user interfaces. User interfaces are often event-driven, with many different events happening simultaneously. In traditional imperative programming, it can be difficult to manage these events, especially when multiple events are happening at the same time. In FRP, we can represent these events as streams, and we can use functional operations to combine and manipulate these streams. This makes it easier to build responsive user interfaces that react to events in real-time.

FRP is also a good choice for building applications that require real-time data processing. Reactive programming is well-suited for dealing with streams of data that change over time. In FRP, we can use functional operations to manipulate these streams and to extract the information we need in real-time. This makes FRP a good choice for building applications that require real-time data processing, such as financial trading systems, real-time monitoring systems, and real-time control systems.

Finally, FRP is a good choice for building scalable applications. Reactive programming is designed to handle large amounts of data, and FRP extends this capability to the realm of functional programming. This makes it easier to build scalable applications that can handle large amounts of data, and that can be easily maintained and extended over time.

In conclusion, FRP is a powerful programming paradigm that combines the best of functional programming and reactive programming. It makes it easier to reason about time-varying values, to build responsive user interfaces, to build applications that require real-time data processing, and to build scalable applications. If you’re looking for a way to build robust, scalable, and responsive applications, then Functional Reactive Programming is definitely worth considering.

REF

https://stackoverflow.com/questions/1028250/what-is-functional-reactive-programming

https://gist.github.com/staltz/868e7e9bc2a7b8c1f754

What is Clean Architecture ?

Clean Architecture which is also known as Domain-Driven Design has evolved with considerable improvements in the last several years. Some architecture names used for clean architecture over the years are given below:

Hexagonal Architecture (https://en.wikipedia.org/wiki/Hexagonal_architecture_(software))
Onion Architecture
Domain-Driven Design (DDD) or Domain Centric Architecture
vertical Slice Architecture
Clean Architecture

Smell of a good architecture

An architecture design is good when
+ it breaks down the complexity of the software in to manageble and simple problems
+ small interfaced were defined
+ the components are decoupled
+ the components have clearly defined repsponsibilities
+ the software is maintainable
+ the software can be easily changed and extended
+ the software is stable
+ Bugs can fixed quickly
+ the software is reusable in other software projects
+ the code is understandable in a natural way

Smell of a bad architecture

Obviously, a bad architecture have those listed attributed, but in opposite.
Besides, more symptoms are:
+ Small change in requirements leads to big changes in code
+ Developers are afraid to change code
+ Many workarounds, code is developed around it
+ Changes have unknown and undetected side effects
+ Reuse through code duplication (copy-paste). Develops chase after the places that need to be changed
+ Cyclic relations between artifacts. Artifacts that are used in different cycles often play several roles, which makes them difficult to understand, and can not be exchanged easily

Basic Principles behind Clean Architecture

The primary idea in Clean Architecture is to make the solution adaptive, keep the core business or application logic use cases independent of frontend and external frameworks. In summary, we can outline the following outcomes of clean architecture,

Independent of UI
Using clean architecture, we should be able to change UI or presentation layers easily without changing the application layer and so on. UI can be from any front-end framework, or console UI, any web, and can be replaced without changing the other layers or rest of the system.
Database Independent
The architecture should be flexible enough to swap the database without affecting the application use cases and entities. The solution can switch the dataset to MS SQL, MySQL, Oracle, MongoDB, or something else.
Independent of External agency/libraries/Drivers
The business rules should be independent of external parties or agencies.
Framework Independent
The core business or application rules should be independent of the existence of frameworks, libraries for the future. We can include the frameworks but as tools, and the solution should not be completely relied on those.
Testable
The architecture should comply with the testing of the core application and business cases and rules without the UI, database, Web server, or any external component.

Architectural Diagram

tranhuy _ Clean Architecture

Layers of Clean Architecture

tranhuy _ Layers of Clean Architecture

Entities
This is also known as enterprise business rules. It consists of plain domains. In this layer, we add objects (entity or domain) with no frameworks and annotations. We add general logics that are applied to every domain like validations, base entities, and so on. They are the least affected by external changes and no dependencies.

Use cases
This is a pure logic layer where we write core business or application logic. This is also called Application Business rules. We, in general, use the term service or manager for application use cases. This layer uses a domain layer and builds results. In this use case, we don’t know who triggers or how the result will be presented. However, based on services, we keep business logic independent of UI or database. We may use some libraries, however, only as tools.

Interface Adapters
This layer acts as a communicator to convert data into desired format for storing into external sources like database, file system, 3rd parties, and convert data for use cases or business logic. This layer is also called as adapters that necessarily do the conversion of data in both ways. We can consider MVC of GUI or REST APIs at this level that consists of presenters, views, and controllers. It also implements the interfaces of use cases required for external components.

External Interfaces, (Frameworks and Drivers)
This is the outermost layer in this clean architecture which changes frequently based on the technologies, updates like database, Web Front-end frameworks. In this layer, we present the data to the UI or database.

Designing and structuring a clean architecture Solution

tranhuy _ clean architecture _ Solution Designing

Domain Class Library – No dependencies, no project or class reference, no logic

Application Class Library – Only Domain is added as reference project, Pure business logic or services.

Domain and Application are the core of this solution which are independent of Infrastructure, WebUI, and external libraries.

Infrastructure Class Library – Application Class is added as reference. This class is responsible for external infrastructure communications like database storage, file system, external systems/APIs/Services and so on.

We can add more class libraries under this folder for external plugins or SDK to organize the solution in a better way.

Web UI – This is a presentation web UI project. This can be an MVC, front-end framework. If we are designing an API based solution, then we can keep both Web API and Front-end in this folder host. We add Application and Infrastructure as reference in this project.

This is a way of organizing and designing a clean architecture solution. This is one way of structuring the solution following the clean architecture principles. However, we can do the organization in several ways, keeping the core values intact.

In this illustration above, we have kept core applications independent and other infrastructure and web UI are dependent on core applications.

In the next article, I will design a real solution demonstrating the clean architecture implementation in .NET 6, stay tuned.

Tool for testing the architecture and Design

Tools to help quickly identify and remediate architecture issues
IntelliJ
Eclipse : install various plugins to visualize dependencies and to detect cycles: eDepend, STAN, jDepend, Java Dependency Viewer
ArchUnit
JQAssistant
Structure 101
Sonargraph
Lattix
Teamscale

Some more keyword about Clean Architecture

Clean Architecture
Basic Principles behind Clean Architecture
Architectural Diagram
Layers of Clean Architecture
Designing and structuring a clean architecture Solution

### Ref

https://rijsat.com/2022/02/01/what-is-clean-architecture/
https://www.c-sharpcorner.com/article/what-is-clean-architecture/

What is SOLID principles

SOLID is a mnemon­ic for five design prin­ci­ples intend­ed to make soft­ware designs more under­stand­able, flex­i­ble and maintainable.

As with every­thing in life, using these prin­ci­ples mind­less­ly can cause more harm than good. The cost of apply­ing these prin­ci­ples into a pro­gram’s archi­tec­ture might be mak­ing it more com­pli­cat­ed than it should be. I doubt that there’s a suc­cess­ful soft­ware prod­uct in which all of these prin­ci­ples are applied at the same time. Striv­ing for these prin­ci­ples is good, but always try to be prag­mat­ic and don’t take every­thing writ­ten here as dogma.

SOLID first principle: Single Responsibility Principle

A class should have just one rea­son to change.
Try to make every class respon­si­ble for a sin­gle part of the func­tion­al­i­ty pro­vid­ed by the soft­ware, and make that respon­si­bil­i­ty entire­ly encap­su­lat­ed by (you can also say hid­den with­in) the class

SOLID second principle: Open/Closed Principle

Class­es should be open for exten­sion but closed for modification.
The main idea of this prin­ci­ple is to keep exist­ing code from break­ing when you imple­ment new features.

A class is open if you can extend it, pro­duce a sub­class and do what­ev­er you want with it—add new meth­ods or fields, over­ride base behav­ior, etc. Some pro­gram­ming lan­guages let you restrict fur­ther exten­sion of a class with spe­cial key­words, such as final. After this, the class is no longer open. At the same time, the class is closed (you can also say com­plete) if it’s 100% ready to be used by other class­es—its inter­face is clear­ly defined and won’t be changed in the future.

SOLID third principle: Liskov Sub­sti­tu­tion Prin­ci­ple

When extend­ing a class, remem­ber that you should be able to pass objects of the sub­class in place of objects of the par­ent class with­out break­ing the client code.
This means that the sub­class should remain com­pat­i­ble with the behav­ior of the super­class. When over­rid­ing a method, extend the base behav­ior rather than replac­ing it with some­thing else entirely.

The sub­sti­tu­tion prin­ci­ple is a set of checks that help pre­dict whether a sub­class remains com­pat­i­ble with the code that was able to work with objects of the super­class. This con­cept is crit­i­cal when devel­op­ing libraries and frame­works because your class­es are going to be used by other peo­ple whose code you can’t direct­ly access and change.

SOLID fourth principle: Interface Segregation Principle

Clients shouldn’t be forced to depend on meth­ods they do not use.
Try to make your inter­faces nar­row enough that client class­es don’t have to imple­ment behav­iors they don’t need.

Accord­ing to the inter­face seg­re­ga­tion prin­ci­ple, you should break down “fat” inter­faces into more gran­u­lar and spe­cif­ic ones. Clients should imple­ment only those meth­ods that they real­ly need. Oth­er­wise, a change to a “fat” inter­face would break even clients that don’t use the changed methods.

Class inher­i­tance lets a class have just one super­class, but it doesn’t limit the num­ber of inter­faces that the class can imple­ment at the same time. Hence, there’s no need to cram tons of unre­lat­ed meth­ods to a sin­gle inter­face. Break it down into sev­er­al more refined inter­faces—you can imple­ment them all in a sin­gle class if need­ed. How­ev­er, some class­es may be fine with imple­ment­ing just one of them.

SOLID fifth principle: Dependency Inversion Principle

High-level class­es shouldn’t depend on low-level class­es. Both should depend on abstrac­tions. Abstrac­tions shouldn’t depend on details. Details should depend on abstractions.
Usu­al­ly when design­ing soft­ware, you can make a dis­tinc­tion between two lev­els of classes.

Low-level class­es imple­ment basic oper­a­tions such as work­ing with a disk, trans­fer­ring data over a net­work, con­nect­ing to a data­base, etc.
High-level class­es con­tain com­plex busi­ness logic that directs low-level class­es to do something.

Some­times peo­ple design low-level class­es first and only then start work­ing on high-level ones. This is very com­mon when you start devel­op­ing a pro­to­type on a new sys­tem, and you’re not even sure what’s pos­si­ble at the high­er level because low-level stuff isn’t yet imple­ment­ed or clear. With such an approach busi­ness logic class­es tend to become depen­dent on prim­i­tive low-level classes.

The depen­den­cy inver­sion prin­ci­ple sug­gests chang­ing the direc­tion of this dependency.

For starters, you need to describe inter­faces for low-level oper­a­tions that high-level class­es rely on, prefer­ably in busi­ness terms. For instance, busi­ness logic should call a method openReport(file) rather than a series of meth­ods openFile(x), readBytes(n), closeFile(x). These inter­faces count as high-level ones.
Now you can make high-level class­es depen­dent on those inter­faces, instead of on con­crete low-level class­es. This depen­den­cy will be much soft­er than the orig­i­nal one.
Once low-level class­es imple­ment these inter­faces, they become depen­dent on the busi­ness logic level, revers­ing the direc­tion of the orig­i­nal dependency.
The depen­den­cy inver­sion prin­ci­ple often goes along with the open/closed prin­ci­ple: you can extend low-level class­es to use with dif­fer­ent

REF

https://barryvanveen.nl/articles/51-8-resources-to-learn-about-solid-design-principles