Click here to Skip to main content
15,849,182 members
Articles / Programming Languages / C#

Using Chain Of Responsibility Instead of if/else Statement

Rate me:
Please Sign up or sign in to vote.
4.12/5 (40 votes)
5 Nov 2015CPOL2 min read 119.8K   40   52
Using chain of responsibility instead of if/else statement

Introduction

Currently, I am involved in building a web application. One of the features of the app is for the user to be able to upload their photo. But the system currently only allows three image formats to be uploaded JPG, BMP and PNG, with the possibility that in the future other formats will be supported.

Each image file has a header that, among other things, it specifies what format it is. In our case, the headers are as follows:

File format Header
JPG [0xff, 0xd8]
BMP [0x42, 0x4D]
PNG [0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]

Our image decoding service is defined as:

C#
public interface IImageDecodingService
{
    ImageFormat DecodeImage(byte[] imageBuffer);
}

And the ImageFormat is defined as an enumeration:

C#
public enum ImageFormat
{
    Unknown,
    Bmp,
    Png,
    Jpeg
}

and one possible implementation can be:

C#
public class ImageDecodingService : IImageDecodingService
{
    private readonly byte[] _jpgHeader = { 0xff, 0xd8 };
    private readonly byte[] _pngHeader = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };
    private readonly byte[] _bmpHeader = { 0x42, 0x4D };

    public ImageFormat DecodeImage(byte[] imageBuffer)
    {
        if (ContainsHeader(imageBuffer, _jpgHeader))
            return ImageFormat.Jpeg;

        if (ContainsHeader(imageBuffer, _pngHeader))
            return ImageFormat.Png;

        if (ContainsHeader(imageBuffer, _bmpHeader))
            return ImageFormat.Bmp;

        return ImageFormat.Unknown;
    }

    protected static bool ContainsHeader(byte[] buffer, byte[] header)
    {
        for (int i = 0; i < header.Length; i += 1)
        {
            if (header[i] != buffer[i])
            {
                return false;
            }
        }

        return true;
    }
}

The problem with this approach is that any time we need to support a new image format, we need to go and change the class. This breaks the "Open/Closed Principle".

A better approach is to implement each decoder as a class and then chain them together (using "Chain of Responsibility Pattern").

To achieve this, first we need to implement the decoders. The interface for the decoders looks like:

C#
public interface IImageDecoder
{
    ImageFormat DecodeImage(byte[] buffer);
}

Since the decoders are very similar, we can extract a base class that implements common methods as follows:

C#
public abstract class BaseDecoder : IImageDecoder
{
    private ImageFormat _decodingFormat;

    protected BaseDecoder(ImageFormat decodingFormat)
    {
        _decodingFormat = decodingFormat;
    }

    protected abstract byte[] Header { get; }

    public ImageFormat DecodeImage(byte[] buffer)
    {
        if(ContainsHeader(buffer, Header))
        {
            return _decodingFormat;
        }

        return ImageFormat.Unknown;
    }

    private static bool ContainsHeader(byte[] buffer, byte[] header)
    {
        for (int i = 0; i < header.Length; i += 1)
        {
            if (header[i] != buffer[i])
            {
                return false;
            }
        }

        return true;
    }
}

Now our decoders look like:

C#
public sealed class JpegDecoder : BaseDecoder, IImageDecoder
{
    public JpegDecoder() : base(ImageFormat.Jpeg)
    { }

    protected override byte[] Header
    {
        get { return new byte[] { 0xff, 0xd8 }; }
    }
}

public sealed class BmpDecoder : BaseDecoder, IImageDecoder
{
    public BmpDecoder() : base(ImageFormat.Bmp)
    { }

    protected override byte[] Header
    {
        get { return new byte[] { 0xff, 0xd8 }; }
    }
}

public sealed class PngDecoder : BaseDecoder, IImageDecoder
{
    public PngDecoder() : base(ImageFormat.Png)
    { }

    protected override byte[] Header
    {
        get { return new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; }
    }
}

And the last decoder is the decoder that just returns ImageFormat.Unknown. The implementation looks like:

C#
public class UnknownImageDecoder : IImageDecoder
{
    public ImageFormat DecodeImage(byte[] buffer)
    {
        return ImageFormat.Unknown
    }
}

The next step is to refactor our base class so it allows chaining. The refactored class looks like:

