One important aspect of any programming paradigm is how it treats data.
In some interpretations of Object Oriented Programming (OOP), data is encapsulated inside objects. Such data is not manipulated/accessed directly by the clients of these objects, but instead, the objects expose some behavior that allows clients to indirectly query or manipulate the data.
This article is published from the DNC Magazine for Developers and Architects. Download or Subscribe to this Free magazine [PDF] to access all previous, current and upcoming editions.
In functional programming, data is not encapsulated or hidden inside objects. Instead, it is given as input to functions that produce other data as output.
In this sense, functions are units of behavior.
Such input data should be immutable, which means that it cannot be modified by the function acting on it. Also, having shared state that can be modified by different functions, is discouraged.
In procedural programming, we have procedures that are also units of behavior that act on data. But as opposed to functional programming, there is no emphasis on data immutability and statelessness. Procedures are free to modify both input data, as well as the state shared between procedures.
Encapsulation in large applications
In large and complex applications, the ability to change software behavior easily and without breaking existing features is not an easy task.
Encapsulation is one mechanism that we can use to enhance software maintainability. However, there is more than one kind of encapsulation and it is important to understand how it affects software maintainability.
In this article, I am going to discuss different types of data and discuss some issues we might face if we try to encapsulate runtime data in complex applications.
I will also talk about behavior-only encapsulation as an alternative to data encapsulation.
Finally, I will discuss state as a special kind of runtime data.
Types of data – Configuration and Runtime
When discussing how to deal with data, it is important to differentiate between different kinds of data.
One type of data is configuration data. Configuration data is used to customize applications.
Examples of such data are connection strings, user preferences, web service settings, etc. This data is mostly static. It is set in some configuration file, is read at application startup, and then used by the application.
One way to treat configuration data is to read it at the application startup and use it to determine which objects to create in the Composition Root. Also, we use it to inject primitive dependencies (such as connection strings) into objects.
Here is an example:
Settings settings = ReadSettings();
IProcessor processor =
new IndexingProcessor(
new Repository(settings.ConnectionString));
if (settings.DeleteFiles)
{
processor =
new CompositeProcessor(
processor,
new DeleteProcessor());
}
processor.Process();
In this example, we read the settings from the configuration file.
We also inject the connection string primitive dependency into the Repository class. In addition to this, based on the DeleteFiles setting, we decide whether to create a single IndexingProcessor or whether to create a CompositeProcessor that invokes both the IndexingProcessor and the DeleteProcessor objects.
Another type of data that is more relevant to the subject of this article is runtime data.
This is the data that the application does not know until after it completely starts. Examples of such data are user input, data queried from the database, data obtained via a web service call, data produced by processing other data, etc.
Please note that in order to change the configurations, we might need some runtime data to do this.
For example, imagine an application that allows you to change its settings. The data entered in the settings window would be runtime data. The same data is called configuration data when it is used at application startup to configure the application.
Treatment of runtime data
One way to treat runtime data is to encapsulate it inside objects.
Here is an example of runtime data that is encapsulated inside an object:
public class Document
{
private readonly string identifier;
private readonly string content;
public Document(string identifier, string content)
{
this.identifier = identifier;
this.content = content;
}
public string[] GetDistinctWords()
{
return content.Split(' ').Distinct().ToArray();
}
public void Print()
{
//...
}
public void Archive()
{
//...
}
public Document Translate(Language language)
{
//...
}
//...
}
This Document class represents a text document.
It has an identifier field to uniquely identify it, and a content field to hold its contents. The document identifier and its contents are run time data. They might have been read from the file system or the database at runtime.
The class contains four methods that allow the consumers to interact with it in meaningful ways.
The GetDistinctWords method for example is responsible for returning the list of distinct words in the document. The consumers of this class do not get to access the content field (the contents of the document) directly, but instead they use methods like GetDistinctWords to gain access to the contents indirectly.
This is data encapsulation.
Actually, this is both data and behavior encapsulation. The consumers cannot access the data directly, and also they don’t know and don’t care how the methods work internally.
The main argument for such design is that it easily allows future changes to the way the data is represented inside the object.
For example, we can change the type of the content field from a string to a string array representing the words of the document.
Or we can store a complex object graph that represents the different paragraphs of the document each with detailed information about the text like color and style, etc. We can even omit this field entirely and use the identity field to query a database or a web service every time a consumer asks the object something.
Such changes would require the implementation of the class methods (e.g. GetDistinctWords) to change, but it wouldn’t require us to change code in other classes.
This is the power of data encapsulation.
Let’s dig deeper.
Let’s first consider how a consumer of the Document class would get an instance of this class.
It wouldn’t make sense for the consumer to construct the Document class itself, because that would mean that the consumer already has access to the internal data of the document needed for the construction of the Document class, and that would break the whole data encapsulation concept.
The only way to hide these details from the consumer is to have the consumer get already built instances of the Document class. For example, a Document object might be passed as a parameter to a method in the consuming object. Or the consumer can invoke a repository/factory to create Document instances.
Consider the example of a DocumentProcessor class that processes documents. It has a dependency on a DocumentSource object that it uses to get the next set of Document objects to process, similar to this:
Document[] documents = documentSource.GetNextSetOfDocuments();
This way, the consumer (the DocumentProcessor class) does not need to know about the internal structure of the Document class. We moved such knowledge to the DocumentSource class.
The DocumentSource class would for example, connect to some database, execute some database query, create instances of the Document class, and return them.
public class DocumentSource
{
private readonly int documentsToTakeAtOnce;
public DocumentSource(int documentsToTakeAtOnce)
{
this.documentsToTakeAtOnce = documentsToTakeAtOnce;
}
public Document[] GetNextSetOfDocuments()
{
using (var context = new DatabaseContext())
{
return
context.Documents
.Where(x => !x.Processed)
.Take(documentsToTakeAtOnce)
.AsEnumerable()
.Select(x => new Document(x.Identifier, x.Content))
.ToArray();
}
}
}
Here is how the composition code looks in the Composition Root:
var documentProcessor =
new DocumentProcessor(
new DocumentSource(documentsToTakeAtOnce: 100));
Now let’s consider how we can modify the behavior of the Document class.
Let’s first classify the behavior changes that we might want to make, into three categories:
1. Changes to handle cross cutting concerns such as performance monitoring, logging, error handling, etc.
2. Changes to the internals of existing behavior in the Document class. For example, changing the way the print functionality works or adding new supported languages for the translate functionality or changing the way we archive documents.
3. Changes to add new behavior to the Document class. For example, we might want to add a method to convert the document into a different format.
Changes to handle cross cutting concerns
Let’s start by considering how we can handle cross cutting concerns in the Document class.
Let’s say that we would like to record the time it takes to extract the distinct words of the document, that is, the time it takes for the GetDistinctWords() method to complete.
Our first option is to add the measurement code inside the GetDistinctWords() method in the Document class. This however will make the code in the method less readable, so we should avoid it.
It is very important to avoid adding such code to existing methods. Adding such code to existing methods will make the code in the methods very long, tangled, and thus hard to maintain.
Also, hardcoding such code into existing methods will make it hard to turn it on or off selectively. For example, what if we want to record performance only for some of the Document objects based on the source of such documents (e.g. database1 or database2)?
A better approach is to use a decorator.
To enable decoration, we need to create an interface for the Document class and make the DocumentSource.GetNextSetOfDocuments() method return an array of IDocument instead of Document.
public IDocument[] GetNextSetOfDocuments()
{
//..
}
Next, we need to create a decorator for IDocument to record the time like this:
public class PerformanceRecordingDocumentDecorator : IDocument
{
private readonly IDocument document;
public PerformanceRecordingDocumentDecorator(IDocument document)
{
this.document = document;
}
public string[] GetDistinctWords()
{
Stopwatch sw = Stopwatch.StartNew();
var distinctWords = document.GetDistinctWords();
RecordTime(sw.Elapsed);
return distinctWords;
}
private void RecordTime(TimeSpan elapsed)
{
//..
}
//..
}
Next, in the factory (the DocumentSource class), we decorate all Document objects using this decorator before we return them like this:
public IDocument[] GetNextSetOfDocuments()
{
using (var context = new DatabaseContext())
{
return
context.Documents
.Where(x => !x.Processed)
.Take(documentsToTakeAtOnce)
.AsEnumerable()
.Select(x =>
new PerformanceRecordingDocumentDecorator(
new Document(x.Identifier, x.Content)))
.Cast< IDocument >()
.ToArray();
}
}
If we add more decorators to the Document object, the factory would look like this:
public IDocument[] GetNextSetOfDocuments()
{
using (var context = new DatabaseContext())
{
return
context.Documents
.Where(x => !x.Processed)
.Take(documentsToTakeAtOnce)
.AsEnumerable()
.Select(x =>
new YetAnotherDocumentDecorator(
new AnotherDocumentDecorator(
new PerformanceRecordingDocumentDecorator(
new Document(x.Identifier, x.Content)))))
.Cast< IDocument >()
.ToArray();
}
}
The problem with this factory code is that code responsible for reading documents from the database, is tangled with code responsible of installing the right decorators on the read documents.
Also, we might want to decorate the document objects differently in different cases. For example, we might want to log actions on documents obtained from database 1, but not documents obtained from database 2.
Since we return IDocument instances instead of concrete Document instances, we can use any of the good things we get from programming to an interface to fix this problem.
For example, we can move the decoration logic of Document objects into a dependency like this:
public class DocumentSource : IDocumentSource
{
private readonly IDocumentDecorationApplier documentDecorationApplier;
//..
public IDocument[] GetNextSetOfDocuments()
{
using (var context = new DatabaseContext())
{
return
context.Documents
.Where(x => !x.Processed)
.Take(documentsToTakeAtOnce)
.AsEnumerable()
.Select(x =>
documentDecorationApplier
.ApplyDecoration(
new Document(x.Identifier, x.Content)))
.ToArray();
}
}
}
public interface IDocumentDecorationApplier
{
IDocument ApplyDecoration(IDocument document);
}
public class DocumentDecorationApplier : IDocumentDecorationApplier
{
public IDocument ApplyDecoration(IDocument document)
{
return
new YetAnotherDocumentDecorator(
new AnotherDocumentDecorator(
new PerformanceRecordingDocumentDecorator(document)));
}
}
The DocumentSource now creates the Document objects and gives them to the IDocumentDecorationApplier dependency to apply any decorations. This would now allow us to create a DocumentSource object that applies some decorations to Document objects, as well as another DocumentSource object that applies different decorations by injecting a different implementation of the IDocumentDecorationApplier interface.
To make this even more flexible, we can make the DocumentDecorationApplier accept a Func< IDocument,IDocument > in the constructor so that we can easily configure decoration from the Composition Root like this:
public class DocumentDecorationApplier : IDocumentDecorationApplier
{
private readonly Func<IDocument, IDocument> decorationApplierFunction;
//..
public IDocument ApplyDecoration(IDocument document)
{
return decorationApplierFunction(document);
}
}
var documentProcessor1 =
new DocumentProcessor(
new DocumentSource(
new DocumentDecorationApplier(
document =>
new YetAnotherDocumentDecorator(
new AnotherDocumentDecorator(document))),
documentsToTakeAtOnce: 100,
connectionString: connectionString1));
var documentProcessor2 =
new DocumentProcessor(
new DocumentSource(
new DocumentDecorationApplier(
document =>
new AnotherDocumentDecorator(
new PerformanceRecordingDocumentDecorator(document))),
documentsToTakeAtOnce: 100,
connectionString: connectionString2));
So far, the cost of data encapsulation seems reasonable, let’s dig deeper.
Changes to modify/vary existing behavior
What if there are three different ways to print a document? Five different ways to translate a document? Two different ways to archive a document?
Different consumers of the Document class might want to print, translate, or archive the documents in different ways.
How can we configure the Document class to behave differently?
One way is to create a new Document class for each permutation a consumer might want. If there are 10 consumers who want different configurations of the Document class, then we need to create 10 Document classes.
A lot of the code in these classes would be duplicate.
One way to fix this problem is to create a single Document class and make it configurable by using constructor parameters like this:
public enum PrintAppoach {/*..*/}
public enum TranslateAppoach {/*..*/}
public enum ArchiveAppoach {/*..*/}
public class Document : IDocument
{
public Document(
string identifier, string content,
PrintAppoach printAppoach, TranslateAppoach translateAppoach, ArchiveAppoach archiveApproach)
{
//..
}
}
Internally, for each behavior (e.g. printing), the Document class would have a switch/if statement that will decide how to act based on the setting.
For example:
public void Print()
{
if (printAppoach == PrintAppoach.Approach1)
{
//..
}
else if (printAppoach == PrintAppoach.Approach2)
{
//..
}
}
One issue with this approach is that the Document class will get very big.
Another issue is that we need to have similar parameters in the constructor of the factory (e.g. the DocumentSource class) because it will need them when constructing Document objects.
What if we want to configure the Document object to try many different translation approaches before it fails? What if we want to control the order in which these different translation approaches execute?
What kind of complex parameters can we use to configure the Document class with all these different nuances? How complex is the Document class and its factory object going to get?
An alternative approach to solving the problem of many Document classes is to delegate each behavior to some interface. We will have a single Document class that looks like this:
public class Document : IDocument
{
private readonly string identifier;
private readonly string content;
private readonly IPrinter printer;
private readonly ITranslator translator;
private readonly IArchiver archiver;
public Document(string identifier, string content, IPrinter printer, ITranslator translator, IArchiver archiver)
{
this.identifier = identifier;
this.content = content;
this.printer = printer;
this.translator = translator;
this.archiver = archiver;
}
public void Print()
{
printer.Print(content);
}
public IDocument Translate(Language language)
{
var translatedContent = translator.Translate(content, language);
//..
}
public void Archive()
{
archiver.Archive(identifier, content);
//..
}
}
Now, we can use all the good things we get from programming to an interface, to inject whatever translation strategy (for example) into the Document object, with any nuances that we need (e.g. retrying with different translation strategies in easily configurable order).
Although this would result in a single Document class, we have introduced more problems.
First, one could argue that data encapsulation, which is what we are going out of our way to preserve, is broken.
The IPrinter service for example, takes a string representing the document content as a parameter. What if we changed the internal representation of the data to represent a sophisticated document that understands different fonts, different colors, etc? We would need to change the IPrinter interface to meet this new data representation.
Another problem is that these dependencies (IPrinter, ITranslator, IArchiver) are dependencies that need to be managed by the factory object (the DocumentSource) as well. They need to be injected into the factory object, just to allow the factory object to inject them into the Document objects it create.
Changes to add new behavior
As the software grows, new behavior will be added to the Document class. For example, we might add a method to convert the document to a different format.
To preserve data encapsulation, such methods need to exist in the Document class itself and cannot be moved to other classes.
The consequence of this is that if we need to decorate one method, say the GetDistinctWords() method, then our decorators will also need to implement all the other methods (which would simply delegate to the decorated instance) just to satisfy the interface.
Any time we add a new behavior to the Document class, all the decorators break and we need to go visit all the decorators and implement the new method.
The problem of complex object construction
There is yet another problem, now inside the Translate() method.
Regardless of how we translate the content, we need to create a new Document object to encapsulate the result of the translation. We cannot simply create and return a new Document object because we need to account for the many possible decorators that were used to decorate the current Document object (or maybe even different ones).
One way to fix this is to inject an IDocumentDecorationApplier dependency into the Document class and making sure that we use it to apply the decorators any time we create a new Document object:
public class Document : IDocument
{
private readonly IDocumentDecorationApplier documentDecorationApplier;
//..
public Document(
string identifier, string content,
IPrinter printer, ITranslator translator, IArchiver archiver,
IDocumentDecorationApplier documentDecorationApplier)
{
//..
this.documentDecorationApplier = documentDecorationApplier;
}
//..
public IDocument Translate(Language language)
{
string translatedContent = ...;
var document = new Document(identifier, translatedContent, printer, translator, archiver, documentDecorationApplier);
return documentDecorationApplier.ApplyDecoration(document);
}//..
}
When the DocumentSource object creates the Document objects, it would inject the IDocumentDecorationApplier dependency into these objects.
In some sense, the Document class is now a factory of Document objects. It has the ability to create new Document objects, and thus it needs to be complex enough to do this.
This becomes even more complex when the Document class needs to construct other types of objects. For example, a Paragraph object, a Header object, etc. We would need to inject into the Document class (and its factory) some dependencies to help with the construction of such objects.
Another approach to treating run time data: Behavior-only Encapsulation
In the previous section, we explored the option of encapsulating runtime data. The advantage of such an approach is that we get to hide the internal representation of the data from consumers and therefore we get to change such representations easily.
However, there is a price that we pay for such benefits.
Another approach to treat runtime data is to separate it from behavior.
In this approach, data passes through units of behavior (e.g. functions or procedures) which can access the internals of such data easily.
For example, we can create a translation unit of behavior that has the following interface:
public interface IDocumentTranslator
{
Document Translate(Document document, Language language);
}
The Document object in this case is a simple data object. Here is how it looks like:
public class Document
{
public string Identifier { get; }
public string Content { get; }
public Document(string identifier, string content)
{
Identifier = identifier;
Content = content;
}
}
An implementation of the IDocumentTranslator interface can simply access the content of the Document object as it wishes.
It is recommended however, that our data objects remain immutable. That is, once a data object is constructed, we cannot modify its contents. If we need to modify a Document, we would need to create another Document object and pass to its constructor, a modified version of the content.
When data objects are immutable, the code becomes easier to reason about.
We are still encapsulating. But instead of encapsulating data, we encapsulate behavior. What this means is that the consumer of the translation unit does not know how the translation functionality is implemented.
This is met by having consumers depend on interfaces instead of concrete types. The implementations of the interfaces are hidden from the consumer (and hence encapsulated).
Composition in this approach is much easier, we wouldn’t need complex factories like the ones we saw earlier.
This is true because all runtime data exist in data objects that don’t contain any behavior and thus they don’t need the complex customization/configuration needed when encapsulating data.
Now, we can simply use the C# “new” keyword to create any data objects.
Also, because units of behavior don’t encapsulate any runtime data, everything about them is known at application startup. Therefore, the construction/configuration/customization of units of behavior (objects that contain behavior) can be done immediately at application startup in the Composition Root.
For a complete example of this alternative approach, see the Object Composition with SOLID article.
A special kind of runtime data: State
A special kind of runtime data is state.
State is the data whose value changes from time to time.
For example, consider the case where we need to count the number of documents processed over the lifetime of an application for statistical reasons. The number of documents represents runtime data that starts with the value 0 when we run the application, and increases as the application processes more documents.
State can have different scopes.
Methods can define local variables and change the value of these variables multiple times before they complete. These variables hold state, but this state has a lifetime that starts when the method is invoked, and ends when it completes.
Therefore, it’s easy to deal with this kind of state.
The number of processed documents example, is an example of state that has a larger scope. The lifetime of such a state is the lifetime of the application. It is this kind of state that requires special handling.
We cannot simply pass this kind of state through method parameters.
One reason is that the lifetime of this kind of a state is larger than the lifetime of a single method invocation or a chain of multiple method invocations.
How can we deal with this kind of state?
Some procedural programming
In procedural programming, we can define a global variable to hold some state (e.g. number of processed documents), and procedures are free to read or update such state.
This approach however has some problems.
1. Having a global variable that is modified from many places (e.g. procedures) means that a single procedure cannot be reasoned about, without understanding how other procedures modify/read this shared state.
2. Using a global variable directly means that the connection between the procedures that share the same state is not obvious.
We cannot do a lot to fix problem one. Sometimes we need state that different parts of the system need to share.
However, we can mitigate the second problem by making the dependency between the shared state and the parts of the system that access it more explicit.
Controlling shared state: The State Holder pattern
We can create an “object” whose sole responsibility is to hold some state. Here is an example of such an object:
public class StateHolder<T> : IStateHolder<T>
{
private T state;
public void SetValue(T value) => state = value;
public T GetValue() => state;
}
public interface IStateGetter<T>
{
T GetValue();
}
public interface IStateSetter<T>
{
void SetValue(T value);
}
public interface IStateHolder<T> : IStateGetter<T>, IStateSetter<T>
{
}
Any unit that requires to read/update the state will have a dependency on IStateGetter, IStateSetter, or IStateHolder depending on the type of access it requires.
For example, the following DocumentProcessor class has read/write access to a state holder object that is used to store the number of processed documents:
public class DocumentProcessor
{
private readonly IStateHolder<int> numberOfDocumentsStateHolder;
//..
private void ProcessDocuments(Document[] documents)
{
//..
numberOfDocumentsStateHolder.SetValue(numberOfDocumentsStateHolder.GetValue() + documents.Length);
}
}
..and this StatisticsService class has only read access to such state:
public class StatisticsService
{
private readonly IStateGetter<int> numberOfDocumentsStateGetter;
//..
public Report GenerateReport()
{
var numberOfDocuments = numberOfDocumentsStateGetter.GetValue();
//..
}
}
This statistics service could for example be called via WCF from a monitoring web site to monitor the health of a document processing application.
Here is how we compose these objects in the Composition Root:
var numberOfProcessedDocumentsStateHolder =
new StateHolder<int>();
//..
var documentProcessor =
new DocumentProcessor(numberOfProcessedDocumentsStateHolder);
//..
var statisticsService =
new StatisticsService(numberOfProcessedDocumentsStateHolder);
This will allow us to easily track which units of behavior have which type of access to such a larger-scope state.
For more information about creating Composition Roots that allow developers to easily navigate and understand the structure of their applications, see the Clean Composition Roots with Pure Dependency Injection article.
Another example of state
Let’s consider another example of state which has a scope larger than a single method, but smaller than the lifetime of the application or even the lifetime of a single request. A request in this context could mean a WCF/Web API request, but it could also mean a set of actions triggered by some event. E.g. a timer interval elapsing, the user interacting with the application, etc.
Imagine an application that translates documents.
A single request in this application starts with a file. A Document object is created from this file and then the Document is parsed into distinct paragraphs. Each paragraph is then processed by invoking some translation service.
The filename is the first piece of runtime data that we encounter in this request. The FileProcessor unit receives this data and uses it to read the file from the file system, create a Document object, and then pass it to the DocumentProcessor.
The Document object that the DocumentProcessor receives is also runtime data. The DocumentProcessor parses the Document into multiple paragraphs and for each paragraph it invokes the ParagraphProcessor unit to process it.
The ParagraphProcessor would then extract the text out of the paragraph and invoke a TranslationService to translate it to some other language.
In real applications, the process will be more complex, but I use this simple example to explain the idea of this section.
Now, let’s say that a new requirement asks that we log the time a translation request is sent to the translation service, along with the name of the file.
Now, these two pieces of information can be obtained from two different places in code. The time of the request is known by the ParagraphProcessor because it is the one that invokes the translation service. And the name of the file is known only to the FileProcessor.
One solution is to change the signatures of DocumentProcessor.Process and ParagraphProcessor.Process to add a filename argument like this:
This solution introduces no state. It simply sends the name of the file to two additional units so that the ParagraphProcessor has access to the filename.
To understand the problem with this approach, imagine that in the near future, we are asked to process documents that are read from the database, not from the file system.
We should be able to reuse the DocumentProcessor and the ParagraphProcessor classes. By including the filename as an argument, we are explicitly saying that these two classes can only be used to process Documents/Paragraphs that are originally read from a file and therefore cannot be used by Documents/Paragraphs that originally come from someplace else.
If we insist to use them, then what should we pass for the filename parameter in case the Document comes from the database? The row id?
The problem with this approach is that the abstractions implemented by the DocumentProcessor class and the ParagraphProcessor class become leaky abstractions. Since we are using dependency injection, the FileProcessor class actually depends on IDocumentProcessor, not DocumentProcessor. Having IDocumentProcessor with a method that has a filename parameter means that this abstraction (IDocumentProcessor) leaks an implementation detail (that the implementation of this interface works with Documents that originate from files).
To help us solve this problem, we can introduce state.
We can create a decorator of IFileProcessor that stores the filename into a state holder. We also create a decorator of ITranslationService that will read the filename from the state holder and log the time and the filename.
Here is how this graph can be composed in the Composition Root:
var currentFilenameStateHolder = new StateHolder<string>();
var fileProcessor =
new FileProcessorDecorator(
currentFilenameStateHolder,
new FileProcessor(
new DocumentProcessor(
new ParagraphProcessor(
new TranslationServiceDecorator(
currentFilenameStateHolder,
new Logger(),
new TranslationServiceProxy(webServiceUrl))))));
Conclusion:
In this article, I have discussed different types of data and discussed different ways to deal with runtime data. I have shown that although encapsulating runtime data makes it easier to later change the internal representation of data, there is a cost associated with it.
In many cases, a better approach to handle runtime data is to pass it as arguments to units of behavior.
A special kind of runtime data is state.
State cannot be simply passed through method arguments because its lifetime is larger than a single method or even a single request. To store state, we can use the State Holder pattern as a replacement of using global variables. The difference is that with the State Holder pattern, the dependency between the state and the units in the system that access it becomes explicit and therefore it becomes easier to understand such units.
This article was technically reviewed by Damir Arh.
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!
Was this article worth reading? Share it with fellow developers too. Thanks!
Yacoub Massad is a software developer and works mainly on Microsoft technologies. Currently, he works at Zeva International where he uses C#, .NET, and other technologies to create eDiscovery solutions. He is interested in learning and writing about software design principles that aim at creating maintainable software. You can view his blog posts at
criticalsoftwareblog.com. He is also the creator of DIVEX(
https://divex.dev), a dependency injection tool that allows you to compose objects and functions in C# in a way that makes your code more maintainable. Recently he started a
YouTube channel about Roslyn, the .NET compiler.