Click here to Skip to main content
15,885,546 members
Please Sign up or sign in to vote.
0.00/5 (No votes)
See more:
Hi there,

This is a long long long post... I'm afraid that I cannot really make it shorter, so please be indulgent with me :)

Well, I'm trying to design an API.NET for communication with Testing Equipment and I cannot figure out a proper way for designing the architecture.

Basically, we have different (physical) products:

- A Controller to perform measurement on channel inputs (e.g. Current, Voltage, Frequency,
Digital, Relay, etc.) and generate some signals on its outputs (AWG, Voltage, Frequency, Digital, etc.) and some typical automotive buses: CAN, LIN and K-LINE.

- A Unit, able to deal with the power supply and to embed several controllers.

- An Extension to perform some temperature measurements and bring, it's connected to a Unit or alone.

The fact is the embedded team (who is charge of designing all the dirty stuff over the hardware) gave me the commands and acknowledgments are not really clear, neither user-friendly, and for some legacy reasons they're not gonna change that...

Mainly cause prior to that situation we used to have a software dealing directly with the commands and it was a huge huge huge mess, aka baked noodles. Since I decided to gather all the communication stuff into assembly dedicated to that matter. It allows btw to make instrument drivers (for NI LabVIEW) by wrapping the objects and methods of the assembly.

I'm pretty proud of that cause it allows our systems to be used in many many different Software platforms (e.g. Matlab) and in our own software platforms as well. I also manage myself to make our assembly Mono compliant.

The protocol as it has been defined it not that extensible (not necessarily there is a need to make it extensible but I feel sometimes that it could be better).

