Click here to Skip to main content
15,884,237 members
Articles / Programming Languages / C#

Concern Oriented Programming (COP)

Rate me:
Please Sign up or sign in to vote.
5.00/5 (6 votes)
30 Jul 2018CPOL12 min read 13.7K   15   12
New programming paradigm involving combining various small pieces of functionality together

Introduction

Imagine that you have a number of software building blocks and you can put those blocks together with minimal or no customization and they will work. This is an aim that every software package aims to solve but very rarely if ever does and this is what Concern Oriented Programming does.

I came up with ideas of Concern Oriented Programming many years ago and was using its ideas to build software, but since no language supports those ideas natively, I had to deal with some plumbing overhead.

Then I built Roxy (an IoC container-like project still very much in progress) though it is taking time to figure out the ways to optimize its API and simplify its usage. I think the current version or Roxy API is much improved and simplified, but I still want to add a lot of new functionality, some of which will be described below.

Illustration of the Main Idea of Concern Oriented Programming

In order to explain the main COP idea, I'd like to resort to a hardware analogy that should be familiar to most of the software engineers - inserting hardware cards into the computer:

Image 1

One can insert or remove or replace hardware cards into the computer adding some new capabilities to it.

Some cards (e.g. network card) come with some extra slots that expand the computer's external interface:

Image 2

Other cards (e.g. a memory card) might only have implementation without modifying the computer's external interface.

The software is more flexible and easier to change than the hardware, so imagine that we can split the external interface from the card's implementation and add them separately. With such extra flexibility, we can even replace the implementation while keeping the external interface the same.

Moreover, imagine that each hardware card has small slots for smaller cards which can also be added and replaced within the card, and so on.

This is what COP is about - only cards in COP are called 'Plugins'.

The software component in COP is represented as consisting of various plugins - some plugins implement some external interface and some plugins contribute only to the component's implementation. The plugins in turn consist of other (smaller) plugins and so on and so forth.

Plugins are easy to add, replace and remove. Once they are added, they can find what to do and how to interact with other plugins within the component with minimal or no extra information.

Important Note

Roxy has undergone important changes recently so, some of the samples in the previous Roxy related articles might no longer work. Here is the list of the articles that contain outdated code - though most of the concepts described in those articles are still perfectly valid:

  1. Introducing Roxy: Powerful Proxy Generation and IoC Container Package
  2. Separation of Concerns and Smart Mixins with the help of Roxy IoC Container and Code Generator
  3. Roxy: Strong Types and Method Overloading
  4. New Roxy Features allowing to Choose Interface Implementation Automatically

 

Downloading Roxy Code and Samples

You can download Roxy code and the samples by using the following git command:

git clone https://github.com/npolyak/NP.Roxy.git --recursive -b master NP.Roxy  

or simply by downloading the zipped archive via Github's Clone or download button:

Image 3

Note, that if you use the button, you will also have to download 3 more reporsitories

  1. NP.Utilities
  2. NP.Concepts
  3. NP.IoCy

and place the code in the corresponding folders under NP.Roxy. The clone command, however, downloads everything at once.

 

Code Structure

Those who looked at my previous articles might notice that the code structure has changed. There are now three projects that NP.Roxy is dependent on:

  1. NP.Utilities - small and basic utility methods project
  2. NP.Concepts - contains some more complex, but still very generic concepts and behaviors
  3. NP.IoCy - IoC container described in Introducing IoCy - Simple and Powerful IoC Container. I plan to integrate IoCy and Roxy in the near future.

There is a TEST folder within the solution as before, but now it contains XUnit subfolder and most of the tests are consolidated there and have been rewritten in XUnit. In this article, I will describe some tests under NP.XUnitAttrRoxyTests project contained within SeparationOfConcerns project folder.

Important Note: The tests within the folder should not be run together as it may cause some name clashes - they should be run one by one.

Note on Changes to Roxy API

