Abstract
This article is the 7th of
a series of seven articles I wrote (described later on) about The Principles
of Object Oriented Design, also known as SOLID.
This post describes the Dependency Inversion Principle (DIP).
As a prerequisite, please read the
first 2 articles:
As architects and solution designers
we’d like to design and write robust & clean solutions.
We’d like our code to be stable,
maintainable, testable and scalable for as long as our product lives.
We’d like to implement and integrate
new features as smoothly as possible in our existing (already tested and
deployed) code, without any regression issues, and by maintaining dead-lines
and production deployments.
In addition, we’d like our code to
perform according to standard performance KPIs, and eventually, to serves its
purpose.
Such desire has many obstacles,
especially as our application evolves and our product become more complex.
More often than we’d like, we are
experiencing these obstacles which could be described as software design
smells.
The purpose of the SOLID principles
is to achieve these desires and to prevent software design smells.
In this article series we’ll
emphasize the importance of well-designed applications.
We’ll show how, by maintaining the
SOLID principles in our code, we’ll create better robust solutions, provide
easier code maintenance, faster bug fixing and the overall impact on the
application performance, especially while adding new features to existing
design, as the application evolves with new requirements.
The complete series:
Intro:
S.O.L.I.D
Content
- Introduction
-
Application
Layers
-
Dependency
Between Layers
-
Preventing
Dependency
- Case
Study
-
Requirements
- A
design that conforms to the DIP
-
Description
-
Important:
Additional Layers
- Implementation
1.
Books
Services Layer
2.
Books
Operations Layer
3.
Books
Validation Layer
4.
Testing
o
Mocking
using Moq
- IoC
Containers, DI Containers & Service Locators
- Summary
Introduction
The
dependency inversion principle states that:
High-level modules should not
depend on low-level modules. Both should depend
on abstractions.
Abstractions should not depend
upon details. Details should depend upon
abstractions.
Application Layers
When
we design software applications, most often we use a ‘Layering Techniques’,
in which we create different layers that provide services to other layers in
the application architecture.
A
high-level layer uses a low-level layer, which in turn, uses another lower layer,
and so on.
This
means that the upper-level layer depends on its corresponding lower-level layer,
since it uses its services.
Simply
put, we have a high-level layer A, which defines some of the
application business logic, and uses a lower-level layer B, which
exposes different services for the upper-level layer A use.
It’s
important to understand that the business logic design is implemented in
the high-level layers, whereas the lower-level layers provide the implementation
details for this purpose.
A
layer could be designed & implemented as a module, several modules, a class
or any other relevant software entity for that matter.
Dependency Between Layers
The
dependency inversion principle indicates that the upper business logic layer should
NOT depend on the lower-level layer.
Such
dependency could lead to undesired modifications in the upper business logic
layer, with every change in the low-level layer, which would have a direct
effect on the business logic implementation.
Naturally,
this could break the business logic requirements, and would eventually lead to software
design smells.
In
addition, in case we need to reuse the high-level business logic layer, a
dependency to lower-level layers could create severe issues, sometimes even
impossible to reuse, and again would lead to software design smells.
Please see the following
illustration:
Preventing Dependency
How
could we prevent the high-level layer from depending on its corresponding
lower-level layer?
Well,
the answer is by using abstraction.
When
we’ll design a layer that needs to use a lower-level layer, we could design an abstract
interface(s) that the upper-level layer would need.
The
lower-level layer would have to provide its services based on this abstract interface(s),
which was declared on the upper-level layer.
This
means that each upper-level layer would use its corresponding lower-level layer
services through those abstract interfaces, thereby preventing the
dependency on the lower-level layers.
Please see the following
illustration:
Important: Independent Packages
The
fact that the upper-level layer declares the interfaces it needs seems a bit inverted
and could sometime be confusing, since we are used to declare the interfaces in
the layer that provides the services, meaning the lower-level layer (on our
case), and not on the client (consumer) that uses those services, meaning the
upper-level layer.
Furthermore,
this design would result in a dependency of the lower-level layer on the
upper-level layer, which we should also prevent, since in case there are
several upper-level layers that consume the lower-level layer, we would have to
duplicate the interfaces, which would eventually lead to severe issues.
Thus,
it’s advisable to declare the interfaces on different independent packages,
that both the upper-level and the lower-level layers could use as required.
Object Composition
As
indicated in the ‘Application Layers’ section above, a layer could also be a class
that uses another class services, meaning the upper-level class contains a
reference to another lower-level class, and uses its services.
In
other words, we designed an Object Composition relationship between
those classes.
This
means that we should apply the Dependency Inversion Principle in such designs
as well, and use abstraction to prevent dependency to the lower-level class
reference.
We
should use dependency-injection techniques as I’ll demonstrate in the
following case study example.
Case Study
We’ll
illustrate the dependency inversion principle using a simple library-management
project.
The
library contains books that users could borrow.
Requirements:
1.
We need to create a service that exposes relevant
operations, such as: BorrowBook, ReturnBook, CreateBook, ReadBook etc.
-
This service would simulate the upper-level business
logic layer.
2.
Different clients could consume this service.
-
For example, an Angular-2 application that invokes this
services operations.
3.
We need to implement the different books operations.
-
We’ll add a detailed operations implementation
layer.
4.
We need to validate the requests arrived from the
clients.
-
We’ll add a validation layer.
Remarks:
-
There are different appropriate designs we could
consider (based on the requirements), however for simplicity and for our
illustration we’ll use the following.
-
I won’t implement the entire solution, only emphasize
the use of the ‘layering technique’ and how to conform to the dependency
inversion principle.
A design that conforms to the DIP
Description:
1. Books Services Layer
-
We added a BooksServices class that would be implemented as a
WCF, MVC or WebApi service.
-
The BooksServices
class contains 2 interface-composition objects: IBooksBorrowOperations, IBooksCRUDOperations.
-
These interfaces contain the abstraction of all the
operations that the BooksServices
class needs.
-
Meaning, classes that would implement these interfaces
could be used in the Books Services Layer.
2. Books Operations Layer
-
We added the BooksOperationsManager class that implements both the IBooksBorrowOperations and IBooksCRUDOperations interfaces.
-
In addition, the BooksOperationsManager class contains 2
interface-composition objects: IBooksBorrowOperationsValidator, IBooksCrudOperationsValidator.
3. Books Validation Layer
-
We added the BooksBorrowOperationsValidator class that implements the IBooksBorrowOperationsValidator
interface.
-
We added the BooksCrudOperationsValidator
class that implements
the IBooksCrudOperationsValidator interface.
Remark:
As
described in the ‘Important: Independent Packages’ section
above, these interfaces implementation is best practiced in a separate assembly,
however for simplicity and for our demonstration, I implemented all these
classes and interfaces in the same assembly.
Important: Additional Layers
This
design is very flexible and easy scalable, since we could use it to add
additional relevant layers as needed.
For
example, in order to build the operations’ requests we could add a Books
Adapter Layer.
In
this layer we’ll build the operations’ requests as required, which means that we’ll
extract this ‘detailed implementation’ from the Books Operation Layer to
a separated layer that could be abstracted (using interfaces) and expand as
requirements would evolved.
Similarly,
we could add additional layers, such as:
-
Books Permissions Layer – that determines who could perform
what.
-
Books DBTasks Layer – that encapsulate the DAL layer for DB operations.
-
Books Auditing Layer – that invokes all relevant auditing & logging
listeners.
-
Etc.
Implementation
1. Books Services Layer
BooksServices.cs
|
using
DependencyInversionPrinciple_Example.Documents.Books.BorrowOperations;
using
DependencyInversionPrinciple_Example.Documents.Books.CrudOperations;
using
DependencyInversionPrinciple_Example.Operations.Books;
using System;
namespace
DependencyInversionPrinciple_Example.Services.Books
{
// TODO: Need to implement as a WCF, MVC or WebApi service...
public class BooksServices
{
// Private readonly variables
private readonly IBooksBorrowOperations _booksBorrowOperations;
private readonly IBooksCRUDOperations _booksCRUDOperations;
// Constructors
public BooksServices(IBooksBorrowOperations
booksBorrowOperations = null,
IBooksCRUDOperations
booksCRUDOperations = null)
{
// Setting default IBooksBorrowOperations
// implementation, in case wasn't provided.
_booksBorrowOperations =
booksBorrowOperations ?? new BooksOperationsManager();
// Setting default IBooksCRUDOperations
// implementation, in case wasn't provided.
_booksCRUDOperations =
booksCRUDOperations ?? new BooksOperationsManager();
}
// Public service operations
public BorrowBookResponse BorrowBook(BorrowBookRequest borrowBookRequest)
{
try
{
return
_booksBorrowOperations.BorrowBook(borrowBookRequest);
}
catch (Exception exception)
{
// TODO: Log relevant message...
return new BorrowBookResponse(false, exception.Message);
}
}
public ReturnBookResponse ReturnBook(ReturnBookRequest returnBookRequest)
{
try
{
return _booksBorrowOperations.ReturnBook(returnBookRequest);
}
catch (Exception exception)
{
// TODO: Log relevant message...
return new ReturnBookResponse(false,
exception.Message);
}
}
public CreateBookResponse CreateBook(CreateBookRequest createBookRequest)
{
try
{
return
_booksCRUDOperations.CreateBook(createBookRequest);
}
catch (Exception exception)
{
// TODO: Log relevant message...
return new CreateBookResponse(false,
exception.Message);
}
}
}
}
|
Description:
-
The BooksServices
class contains the required ‘Books Operations’, such as: BorrowBook,
ReturnBook, CreateBook, ReadBook, etc.
-
In addition, the BooksServices class contains the following
interfaces which represent the abstractions of all the operations (services)
that it needs: IBooksBorrowOperations, IBooksCRUDOperations.
-
I’ve created a dependency-injection constructor that
allows users to inject the relevant interfaces as desired, which means that
we’ve eliminated any dependency to the lower-level layer that implements those
interfaces, this is also very useful for testing, using mocking.
-
The BooksServices
class’s constructor also provides a default implementation for those
interfaces.
2. Books Operations Layer
IBooksBorrowOperations.cs
|
using
DependencyInversionPrinciple_Example.Documents.Books.BorrowOperations;
namespace DependencyInversionPrinciple_Example.Operations.Books
{
public interface IBooksBorrowOperations
{
BorrowBookResponse BorrowBook(BorrowBookRequest
borrowBookRequest);
ReturnBookResponse ReturnBook(ReturnBookRequest
returnBookRequest);
}
}
|
IBooksCRUDOperations.cs
|
using
DependencyInversionPrinciple_Example.Documents.Books.CrudOperations;
namespace
DependencyInversionPrinciple_Example.Operations.Books
{
public interface IBooksCRUDOperations
{
CreateBookResponse CreateBook(CreateBookRequest
createBookRequest);
// TODO: Need to implement...
//UpdateBookResponse UpdateBook(UpdateBookRequest
updateBookRequest);
//ReadBookResponse ReadBook(ReadBookRequest readBookRequest);
//DeleteBookResponse DeleteBook(DeleteBookRequest
deleteBookRequest);
}
}
|
BooksOperationsManager.cs
|
using
DependencyInversionPrinciple_Example.Documents.Books.BorrowOperations;
using
DependencyInversionPrinciple_Example.Documents.Books.CrudOperations;
using DependencyInversionPrinciple_Example.Tasks.Books;
using
DependencyInversionPrinciple_Example.Validation.Books;
namespace
DependencyInversionPrinciple_Example.Operations.Books
{
public class BooksOperationsManager : IBooksBorrowOperations, IBooksCRUDOperations
{
// Private readonly variables
private readonly IBooksBorrowOperationsValidator _booksBorrowOperationsValidator;
private readonly IBooksCrudOperationsValidator _booksCrudOperationsValidator;
// Constructors
public BooksOperationsManager(
IBooksBorrowOperationsValidator
booksBorrowOperationsValidator = null,
IBooksCrudOperationsValidator
booksCrudOperationsValidator = null)
{
// Setting default IBooksBorrowOperationsValidator
// implementation, in case wasn't provided.
_booksBorrowOperationsValidator =
booksBorrowOperationsValidator ?? new BooksBorrowOperationsValidator();
// Setting default IBooksCrudOperationsValidator
// implementation, in case wasn't provided.
_booksCrudOperationsValidator =
booksCrudOperationsValidator ?? new BooksCrudOperationsValidator();
}
// Public borrow operations
public BorrowBookResponse BorrowBook(BorrowBookRequest borrowBookRequest)
{
// Validation
if
(!_booksBorrowOperationsValidator.ValidateBorrowBookRequest(borrowBookRequest))
{
// TODO: Log relevant message...
return new BorrowBookResponse(false, "Request is Not valid");
}
// Borrow Book Implementation
// Remark:
// We could also add
another layer to encapsulate the
// DAL layer for DB operations, using a proper
interface.
// In the current
implementation I used a concrete class, for testing only!
return new BorrowBookTask().Execute(borrowBookRequest) as BorrowBookResponse;
}
public ReturnBookResponse ReturnBook(ReturnBookRequest returnBookRequest)
{
// Validation
if(!_booksBorrowOperationsValidator.ValidateReturnBookRequest(returnBookRequest))
{
// TODO: Log
relevant message...
return new ReturnBookResponse(false, "Request is Not valid");
}
// TODO: Return Book Implementation...
return new ReturnBookResponse(true);
}
// Public CRUD operations
public CreateBookResponse CreateBook(CreateBookRequest createBookRequest)
{
// Validation
if
(!_booksCrudOperationsValidator.ValidateCreateBookRequest(createBookRequest))
{
// TODO: Log
relevant message...
return new CreateBookResponse(false, "Request is Not valid");
}
// TODO: Create Book Implementation...
return new CreateBookResponse(true);
}
}
}
|
Description:
-
The BooksOperationsManager class implements both IBooksBorrowOperations and IBooksCRUDOperations interfaces.
-
In addition, the BooksOperationsManager class contains the following
interfaces which represent the abstractions of all the operations (services)
that it needs: IBooksBorrowOperationsValidator, IBooksCrudOperationsValidator.
-
I’ve created a dependency-injection constructor that
allows users to inject the relevant interfaces as desired, which means that
we’ve eliminated any dependency to the lower-level layer that implements those
interfaces, this is also very useful for testing, using mocking.
-
The BooksOperationsManager class’s constructor also provides a default implementation
for those interfaces.
3. Books Validation Layer
IBooksBorrowOperationsValidator.cs
|
using
DependencyInversionPrinciple_Example.Documents.Books.BorrowOperations;
namespace
DependencyInversionPrinciple_Example.Validation.Books
{
public interface IBooksBorrowOperationsValidator
{
bool ValidateBorrowBookRequest(BorrowBookRequest
borrowBookRequest);
bool ValidateReturnBookRequest(ReturnBookRequest
returnBookRequest);
}
}
|
BooksBorrowOperationsValidator.cs
|
using
DependencyInversionPrinciple_Example.Documents.Books.BorrowOperations;
namespace
DependencyInversionPrinciple_Example.Validation.Books
{
public class BooksBorrowOperationsValidator : IBooksBorrowOperationsValidator
{
public bool ValidateBorrowBookRequest(BorrowBookRequest borrowBookRequest)
{
return borrowBookRequest?.Id > 0;
}
public bool ValidateReturnBookRequest(ReturnBookRequest returnBookRequest)
{
return returnBookRequest?.Id > 0;
}
}
}
|
IBooksCrudOperationsValidator.cs
|
using DependencyInversionPrinciple_Example.Documents.Books.CrudOperations;
namespace
DependencyInversionPrinciple_Example.Validation.Books
{
public interface IBooksCrudOperationsValidator
{
bool ValidateCreateBookRequest(CreateBookRequest
createBookRequest);
}
}
|
BooksCrudOperationsValidator.cs
|
using
DependencyInversionPrinciple_Example.Documents.Books.CrudOperations;
using System;
namespace
DependencyInversionPrinciple_Example.Validation.Books
{
public class BooksCrudOperationsValidator : IBooksCrudOperationsValidator
{
public bool ValidateCreateBookRequest(CreateBookRequest createBookRequest)
{
// TODO: Need to implement...
return true; // For testing
}
}
}
|
Description:
-
In this layer I’ve declared 2 classes for the validation
interfaces for the upper-level layer (Books Operations Layer).
-
Class BooksBorrowOperationsValidator that implements IBooksBorrowOperationsValidator.
-
Class BooksCrudOperationsValidator that implements IBooksCrudOperationsValidator.
4. Testing
TestBooksOperations.cs
|
using
DependencyInversionPrinciple_Example.Documents.Books.BorrowOperations;
using
DependencyInversionPrinciple_Example.Documents.Books.CrudOperations;
using
DependencyInversionPrinciple_Example.Operations.Books;
using
DependencyInversionPrinciple_Example.Services.Books;
using
DependencyInversionPrinciple_Example.Validation.Books;
using Moq;
using NUnit.Framework;
namespace DIP_Testing
{
[TestFixture]
public class TestBooksOperations
{
#region Test Methods
[Test]
public void TestBorrowBookOperation()
{
// 1. Create a Mock object.
var booksOperationsManagerMock =
Create_BooksOperationsManager_Mock();
// 2. Test the 'BorrowBook()' method using the mock objects.
var response = new BooksServices(booksOperationsManagerMock.Object)
.BorrowBook(It.IsAny<BorrowBookRequest>());
// 3. Assert
Assert.IsTrue(response.IsSuccessful);
}
#endregion Test Methods
#region Private Helper Methods
// Create an 'IBooksBorrowOperationsValidator' mock object, and
setting
// up its 'ValidateBorrowBookRequest()' method return-value to
true.
private Mock<IBooksBorrowOperationsValidator>
Create_IBooksBorrowOperationsValidator_Mock()
{
var
booksBorrowOperationsValidatorMock = new Mock<IBooksBorrowOperationsValidator>();
booksBorrowOperationsValidatorMock
.Setup(x => x.ValidateBorrowBookRequest(It.IsAny<BorrowBookRequest>()))
.Returns(true);
return
booksBorrowOperationsValidatorMock;
}
// Creating a
'BooksOperationsManager' Mock object and sending its
// constructor the 'IBooksBorrowOperationsValidator' mock
object.
private Mock<BooksOperationsManager> Create_BooksOperationsManager_Mock()
{
return
new Mock<BooksOperationsManager>(
Create_IBooksBorrowOperationsValidator_Mock().Object, null);
}
#endregion Private Helper Methods
}
}
|
Description:
-
I’ve added a simple test only to demonstrate how to
invoke this implementation using Mock Objects, and by using the benefits
of the loosed-couple code we’ve designed.
Mocking
using Moq
In this example I’ve used Moq, which
is (in short) an open source mocking-framework for testing .Net applications.
With Moq we could isolate and
decouple the tested code from its (real) dependencies, by using fake
dependencies.
This way we could easily identify the
‘problematic code’, since we are removing any other obstacle that could
otherwise interfere with the tests.
In addition, Moq also provides LINQ
capabilities which makes it easy to write and maintain.
IoC Containers, DI Containers & Service
Locators
When
discussing the DIP and dependency-injection techniques, it’s only appropriate
to mention Inversion of Control (IoC) Containers also known as Dependency
Injection (DI) Containers, and Service Locators.
In
short…
An
IoC (or DI) Container is a generic
implementation of the Dependency Inversion Principle, which describes
the architecture to decouple objects from their services (other objects they
use), in order to achieve better flexible, testable and scalable designs (as
described in this article).
The
term “inversion of control” refers to the fact that the consumed-class
is no longer responsible to instantiate its composite services, this
responsibility was provided to an external container.
The
Dependency Injection Pattern is a
private case of the IoC pattern. We could use interfaces to create abstraction
of the services we need, and use dependency-injection techniques to decuple the
class from the desired services.
The
Service Locator Pattern is also a
private case of the IoC pattern. We could use a Service Locator Object
in our implementation, which would be responsible to locate the desired service
implementation at run-time. In this pattern we don’t have to use
dependency-injection techniques, although we could integrate these 2 patterns.
There
are several IoC Containers we could use. Few of the most popular are:
Example:
I’ve
added a Ninject Module to register interfaces with their concrete
implementation.
This
means that every time we’d like to use an interface we’ll get the proper
concrete class.
BooksNinjectModule.cs
|
using
DependencyInversionPrinciple_Example.Operations.Books;
using
DependencyInversionPrinciple_Example.Validation.Books;
using Ninject.Modules;
namespace DependencyInversionPrinciple_Example.NinjectModules
{
public class BooksNinjectModule : NinjectModule
{
public override void Load()
{
Bind<IBooksBorrowOperations>().To<BooksOperationsManager>();
Bind<IBooksCRUDOperations>().To<BooksOperationsManager>();
Bind<IBooksBorrowOperationsValidator>().To<BooksBorrowOperationsValidator>();
Bind<IBooksCrudOperationsValidator>().To<BooksCrudOperationsValidator>();
}
}
}
|
I’ve
modified the BooksOperationsManager class to
use BooksNinjectModule
class to get the default implementation as was pre-registered.
Remark:
We
could implement this solution in several different ways.
For
example, to create this class without a dependency-injection constructor, and to
retrieve the proper concrete class using the Ninject StandardKernel as shown below.
Alternately,
we could also use a Service Locator implementation to auto-inject these
dependencies, which would occur on run-time. (not shown below)
Please
see the following code fragment:
BooksOperationsManager.cs
|
using
DependencyInversionPrinciple_Example.Documents.Books.BorrowOperations;
using
DependencyInversionPrinciple_Example.Documents.Books.CrudOperations;
using DependencyInversionPrinciple_Example.NinjectModules;
using
DependencyInversionPrinciple_Example.Tasks.Books;
using
DependencyInversionPrinciple_Example.Validation.Books;
using Ninject;
namespace
DependencyInversionPrinciple_Example.Operations.Books
{
public class BooksOperationsManager : IBooksBorrowOperations, IBooksCRUDOperations
{
// Private readonly variables
private readonly IBooksBorrowOperationsValidator _booksBorrowOperationsValidator;
private readonly IBooksCrudOperationsValidator _booksCrudOperationsValidator;
// Constructors
public BooksOperationsManager(
IBooksBorrowOperationsValidator
booksBorrowOperationsValidator = null,
IBooksCrudOperationsValidator
booksCrudOperationsValidator = null)
{
IKernel kernel = new StandardKernel(new BooksNinjectModule());
// Setting default IBooksBorrowOperationsValidator
// implementation, in case wasn't provided.
//_booksBorrowOperationsValidator =
//
booksBorrowOperationsValidator ?? new
BooksBorrowOperationsValidator();
_booksBorrowOperationsValidator = booksBorrowOperationsValidator ??
kernel.Get<IBooksBorrowOperationsValidator>();
// Setting default IBooksCrudOperationsValidator
// implementation, in case wasn't provided.
//_booksCrudOperationsValidator =
//
booksCrudOperationsValidator ?? new BooksCrudOperationsValidator();
_booksCrudOperationsValidator
= booksCrudOperationsValidator ??
kernel.Get<IBooksCrudOperationsValidator>();
}
. . .
}
}
|
Summary
We
managed to examine a simple case study that emphasizes the importance of the Dependency
Inversion Principle, and its effect on our codebase with respect to
software design smells.
I
would say that it’s quite easy to adhere to this principle and to use
dependency-injection techniques in our code wherever needed.
As
a best practice, we should use IoC/DI Containers along with Dependency
Injection & Service Locators implementations, in order to
achieve better robust and loosely-coupled solutions.
Furthermore,
the DIP highlights the ‘debatable’ Favor
Composition Over Inheritance key-design principle (a.k.a composite reuse principle), which (in short)
states that we should use composition in our classes instead of inheritance,
since such classes are much more flexible, scalable and tolerance to changes,
especially with abstraction and dependency-injection capabilities.
If
I were to extreme, I would strive to design every feature in our codebase to
depend upon abstraction, in order to provide a better robust solution, especially
when most of the S.O.L.I.D principles guide us to abstraction.
This
would also provide better Unit Testing support, using mocking
techniques.
However, I would stress the notion that, as with
the rest of the S.O.L.I.D principles, we should always consider different
development elements, and in a common-sense manner, and not follow any
instruction blindly, even the Object Oriented Design Principles.
Furthermore,
I would say that additional important patterns were concluded from the Dependency
Inversion Principle, that emphasized the loosely-coupled designs, patterns such
as: The Hollywood Principle (“Don’t Call Us, We’ll Call You.”), The Law of Demeter - LoD (a.k.a The Principle of Least Knowledge) and others.
So,
to conclude I would say that the DIP is very simple to use, we should examine
where to use it in our designs and is best used with the rest of the design
principles, in order to provide better maintainable, testable & scalable
solutions, and naturally to prevent software design smells.
---
This
article was the last of 7 articles describing & illustrating the Object
Oriented Design Principles, a.k.a S.O.L.I.D Principles, which
aspired to reveal the beauty & science of software design.
As
stated in the first article: SOLID– The Principles of Object Oriented Design these principles were first gathered
and presented by Robert Cecil Martin (a.k.a Uncle Bob).
Hope
you enjoyed, and more importantly apply in your designs.
The End
Hope you enjoyed!
Appreciate your
comments…
Yonatan Fedaeli
No comments:
Post a Comment