C#
public abstract class BaseDecoder : IImageDecoder
{
    private readonly ImageFormat _decodingFormat;
    private IImageDecoder _nextChain;

    protected BaseDecoder(ImageFormat decodingFormat)
    {
        _decodingFormat = decodingFormat;
    }

    protected BaseDecoder(IImageDecoder nextChain, ImageFormat decodingFormat) : this(decodingFormat)
    {
        if (nextChain == null)
        {
            throw new ArgumentNullException("nextChain");
        }

        _nextChain = nextChain;
    }

    protected abstract byte[] Header { get; }

    public ImageFormat DecodeImage(byte[] buffer)
    {
        if (ContainsHeader(buffer, Header))
        {
            return _decodingFormat;
        }

        if (_nextChain != null)
        {
            return _nextChain.DecodeImage(buffer);
        }

        return ImageFormat.Unknown;
    }

    private static bool ContainsHeader(byte[] buffer, byte[] header)
    {
        for (int i = 0; i < header.Length; i += 1)
        {
            if (header[i] != buffer[i])
            {
                return false;
            }
        }

        return true;
    }
}

As you can see now we have two constructors, one that takes the ImageFormat and the other that takes IImageDecoder as a next chain and ImageFormat. The reason for two constructors is that the first constructor (with only one parameter) allows the decoder to be used on its own, whereas the second constrcutor (the one with two parameters) enables to build the chain.

Pay attention to the DecodeImage(...) method. Now if this method does not know how to decode the image, and the next chain is specified, it passes the responsibility to the next chain.

We also need to add the second constructor to our decoders:

C#
public sealed class BmpDecoder : BaseDecoder
{
    public BmpDecoder() 
        : base(ImageFormat.Bmp)
    { }

    public BmpDecoder(IImageDecoder nextChain) 
        : base(nextChain, ImageFormat.Bmp)
    { } 

    protected override byte[] Header
    {
        get { return new byte[] { 0xff, 0xd8 }; }
    }
}

public sealed class PngDecoder : BaseDecoder
{
    public PngDecoder() 
        : base(ImageFormat.Png)
    { }

    public PngDecoder(IImageDecoder nextChain) 
        : base(nextChain, ImageFormat.Png)
    { }            

    protected override byte[] Header
    {
        get { return new byte[] { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A }; }
    }
}

public sealed class JpegDecoder : BaseDecoder
{
    public JpegDecoder()
        : base(ImageFormat.Jpeg)
    { }

    public JpegDecoder(IImageDecoder nextChain)
        : base(nextChain, ImageFormat.Jpeg)
    { } 

    protected override byte[] Header
    {
        get { return new byte[] { 0xff, 0xd8 }; }
    }
}

To construct the chain, we need a factory that constructs it and returns the first one. The interface for the factory looks like:

C#
public interface IImageDecoderFactory
{
    IImageDecoder Create();
}

And the implementation looks like:

C#
public class ImageDecoderFactory : IImageDecoderFactory
{
    public IImageDecoder Create()
    {
        return new BmpDecoder(new JpegDecoder(new PngDecoder(new UnknownImageDecoder())));
    }
}

Now our ImageDecodingService looks like:

C#
public class ImageDecodingService : IImageDecodingService
{
    private readonly IImageDecoderFactory _imageDecoderFactory;

    public ImageDecodingService(IImageDecoderFactory imageDecoderFactory)
    {
        _imageDecoderFactory = imageDecoderFactory;
    }

    public ImageFormat DecodeImage(byte[] imageBuffer)
    {
            var decoder = _imageDecoderFactory.Create();
        return decoder.DecodeImage(imageBuffer);
    }
}

So, if we need to support another format, we would implement the decoder for it and then add it to the factory. In a real-world application, you would register the decoders with a DI Container and then the DI Container would pass the decoders to the factory and the factory would chain them together. In this way, you do not need to change any existing code to support another format.

License

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


Written By
Software Developer (Senior)
United Kingdom United Kingdom
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.