As will be seen in a moment, Roxy API changed. In particular, in order to simplify constructing the plugin containers, most of the work is now achieved via attributes to an interface or class called 'Implementor' - in the previous versions of Roxy, this class was named 'Wrapper' and most of the customization was done in C# code.

Running XUnit Samples

In order to run XUnit samples within Visual Studio, you need to install the following xunit.runner.visualstudio nuget package. You can install it only for one project and after that, you'll be able to debug XUnit for any project on the same computer. The tests are easiest to run and debug from the Test Explorer window within VS 2017. You can open Test Explorer by choosing TEST->Windows->Test Explorer menu item.

Plan for the Remainder of the Article

In order to demonstrate Roxy and its concepts, I'll start with a very simple sample.

After that, I'll introduce the pseudo code notations that will show a way how to make COP functionality become part of any language (including C#).

Finally, I'll explain more examples that will demonstrate the power of COP - each example will be followed by detailed explanations and pseudo code.

The Person Test Example

The Simplest Example

The simplest XUnit example is within PersonTest.cs file inside SeparationOfConcerns folder within NP.XUnitAttrRoxyTest project:

Image 4

In order to run it, you can open Test Explorer and navigate to the test:

Image 5

Right mouse click on the test and choose "Run Selected Tests".

Once the test is successfully completed, click on the Output link in the panel below the test tree to check what was the console output of the test, it should be "The Person Bruce is walking":

Image 6

Let us take a look at the code within PersonTest.cs file. At the top of the file, IPerson interface and Person implementation class are defined:

C#
public interface IPerson
{
    string Name { get; set; }

    void Walk();
}

public class Person
{
    public string Name { get; set; }

    public void Walk()
    {
        Console.WriteLine($"The Person {Name} is walking");
    }
}

Note that Person implementation class does not have to implement the IPerson interface.

Next, within the Test class, we define PersonImplementor class:

C#
// generate class called PersonImplementation
[ImplementationClassName("PersonImplementation")]
public abstract class PersonImplementor
{
    //[PullMember(WrappedMemberName = "PersonName", WrapperMemberName = "Name")]
    [Plugin]
    protected abstract Person ThePersonPlugin { get; set; }

    //public void Walk()
    //{
    //    Console.WriteLine($"Overridden The Person {ThePersonPlugin.Name} is walking");
    //}
}

This class contains all information for Roxy about how to generate its implementation class. This is the major difference from the older versions of Roxy - the older versions had a Wrapper class to wrap the plugins, but they also required some bulky C# functionality specifying how the plugins fit into the container. New Roxy provides all such information via C# Attributes.

Parts of the class are commented out - I will show how they can be used for customisation below.

The only uncommented member of the class is ThePersonPlugin property - defines the plugin for implementing IPerson interface - remember that picture of inserting a card inside a computer?

Now, let us take a look at the method that runs the test:

C#
[Fact]
public void RunPersonTest()
{
    // make Roxy save the generated code under
    // <executable>/GeneratedCode folder
    // in case of Roxy error
    Core.SetSaveOnErrorPath("GeneratedCode");

    // make roxy generate IPerson implementation
    // based on PersonImplementor
    IPerson person =
        Core.CreateImplementedInstance<IPerson, PersonImplementor>();

    // set name to Bruce on the object
    // of the Roxy generated class
    person.Name = "Bruce";

    // call method work on the object
    // of the Roxy generated class
    person.Walk();

    // make Roxy save the generated code under
    // <executable>/GeneratedCode folder
    // in case of successful completion
    Core.Save("GeneratedCode");
} 

The most important method is Core.CreateImplementedInstance(...). It generates the Roxy class to implement IPerson interface based on PersonImplementation Implementor and returns an object of that class.

We set the object's Name property to "Bruce" and call its Walk() method - which is essentially just a wrapper around Person plugin's Walk() method.

Core.Save("GeneratedCode") writes all the generated classes into the same named files under GeneratedCode folder. This folder path is taken with respect of the executable. Let us take a look at the generated PersonImplementation.cs file.