Unit Command: {@}{UnitId}{XX}{_}{UnitMessageKeyword}{=Parameters}(if any){;}
Unit Acknowledgment: {#}{UnitId}{XX}{_}{UnitMessageKeyword}{=Answer}(if any){;}

Controller Command: {@}{ParentUnitId}{ControllerId}{_}{ControllerMessageKeyword}{=Parameters}(if any){;}
Controller Acknowledgment: {#}{ParentUnitId}{ControllerId}{_}{ControllerMessageKeyword}{=Answers}(if any){;}

Extension Command: {@}{ExtensionId}{XX}{_}{ExtensionMessageKeyword}{=Parameters}(if any){;}
Extension Acknowledgment: {@}{ExtensionId}{XX}{_}{ExtensionMessageKeyword}{=Parameters}(if any){;}

Controller Instead to provide a dummy interface with the command to the end users, I decided to gather messages and acknowledgment into things what really the user needs through properties, it's more natural as it really represents the Unit:

- Property (Category) Power Supply => Enabled { get; set; } / CurrentMax { set; get; } / Current { get; } / Voltage { get; set; }
- Property (category) Information => SubCategory Identifiers { get; } / SubCategory OperatingTimes { get; } / etc.
- Method On() // Turn On...
- Method Off() // Turn Off...
- Method Lock() // Remote Control Only
- Method Unlock() // Allow Manual Mode
- ReadOnlyKeyedCollection Controllers { get; }
- Etc.

Lock() / Unlock() methods could be gathered as a Boolean Property Locked { get; set; }
The same for On() / Off() methods into Powered / On { get; set; } or whatsoever.

The fact is that we are making new products, some of them are based on the previous one: like a Controller which is out of the unit for the customers who do not need a big power supply.

For some reasons a Message Keyword does not necessarily match a Command Implementation in the assembly, sometimes the possibilities for one keywords are simply too many (and can lead to have a lot of Nullables as properties of the message), therefore when it's possible a keyword can be used in different command implementations when the concerns are properly separated.

Message => Object
- Acknowledgment (Token Reception: Start / Stop) => Message
- AcknowledgmentUnit => Acknowledgment
- AcknowledgmentController => Acknowledgment
- AcknowledgmentExtension => Acknowledgment

- Command (Token Transmission: Start / Stop) => Message
- CommandUnit => Command
- CommandController => Command
- CommandExtension => Command

And here is start my real problems, some might think that the context above is not really useful but I believe that knowing the context helps to have proper answers.

I have several problems but I'm not really sure that they can be solved.

- Overuse of generics, and some with "circular" definitions... [http://blogs.msdn.com/b/ericlippert/archive/2011/02/03/curiouser-and-curiouser.aspx][1]
[1]: http://blogs.msdn.com/b/ericlippert/archive/2011/02/03/curiouser-and-curiouser.aspx

- I have some virtual calls in base Constructors... yeah not good either...

- and some generics lead for some convenience stuff to generate automatically the properties give in generics via a private setter and reflection, then only the public getter is available with public modifier.

- A lack of interface use, mostly relying on abstracts...

Sometimes C# is not powerful enough to express all the constraints I would like to set and avoid any mistakes for further maintenance and improving this assembly.


I'm gonna paste only the parts presenting issues and introduce the design flaws:
Let's start with Message Class, with the virtual calls in the constructor:
C#
public abstract class Message<TDeviceId, TDeviceMessageKeyword>
        where TDeviceId: struct, IComparable, IFormattable, IConvertible // Enum "Constraint"
        where TDeviceMessageKeyword: struct, IComparable, IFormattable, IConvertible // Enum "Constraint"
    {
        internal abstract String Header { get; }
        internal abstract String Content { get; }

        public abstract TDeviceId Id { get; }
        public abstract TDeviceMessageKeyword MessageKeyword { get; }

        private const String _separatorIdKeyword = "_";
        protected String SeparatorIdKeyword
        {
            get
            {
                return Message<TDeviceId, TDeviceMessageKeyword>._separatorIdKeyword;
            }
        }

        private const String _separatorHeaderContent = "=";
        protected String SeparatorHeaderContent
        {
            get
            {
                return Message<TDeviceId, TDeviceMessageKeyword>._separatorHeaderContent;
            }
        }

        private const String _flagStop = ";";
        protected virtual String FlagStop
        {
            get
            {
                return Message<TDeviceId, TDeviceMessageKeyword>._flagStop;
            }
        }

        protected abstract String FlagStart { get; }

        public abstract CommunicantPacket Packet { get; }
    }


I'm a little bit embarassed cause I could use interface to avoid with this kind of "virtual calls in the constructor" warnings but that's not really the purpose of an interface....

C#
public abstract class Command<TDeviceId, TDeviceMessageKeyword> : Message<TDeviceId, TDeviceMessageKeyword>
        where TDeviceId: struct, IComparable, IFormattable, IConvertible
        where TDeviceMessageKeyword: struct, IComparable, IFormattable, IConvertible
    {
        protected Command(TDeviceId id)
        {
            var idByte = Convert.ToByte(id);
            var idByteStringHex = BitConverter.ToString (new [] { idByte });

            this._id = id;
            this._idByteStringHex = idByteStringHex;
        }

        private readonly String _idByteStringHex ;
        protected String IdByteStringHex
        {
            get
            {
                return this._idByteStringHex;
            }
        }

        private const String _flagStart = "@";
        protected override sealed String FlagStart
        {
            get
            {
                return Command<TDeviceId, TDeviceMessageKeyword>._flagStart;
            }
        }

        private readonly TDeviceId _id;
        public override TDeviceId Id
        {
            get
            {
                return this._id;
            }
        }

        private CommunicantPacket _packet;
        public sealed override CommunicantPacket Packet
        {
            get
            {
                if (this._packet == null)
                {
                    // Analysis disable once ConvertIfStatementToConditionalTernaryExpression
                    if (String.IsNullOrEmpty (this.Content))
                    {
                        this._packet = new CommunicantPacket (this.FlagStart + this.Header + this.FlagStop);
                    }
                    else
                    {
                        this._packet = new CommunicantPacket (this.FlagStart + this.Header + this.SeparatorHeaderContent + this.Content + this.FlagStop);
                    }
                }

                return this._packet;
            }
        }
    }

    public abstract class Command<TDeviceId, TDeviceKeyword, TAcknowledgment> : Command<TDeviceId, TDeviceKeyword>
        where TDeviceId: struct, IComparable, IFormattable, IConvertible
        where TDeviceKeyword: struct, IComparable, IFormattable, IConvertible
        where TAcknowledgment : Acknowledgment<TDeviceId, TDeviceKeyword>
    {
        protected Command(TDeviceId id)
            : base(id)
        {
        }

        // TODO: Optimization... Caching and stuff, using Expression Trees
        internal TAcknowledgment GetAcknowledgment (CommunicantPacket communicantPacket)
        {
            try
            {
                var typeAcknowledgment = typeof(TAcknowledgment);
                const BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic;
                var arguments = new Object[] { communicantPacket, this };
                var acknowledgment = Activator.CreateInstance (typeAcknowledgment, bindingFlags, null, arguments, null) as TAcknowledgment;

                return acknowledgment;
            }
            catch (Exception exception)
            {
                if (exception.InnerException != null)
                {
                    throw exception.InnerException;
                }
                else
                {
                    throw;
                }
            }
        }
    }


public abstract class Acknowledgment<TDeviceId, TDeviceMessageKeyword> : Message<TDeviceId, TDeviceMessageKeyword>
        where TDeviceId: struct, IComparable, IFormattable, IConvertible
        where TDeviceMessageKeyword: struct, IComparable, IFormattable, IConvertible
    {
        protected Acknowledgment(CommunicantPacket acknowledgmentPacket)
            : base()
        {
            if (String.IsNullOrEmpty(acknowledgmentPacket.Data.String))
            {
                var message = @"The argument ""acknowledgmentPacket"" cannot be null or empty.";
                throw new ArgumentNullException ("acknowledgmentPacket", message);
            }
            else
            {
                const String pattern = @"^(?<FlagStart>{0})(?<Header>{1})((?<SeparatorHeaderContent>{2})(?<Content>{3}))?(?<FlagStop>{4})";
                var patternApplied = String.Format (pattern, this.FlagStart, RegexProxy.AnyOnceOrMoreButEqual, this.SeparatorHeaderContent, RegexProxy.AnyOnceOrMore, this.FlagStop);

                var regex = new Regex (patternApplied);

                var match = regex.Match (acknowledgmentPacket.Data.String);

                if (match.Success)
                {
                    this._header = match.Groups ["Header"].Value;
                    this._content = match.Groups ["Content"].Value;
                    this._packet = acknowledgmentPacket;
                }
                else
                {
                    throw new FormatException(acknowledgmentPacket.Data.String);
                }
            }
        }

        private readonly String _header;
        internal override String Header
        {
            get
            {
                return this._header;
            }
        }

        private readonly String _content;
        internal override String Content
        {
            get
            {
                return this._content;
            }
        }

        private const String _flagStart = "#";
        protected override sealed String FlagStart
        {
            get
            {
                return Acknowledgment<TDeviceId, TDeviceMessageKeyword>._flagStart;
            }
        }

        private readonly CommunicantPacket _packet;
        public sealed override CommunicantPacket Packet
        {
            get
            {
                return this._packet;
            }
        }
    }

    public abstract class Acknowledgment<TDeviceId, TDeviceKeyword, TCommand> : Acknowledgment<TDeviceId, TDeviceKeyword>
        where TDeviceId: struct, IComparable, IFormattable, IConvertible
        where TDeviceKeyword: struct, IComparable, IFormattable, IConvertible
        where TCommand : Command<TDeviceId, TDeviceKeyword>
    {
        private void VirtualCallProcessContent (TCommand relatedCommand, String content)
        {
            this.ProcessContent (relatedCommand, content);
        }

        protected Acknowledgment(CommunicantPacket acknowledgmentPacket, TCommand relatedCommand)
            : base(acknowledgmentPacket)
        {
            if (this.Header != relatedCommand.Header)
            {
                String message = String.Format (@"The Command and Acknowledgment Headers, respectively @""{0}"" and ""{1}"", are not matching.", this.Header, relatedCommand.Header);
                throw new FormatException (message);
            }
            else
            {
                this._id = relatedCommand.Id;
                this._keyword = relatedCommand.MessageKeyword;
                this._relatedCommand = relatedCommand;

                this.VirtualCallProcessContent (relatedCommand, this.Content);
            }
        }

        protected abstract void ProcessContent (TCommand relatedCommand, String content);

        private readonly TDeviceKeyword _keyword;
        public override TDeviceKeyword MessageKeyword
        {
            get
            {
                return this._keyword;
            }
        }

        private readonly TDeviceId _id;
        public override TDeviceId Id
        {
            get
            {
                return this._id;
            }
        }

        private readonly TCommand _relatedCommand;
        public TCommand RelatedCommand
        {
            get
            {
                return this._relatedCommand;
            }
        }
    }


Then we have the internal TAcknowledgment GetAcknowledgment (CommunicantPacket communicantPacket) done with Reflection to avoid the user to have to redefine it in every inherited implementions, apart that this constructor calls could be cached to run nearly as fast as compiled instructions (thanks to compiled expressions), I feel like something smells here.

Also an abstract method that is also called in the constructor... which btw does not help
but enforces the fact this method needs to be implemented for each command inherited.

This is mostly all about the message design flaws...

The other big design flaws are related to the classes representing the physical Devices.
C#
public abstract class Device : Disposer, IDevice
{
    protected Device(Byte id)
    {
        this._id = id;
    }

    private readonly Byte _id;

    #region IDevice Implementation
    Byte IDevice.Id
    {
        get
        {
            return this._id;
        }
    }
    #endregion
}

// TODO: Check possible improvements using Laziness...
    public abstract class Device<TDeviceId, TDeviceInformation> : Device
        where TDeviceId : IComparable, IConvertible, IFormattable
        where TDeviceInformation : IDeviceInformation
{
    protected Device(TDeviceId id)
        : base(Convert.ToByte(id))
    {
        this._id = id;

        var typeDeviceInformation = typeof(TDeviceInformation);

        const BindingFlags bindingFlags = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance;

        this._information = (TDeviceInformation)Activator.CreateInstance (typeDeviceInformation, bindingFlags, null, new Object[] { this }, null);
    }

    private readonly TDeviceId _id;
    public TDeviceId Id
    {
        get
        {
            return this._id;
        }
    }

    private readonly TDeviceInformation _information;
    public TDeviceInformation Information
    {
        get
        {
            return this._information;
        }
    }
}


public interface IDeviceCategory
{
}

public abstract class DeviceCategory<TDevice>
    where TDevice : class, IDevice
{
    internal DeviceCategory (TDevice parentDevice)
    {

    }

    private readonly TDevice _parentDevice;
    internal TDevice ParentDevice
    {
        get
        {
            return this._parentDevice;
        }
    }
}

public interface IDeviceInformation : IDeviceCategory
{
}

public abstract class DeviceInformation<TDevice> : DeviceCategory<TDevice>, IDeviceInformation
    where TDevice : class, IDevice
{
    internal DeviceInformation (TDevice parentDevice)
        : base (parentDevice)
    {

    }
}

public abstract class DeviceCategorySub<TParentDeviceCategory>
    where TParentDeviceCategory : class, IDeviceCategory
{
    internal DeviceCategorySub (TParentDeviceCategory parentDeviceCategory)
    {
        this._parentDeviceCategory = parentDeviceCategory;
    }

    private readonly TParentDeviceCategory _parentDeviceCategory;
    protected TParentDeviceCategory ParentDeviceCategory
    {
        get
        {
            return this._parentDeviceCategory;
        }
    }
}


Here I didnt find any proper way to enforce properly constraints so I have those useless interfaces instead... and still it does not enforce constraints enough...

I dunno if there something any other implementations that would make my code more elegant... and at the same time keeping its conveniency...
Posted
Updated 23-Jun-14 23:49pm
v2
Comments
Sergey Alexandrovich Kryukov 24-Jun-14 11:41am    
It really sounds that you lack a real architect. I cannot imagine helping you in the format of the Quick Questions and Answers. The size of your question would not be a problem at all. The problem is that it would take a long interactive talk, to say the least, not just answering you, as the format of the forum demands.

One note: you should not hope for the miracle. You say, embedded stuff is dirty, cannot be changed, protocol "is not that extensible", this part does not perfectly match that, and so on... So, what would you expect? If you added unreasonable amount of entropy in the project, you will either need to redo bad parts, or make the system even more complex by wrapping the legacy code in some more flexible, unified and extensible elements of architecture, and it's too hard to analyze, without knowing a lot more detail, which way is going to lead you where...

—SA
Ehouarn Perret 24-Jun-14 21:48pm    
I think you're overexagerating... a bit, I dont think I added that much entropy to the project, and for most of it, that can be easily changed into something a bit less trickier and more conventional but will increase a lot the number of lines for barely nothing... that's always the same old song.

I don't think it has something to do with the architect... or maybe you're probably have any good idea about how to make it better. But you're probably gonna defend yourself with the fact that there is not enough details... wait the post was not long enough? I think that if it sounds already that much doogy for you, that means there is already something to tell about the organization / architecture / code...

I'm working in startup and as for now we do not have that many guys to code, (aka we are two and we have plenty of other projects to deal with, plus meetings, plus support, plus unit tests, plus other libraries, plus UI...), I guess what we have done so far is not that bad cause we still have customers and many new projects, and soon we're gonna add three guys to my team, so I was wondering how I could improve what has been already done.

Also I have this feeling that your answer is a little arrogant. No offense, this is just what I feel and it's probably not your intention.

Still that's true that there is no proper section for such analysis, so I just gave a try here. I'm terribly sorry, if, to you, was so disturbing.
Sergey Alexandrovich Kryukov 24-Jun-14 22:25pm    
Maybe this is just the impression of your post. And it sounds like it wasn't you, but the legacy you have. If it's not so, please correct me. No, there is nothing disturbing in your post; it's different: I feel frustrated by my inability to grasp it all and really help. I think it would take some couple of hours to start considering the state of the project seriously. I would need to ask a lot of questions.

I really have my dedicated and very advanced approach to architecture of such hardware system, but it's too big to explain even in one big article. Some ideas are: separation of the hierarchy classes representing devices and metadata classes representing device metadata. You device class is too abstract to do any real work. So, we need some set of generic devices classifying them into functional units: for example, digital I/O control/acquisition units, oscilloscopes, motion systems and the like. Each class (not class in the sense of programming, but in the sense of set theory and classification) should have its separate interface, so all devices would not have to be derived from the same class.

Further item is plug-ins. All devices (or some sets of similar devices, say, produced by the same company, or a product like) should come in a separate assembly. For plug-in technology, I used both MEF and my own, much lighter one. The set of plug-ins to be loaded can be defined by some general metadata (object graph) or everything found in some executable directory, and, say, matched with the detected devices (for those which can be detected through some common interface, which is not always the case).

A big separate chapter is threading...

And so on. Plug-ins, layers, metadata-driven approach, separation of concerns and inversion of control — it all is involved in just one aspect: definition and description of the set of devices... To cover all aspects, it would take a pretty big book, even without consideration of major available system, buses and platforms...

—SA
Ehouarn Perret 25-Jun-14 2:10am    
Well, at least that's a start, I was considering the ioc for a while but I also considered my workload and it was not possible for me to manage everything. But could make the things definitely easier. For now... everything is bit too static and I'm dreaming to have equipment descriptors that can be properly turned into static properties for the end-user /developed (static != C# keyword).

I got your point about the device, I already thought about, actually I'm struggling with myself to decide when to come up with interfaces but for now as our products are not that numerous, I'm containing myself and try to reduce as much as I can for minimizing the maintenance load... but that's nice to see that there is some general stuff that I can rely on, and especially I feel a bit better that someone else thought the same thing.

The legacy I have is all about the hardware and embedded part, the rest is all from me. I see, yup that's frustrating, just like it's for me, I can't find a proper way to split this design mess into proper smaller parts cause it involves many many things all interconnected to each other.

I used MEF in our analysis software application for plugins approach (some special requirements from some special customers...), I didnt think about using it for those drivers, it might be good, I will consider it.

Well thanks at least, if you have any idea of books concerning this sort of design, could be good.

For the IOC, I will see whether, I will use a framework on my own (sometimes I like reinventing, even without that much time for doing that) or using some of the existing ones.

Thanks again for pointing out some key elements in this kind of design approach.

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



CodeProject, 20 Bay Street, 11th Floor Toronto, Ontario, Canada M5J 2N8 +1 (416) 849-8900