Comments and Discussions

 
QuestionGiven you 5 for your attempt Pin
Shivprasad koirala17-Nov-15 5:36
Shivprasad koirala17-Nov-15 5:36 
GeneralMy vote of 3 Pin
Paulo Zemek12-Nov-15 8:25
mvaPaulo Zemek12-Nov-15 8:25 
GeneralRe: My vote of 3 Pin
Fitim Skenderi12-Nov-15 9:40
professionalFitim Skenderi12-Nov-15 9:40 
GeneralRe: My vote of 3 Pin
Paulo Zemek12-Nov-15 9:48
mvaPaulo Zemek12-Nov-15 9:48 
GeneralRe: My vote of 3 Pin
Fitim Skenderi12-Nov-15 10:10
professionalFitim Skenderi12-Nov-15 10:10 
GeneralViolation of Open/Closed Principle Pin
Paulo Zemek11-Nov-15 16:00
mvaPaulo Zemek11-Nov-15 16:00 
GeneralRe: Violation of Open/Closed Principle Pin
Fitim Skenderi12-Nov-15 0:05
professionalFitim Skenderi12-Nov-15 0:05 
GeneralRe: Violation of Open/Closed Principle Pin
Paulo Zemek12-Nov-15 8:23
mvaPaulo Zemek12-Nov-15 8:23 
QuestionRe: Violation of Open/Closed Principle Pin
Fitim Skenderi12-Nov-15 9:37
professionalFitim Skenderi12-Nov-15 9:37 
AnswerRe: Violation of Open/Closed Principle Pin
Paulo Zemek12-Nov-15 9:44
mvaPaulo Zemek12-Nov-15 9:44 
GeneralRe: Violation of Open/Closed Principle Pin
Fitim Skenderi12-Nov-15 9:55
professionalFitim Skenderi12-Nov-15 9:55 
GeneralRe: Violation of Open/Closed Principle Pin
Paulo Zemek12-Nov-15 10:03
mvaPaulo Zemek12-Nov-15 10:03 
GeneralRe: Violation of Open/Closed Principle Pin
Fitim Skenderi12-Nov-15 10:16
professionalFitim Skenderi12-Nov-15 10:16 
GeneralRe: Violation of Open/Closed Principle Pin
Paulo Zemek12-Nov-15 10:51
mvaPaulo Zemek12-Nov-15 10:51 
GeneralRe: Violation of Open/Closed Principle Pin
Patrick Blackman24-Feb-16 5:21
professionalPatrick Blackman24-Feb-16 5:21 
GeneralRe: Violation of Open/Closed Principle Pin
Patrick Blackman24-Feb-16 5:22
professionalPatrick Blackman24-Feb-16 5:22 
GeneralRe: Violation of Open/Closed Principle Pin
Fitim Skenderi24-Feb-16 6:39
professionalFitim Skenderi24-Feb-16 6:39 
QuestionMy vote of 4 Pin
Sung M Kim11-Nov-15 12:16
professionalSung M Kim11-Nov-15 12:16 
SuggestionClient Program Pin
Juzer9-Nov-15 13:38
Juzer9-Nov-15 13:38 
QuestionI came for the "replace if"... Pin
Tedd Hansen9-Nov-15 3:17
Tedd Hansen9-Nov-15 3:17 
QuestionDon't understand Pin
Илья Марголин8-Nov-15 8:11
Илья Марголин8-Nov-15 8:11 
AnswerRe: Don't understand Pin
Fitim Skenderi8-Nov-15 13:20
professionalFitim Skenderi8-Nov-15 13:20 
Question[My vote of 2] Complicated without reason Pin
Member 80071558-Nov-15 2:43
Member 80071558-Nov-15 2:43 
AnswerRe: [My vote of 2] Complicated without reason Pin
Fitim Skenderi8-Nov-15 7:50
professionalFitim Skenderi8-Nov-15 7:50 
GeneralRe: [My vote of 2] Complicated without reason Pin
Member 800715510-Nov-15 5:19
Member 800715510-Nov-15 5:19 
But why not encapsulate that responsibility within a single class ... again configurable outside of code. For example;
- map the doc type headers
- the mime equivalent codeless
- the UX common termset
- the winform (if that is your target) rendering class.
So a bitmap is mapped via the unique header string, has the appropriate content type when viewed in html/email, is prompted for as a "Bitmap File" and calls on the System.Drawing.Bitmap to render it within your WinForm UI.

All via soft configuration ... not a line of code changed to introduce another document type.

Perhaps if you had detailed some use cases that demanded the code population explosion we could have appreciated the reason ... but at the moment Frown | :(

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.