The class has a reference to the NP.Roxy.Core object at the very top:

C#
public static Core TheCore { get; set; }  

Then, it has a region Plugin containing the definitions of Plugin properties - in our case, it is just one property ThePersonPlugin:

C#
#region Plugins
private bool _isSharedFromExternalSource_ThePersonPlugin;
private Person _thePersonPlugin;
protected override Person ThePersonPlugin
{
    get
    {
        return _thePersonPlugin;
    }
    set
    {
        if (_thePersonPlugin == value)
        {
            return;
        }
        if ((ThePersonPlugin != null) && (!_isSharedFromExternalSource_ThePersonPlugin))
        {
            ThePersonPlugin.Name = default(string);
        }

        _thePersonPlugin = value;

        if ((ThePersonPlugin != null) && (!_isSharedFromExternalSource_ThePersonPlugin))
        {
            ThePersonPlugin.Name = Name;
        }
    }
}
#endregion Plugins  

Note that each plugin has a corresponding _isShared... field that specifies if it is Shared or not. The full name of such field for our ThePersonPlugin is _isSharedFromExternalSource_ThePersonPlugin. 'Shared' means that the plugin is defined at a higher level and should not be updated at its own level (but still can be used for reading its values). I'll talk more about the Shared plugins later in this article when it comes to 'diamond' inheritance.

There are two constructors defined:

C#
#region Constructor
public PersonImplementation ()
{
    ThePersonPlugin = new Person();
}
#endregion Constructor

#region Plugins Constructor
public PersonImplementation(Person person)
{
    if (person == null)
    {
        ThePersonPlugin = new Person();

    }
    else
    {
        _isSharedFromExternalSource_ThePersonPlugin = true;
        this.ThePersonPlugin = person;
    }
}

#endregion Plugins Constructor  

First is the default constructor that sets the plugin property to new Person(). Second constructor allows to pass the plugin object and if the passed plugin is non-null, it declares it 'Shared'.

Finally, the class contains the generated wrappers around the plugins' properties and methods - in our case, it is property Name and method Walk():

C#
#region Generated Properties
public string Name
{
    get
    {
        if (!_isSharedFromExternalSource_ThePersonPlugin)
        {
            return ThePersonPlugin.Name;
        }
        else
        {
            return default(string);
        }
    }
    set
    {
        if (!_isSharedFromExternalSource_ThePersonPlugin)
        {
            ThePersonPlugin.Name = value;
        }
    }
}
#endregion Generated Properties

#region Wrapped Methods
public void Walk()
{
    if (!_isSharedFromExternalSource_ThePersonPlugin)
    {
        ThePersonPlugin.Walk();
    }
}

#endregion Wrapped Methods  

Customizing Person Test

Here, I show the power of Roxy by adding some customization to the Person test. First of all, let us uncomment the Walk() implementation within PersonImplementor class:

C#
// generate class called PersonImplementation
[ImplementationClassName("PersonImplementation")]
public abstract class PersonImplementor
{
    //[PullMember(WrappedMemberName = "PersonName", WrapperMemberName = "Name")]
    [Plugin]
    protected abstract Person ThePersonPlugin { get; set; }

    public void Walk()
    {
        Console.WriteLine($"Overridden The Person {ThePersonPlugin.Name} is walking");
    }
}

Now, we can easily test that the Walk() method implementation within PersonImplementor overrode the Walk() method within the plugin and after we run the test, the output will start with the word "Overridden": "Overridden The Person Bruce is walking". Note that the PersonImplementor.Walk() method is perfectly strongly types - its errors will be reported at compilation time.

The second modification shows how easy it is to do a name change using Roxy. Comment the Walk() method within PersonImplementor class back (we do not need it any more).

Change the property name within Person class to PersonName (do not rename the Name property within the IPerson interface though). Now Person class cannot truly implement IPerson interface.

Uncomment [PullMember(...)] attribute above the plugin definition within PersonImplementor class:

C#
// generate class called PersonImplementation
[ImplementationClassName("PersonImplementation")]
public abstract class PersonImplementor
{
    [PullMember(WrappedMemberName = "PersonName", WrapperMemberName = "Name")]
    [Plugin]
    protected abstract Person ThePersonPlugin { get; set; }

    //public void Walk()
    //{
    //    Console.WriteLine($"Overridden The Person {ThePersonPlugin.Name} is walking");
    //}
}  

PullMemberAttribute maps the plugin class's property name 'PersonName' into the name that should be implemented. (It can also change accessibility of the plugin and customize some other parameters - but such customization is beyond the scope of this article).

You can now run the test and it will still print the same message to the console: "The Person Bruce is walking".

Pseudo Code Notations for PersonTest Sample

At this point, I implement the Concern Oriented Programming concepts by using a a code generation Container (akin to an IoC container) that returns objects after being passed some configuration information (in our case, it is the Implementor type).

In the future, I hope that Concern Oriented concepts will be implemented natively to each language. Here, I describe the way I propose to implement it. Below is the pseudo code for our PersonImplementation class in C# style:

C#
public class PersonImplementation : 
 implements IPerson,
 implemented by Person as ThePersonPlugin
{

}

Here is the implementation for renaming Person's PersonName property to IPerson's Name property:

C#
  public class PersonImplementation : 
     implements IPerson,
     implemented by Person as ThePersonPlugin
{
   public string Name => ThePersonPlugin.Name
}  

Plugin with Abstract Method Sample

Our next sample shows how an abstract method from one plugin can be implemented by another plugin.

Take a look at NP.XUnitAttrRoxyTests.PersonWithAbstractMethodTest.Test.RunPersonTest within Test Explorer or at PersonWithAbstractMethodTest.cs file within the same project as the previous test.

The file defines interface IPerson and class Person very similar to those of the previous sample. The only different now is that Person has an abstract method Log(string message) and this method is called within its Walk() implementation to specify the entry and exit points:

C#
public abstract class Person
{
    public string Name { get; set; }

    protected abstract void Log(string message);

    public void Walk()
    {
        Log("Entering Walk");
        Console.WriteLine($"The Person {Name} is walking");
        Log("Exiting Walk");
    }
} 

We also define interface ILog containing Log(...) method and its two implementations ConsoleLogger and AnotherConsoleLogger:

C#
public interface ILog
{
    void Log(string message);
}

public class ConsoleLogger : ILog
{
    public void Log(string message)
    {
        Console.WriteLine($"----------{message}----------");
    }
}

public class AnotherConsoleLogger : ILog
{
    public void Log(string message)
    {
        Console.WriteLine($"**********{message}**********");
    }
}  

The difference between the two console loggers is only in the character they use to frame the message.

Here is the Implementor interface for our task:

C#
[ImplementationClassName("PersonImplementation")]
public interface IPersonImplementor
{
    [Plugin]
    Person ThePerson { get;  }

    //[Plugin(InitType = typeof(AnotherConsoleLogger))]
    [Plugin(InitType = typeof(ConsoleLogger))]
    ILog TheLog { get; }
}  

Note that in order to increase the flexibility and make it easier to swap a Logger, we define TheLog plugin as ILog type and provide the initialization type as an argument to PluginAttribute.

Here is the code of the test:

C#
[Fact]
public void RunPersonTest()
{
    Core.SetSaveOnErrorPath("GeneratedCode");

    IPerson personImplementation =
        Core.CreateImplementedInstance<IPerson, IPersonImplementor>();

    Core.Save("GeneratedCode");

    personImplementation.Name = "Bruce";

    personImplementation.Walk();
}  

If you run the test, the output will be:

C#
----------Entering Walk----------
The Person Bruce is walking
----------Exiting Walk----------

If you change InitType of the PluginAttribute to typeof(AnotherConsoleLogger), the output will be:

C#
**********Entering Walk**********
The Person Bruce is walking
**********Exiting Walk**********   

I just want to go over very briefly about how this functionality is implemented in the generated classes.

When Roxy sees that a plugin class is abstract, it creates a concretization of that class, where the abstract properties and methods are overridden with the concrete properties and methods that call corresponding delegates defined in the same class. The implementation class assigns those delegates to concrete properties and methods implemented within the Implementor class or within other plugins.

Here is the pseudo code for this sample

public class PersonImplementation
    implements IPerson,
    implemented by Person as ThePersonPlugin,
    implemented by ILog as TheLog;
{
    public PersonImplementation()
    {
        ThePersonPlugin = new Person();
        TheLog = new ConsoleLogger();
    }
}

Plugin Hierarchy Sample

The next test will demonstrate the plugin hierarchy - analogous to big hardware cards having slots for smaller hardware cards.

This test is located within StudentProfessorPersonTest.cs file and can be found under NP.XUnitAttrRoxyTests.StudentProfessorPersonTest within Test Explorer.

Interface IPerson and class Person are exactly the same as in the first and simplest test:

C#
public interface IPerson
{
    string Name { get; set; }

    void Walk();
}

public class Person
{
    public string Name { get; set; }

    public void Walk()
    {
        Console.WriteLine($"Person {Name} is walking");
    }
} 

Then there is ILearner interface with one method Learn() and ITeacher interface with one method Teach:

C#
public interface ILearner
{
    void Learn();
}

public interface ITeacher
{
    void Teach();
}  

IStudent is an IPerson and ILearner, while IProfessor is an IPerson and ITeacher:

C#
public interface IStudent : IPerson, ILearner
{

}

public interface IProfessor : IPerson, ITeacher
{

}  

Here are the diagrams for these two interfaces:

Image 7
Image 8

Finally, there is IStudentAndProfessor interface to represent someone who is a student and professor at the same time:

C#
public interface IStudentAndProfessor : IStudent, IProfessor
{

} 

Image 9

Here is the full diagram of the dependencies (including the hierarchy):

Image 10

The next image highlights the so called 'diamond problem' within the hierarchy - both IStudent and IProfessor inherit from IPerson:

Image 11

Here are the implementations for ILearner and ITeacher:

C#
public abstract class Learner : ILearner
{
    public abstract string Name { get; }

    public void Learn()
    {
        Console.WriteLine($"Student {Name} is learning");
    }
}

// teaching concern implementation
public abstract class Teacher : ITeacher
{
    public abstract string Name { get; }

    public void Teach()
    {
        Console.WriteLine($"Professor {Name} is teaching");
    }
}  

Now, let us take a look at the Implementor interfaces for IStudent and IProfessor:

C#
[ImplementationClassName("Student1")]
public interface IStudentImplementor
{
    [Plugin]
    Person ThePerson { get; }

    [Plugin]
    Learner TheLearner { get; }
}

[ImplementationClassName("Professor1")]
public interface IProfessorImplementor
{
    [Plugin]
    Person ThePerson { get; }

    [Plugin]
    Teacher TheTeacher { get; }
}  

Here is the Implementor for IStudentAndProfessor:

C#
[ImplementationClassName("StudentAndProfessorSharedImplementor")]
public interface IStudentAndProfessorImplementor
{
    [Plugin(typeof(IStudentImplementor))]
    IStudent TheStudent { get; }

    [Plugin(typeof(IProfessorImplementor))]
    IProfessor TheProfessor { get; }
}  

Here is the test method code:

C#
[Fact]
public void RunStudentAndProfessorPersonTest()
{
    Core.SetSaveOnErrorPath("GeneratedCode");

    // combining Person, learning and teaching concerns. 
    IStudentAndProfessor studentAndProfessorImplementation =
        Core.CreateImplementedInstance<IStudentAndProfessor, IStudentAndProfessorImplementor>();

    studentAndProfessorImplementation.Name = "Bruce";

    studentAndProfessorImplementation.Walk();
    studentAndProfessorImplementation.Learn();
    studentAndProfessorImplementation.Teach();

    Core.Save("GeneratedCode");
}  

We successively call Walk(), Learn() and Teach() method on the created object of the generated class.

Here is the output we are going to get, once we run this test:

Person Bruce is walking
Person Bruce is walking
Student Bruce is learning
Professor Bruce is teaching

Note that Walk() method has been called twice - since person part is contained within both the student and professor parts - the so called 'diamond problem'.

Roxy allows us to get around this problem by slightly changing IStudentAndProfessorImplementor adding a shared IPerson plugin:

C#
[ImplementationClassName("StudentAndProfessorSharedImplementor")]
public interface IStudentAndProfessorImplementor
{
    // added shared plugin
    [Plugin(IsShared = true)]
    Person ThePerson { get; }

    [Plugin(typeof(IStudentImplementor))]
    IStudent TheStudent { get; }

    [Plugin(typeof(IProfessorImplementor))]
    IProfessor TheProfessor { get; }
}

The shared plugin indicates that the Person sub-plugins should be shared across all the plugins that contain it. If you re-run the test, you'll see the following output:

Person Bruce is walking
Student  is learning
Professor  is teaching  

"Person Brush is walking" appears only once.

Here is the the pseudo code for the sample:

public class StudentImplementation :
    implements IStudent,
    implemented by Person as ThePerson,
    implemented by Learner as TheLearner
{

}
public class ProfessorImplementation :
    implements IProfessor,
    implemented by Person as ThePerson,
    implemented by Teacher as the Teacher
{
    
}    
public class StudentAndProfessorImplementation : 
    implements IStudent, IProfessor,
    implemented by StudentImplementation as TheStudent,
    implemented by ProfessorImplementation as TheProfessor,
    implemented by shared Person as ThePerson
{

}

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Architect AWebPros
United States United States
I am a software architect and a developer with great passion for new engineering solutions and finding and applying design patterns.

I am passionate about learning new ways of building software and sharing my knowledge with others.

I worked with many various languages including C#, Java and C++.

I fell in love with WPF (and later Silverlight) at first sight. After Microsoft killed Silverlight, I was distraught until I found Avalonia - a great multiplatform package for building UI on Windows, Linux, Mac as well as within browsers (using WASM) and for mobile platforms.

I have my Ph.D. from RPI.

here is my linkedin profile

Comments and Discussions

 
QuestionReally good Pin
Member 1055764231-Jul-18 4:15
Member 1055764231-Jul-18 4:15 
AnswerRe: Really good Pin
Nick Polyak31-Jul-18 5:14
mvaNick Polyak31-Jul-18 5:14 
QuestionWheel of 21 century Pin
Thornik31-Jul-18 4:01
Thornik31-Jul-18 4:01 
AnswerRe: Wheel of 21 century Pin
Nick Polyak31-Jul-18 5:14
mvaNick Polyak31-Jul-18 5:14 
GeneralRe: Wheel of 21 century Pin
Thornik31-Jul-18 6:09
Thornik31-Jul-18 6:09 
GeneralRe: Wheel of 21 century Pin
Nick Polyak31-Jul-18 13:32
mvaNick Polyak31-Jul-18 13:32 
SuggestionPoints for effort Pin
KBZX500031-Jul-18 1:36
KBZX500031-Jul-18 1:36 
GeneralRe: Points for effort Pin
Nick Polyak31-Jul-18 13:46
mvaNick Polyak31-Jul-18 13:46 
QuestionGood place to start with roslyn Pin
Sacha Barber30-Jul-18 21:29
Sacha Barber30-Jul-18 21:29 
AnswerRe: Good place to start with roslyn Pin
Nick Polyak31-Jul-18 2:03
mvaNick Polyak31-Jul-18 2:03 
GeneralRe: Good place to start with roslyn Pin
Sacha Barber31-Jul-18 7:40
Sacha Barber31-Jul-18 7:40 
GeneralRe: Good place to start with roslyn Pin
Nick Polyak31-Jul-18 12:22
mvaNick Polyak31-Jul-18 12:22 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.