1

Let's say there are classes D1, D2, etc. describing different types of an abstract class D.

Let's say there are SenderReceiver classes describing different ways of communicating for each D1, D2, etc.:

abstract class SenderReceiver {
    abstract void Setup();
    abstract void SendA();
    public void M1() { /* Common code */ }
    ...
    public void MN() { /* Common code */ }
}

abstract class SenderReceiverPlus : SenderReceiver {
    abstract void SendB();
}

class SenderReceiverPlusTcp : SenderReceiverPlus {
    override void Setup() { /* BeginConnect code */ }
}

class SenderReceiverTcp : SenderReceiver {
    override void Setup() { /* Same code as above (vomit) */ }
}

class SenderReceiverPlusTcpD1 : SenderReceiverPlusTcp {
    override void SendA()  { /* Code */ }
    override void SendB()  { /* Code */ }
}

class SenderReceiverTcpD2 : SenderReceiverTcp {
    override void SendA()  { /* Code */ }
}

class SenderReceiverPlusUdp : SenderReceiverPlus {
    override void Setup() { /* Socket bind code */ }
}

class SenderReceiverUdp : SenderReceiver {
    override void Setup() { /* Same code as above (vomit) */ }
}

class SenderReceiverPlusUdpD3 : SenderReceiverPlusUdp {
    override void SendA()  { /* Code */ }
    override void SendB()  { /* Code */ }
}

class SenderReceiverUdpD4 : SenderReceiverUdp {
    override void SendA()  { /* Code */ }
}

abstract class D {
    SenderReceiver S;
    abstract void DoThing();
}

class D1 : D {
    D1 () { S = new SenderReceiverPlusTcpD1(); }
    override void DoThing() { S.SendA(); S.SendB(); S.M1(); }
}

class D2 : D {
    D2 () { S = new SenderReceiverTcpD2(); }
    override void DoThing() { S.SendA(); S.M1(); }
}

class D3 : D {
    D1 () { S = new SenderReceiverPlusUdpD3(); }
    override void DoThing() { S.SendA(); S.SendB(); S.M1(); }
}

class D4 : D {
    D2 () { S = new SenderReceiverUdpD4(); }
    override void DoThing() { S.SendA(); S.M1(); }
}

And there is a class with a list of D objects:

class Controller {
    List<D> ds;

    public Controller() {
        ds = new List<D>(new [] {new D1(), new D2(), new D3(), new D4()});
    }

    public DoAllThings() {
        foreach (D d in ds) {
            d.DoThing();
        }
    }
}

This does not work because (for one) D1's reference to S references a SenderReceiver and so SendB() is not assuredly there. Also, it's gross and there's duplicated code in the different Tcp and Udp types.

I've considered empty virtual methods in the base class, casting S to the correct type in D1 and D2, and not having S in the D class. None of these are particularly satisfying.

Is there a better way to do this?

EDIT:

Updated code to show attempts in current code at flexibility for Udp/Tcp protocols.

EDIT2: So it is clear that the class hierarchy here is a bit overdone. To simplify it, I could do

abstract class SenderReceiver {
    Socket _Socket;
    protected class StateObject { /* For async network methods */ }
    void Dispose();
    void Exit();
}
class SenderReceiverTcp : SenderReceiver {
    void Connect() { /* Sets up socket and calls BeginConnect */ }
    void EndConnect() { /* Callback for Connect() */ }

    void Accept() { /* Sets up socket, Bind, Listen, BeginAccept */ }
    void EndAccept() { /* Callback for Accept() */ }

    void Send(msg) { /* Send message */ }
    void Receive() { /* Receive or BeginReceive */ }
    void EndReceive() { /* Callback if Receive is async */ }
}

class SenderReceiverUdp : SenderReceiver {
    void Connect() { /* Sets up socket and calls Bind */ }

    void Send(msg) { /* Send message */ }
    void Receive() { /* Receive or BeginReceive */ }
    void EndReceive() { /* Callback if Receive is async */ }
}

class SenderRecevierD1 {
    SenderReceiverTcp _SenderReceiverTcp;

    void Connect() { _SenderReceiverTcp.Connect(); }
    void Accept() { _SenderReceiverTcp.Accept(); }
    void Send() { msg.Create(); _SenderReceiverTcp.Send(msg); }
    ...
}

class SenderReceiverD2 {
    SenderReceiverUdp _SenderReceiverUdp;

    void Connect() { _SenderReceiverUdp.Connect(); }
    void Send() { msg.Create(); _SenderReceiverUdp.Send(msg);
                _SenderReceiverUdp.Receive();
                if (!_SenderReceiverUdp.ReceiveEnded()) // Check for ack
                    _SenderReceiverUdp.Send(msg);
                }
    ...
}

This way, the D# classes don't need to know how to send messages for their type (maybe one D class with TCP retries if there is no acknowledgement while another doesn't wait for an acknowledgement because that D type doesn't send acknowledgements) but the differences in TCP/UDP setup and send are abstracted in the SenderReceiverTcp/Udp classes.

This is sort of an extension of "Removing SenderReceiver from the base class". I don't want to post this as an answer because I'm not positive if it's maximally right. I'll leave it for a bit and wait for community feedback.

2
  • 1
    At a first glance, I get the impression of combinatorial explosion. What about favor composition over inheritance? Commented Apr 27, 2017 at 8:12
  • @BernhardHiller I agree - that's what I was attempting to rectify in the lower example. I was trying to keep a TCP and UDP SenderReceiver to abstract the idea of "Connecting" and "Closing" away from the D#s and either have: 1) SenderReceiverD# classes with SenderReceiverTCP/UDP members and each D# has a SenderReceiverD# member 2) D# classes with SenderReceiverTCP/UDP members and put the messaging functionality in each D# class. Do these make sense? Any new D# will have its specific behavior in its class and leverage either the SenderReceiverD# or the SenderReceiverTCP/UDP class. Commented Apr 28, 2017 at 16:43

2 Answers 2

1

I tried to understand your question and examples, and I guess the Command pattern could help to improve your design. Below is some code explaining what I thought:

abstract class Communicator {
    abstract void Setup();
    public void M1() { /* Common code */ }
    ...
    public void MN() { /* Common code */ }
}

class TcpCommunicator {
    override void Setup() { //specific code }
}
class UcpCommunicator {
    override void Setup() { //specific code }
}

//...and there might be other communicator implementations...

interface ISendCommand {
    Communicator communicator;
    void execute();
}

class SendACommand : ISendCommand {
    Communicator communicator; //injected via constructor
    void execute();
}
class SendBCommand : ISendCommand {
    Communicator communicator; //injected via constructor
    void execute();
}

//...and there might be other types of messages to be sent...


abstract class D {
    Communicator S;
    abstract void DoThing();
}

class D1 : D {
    D1 () { S = new TcpCommunicator(); }
    override void DoThing() { 
        new SendACommand(S).execute(); 
        new SendBCommand(S).execute(); 
        S.M1(); 
    }
}

class D2 : D {
    D2 () { S = new TcpCommunicator(); }
    override void DoThing() {
        new SendACommand(S).execute();  
        S.M1(); 
    }
}

class D3 : D {
    D1 () { S = new UdpCommunicator(); }
    override void DoThing() { 
        new SendACommand(S).execute(); 
        new SendBCommand(S).execute(); 
        S.M1(); 
    }
}

class D4 : D {
    D2 () { S = new UdpCommunicator(); }
    override void DoThing() { 
        new SendACommand(S).execute();  
        S.M1(); 
    }
}

//...and so on...
3
  • 1
    I haven't worked on that project for a while, but I'm always interested in learning! Unfortunately, I've been outside of C# for a while so I forget some details. I thought interfaces are contracts for method signatures, so what is the "Communicator communicator;" in ISendCommand? Otherwise, I do like this because it's up to the command and D classes to know the details while the communicators are general to their protocol. I'll upvote this for now and, if nothing changes in a week, mark it as the answer and check with someone on the project to see if they want to pull it in! Commented Oct 26, 2017 at 12:18
  • @MarkLodato: actually, I just wanted to point that the ISendCommand instances must have a reference to some Communicator. The "Communicator communicator; " is actually useless within the interface. Commented Oct 26, 2017 at 15:51
  • After spending a few years away and learning more about design "in general", I think this ties back nicely to the first comment on the question about composition over inheritance. By injecting the type of communicator instead of inheriting from it we can hopefully avoid the "combinatorial explosion" mentioned above Commented Dec 23, 2020 at 20:17
0

One possibility that might work is to make two new interfaces

public interface ASenderReceiver {
    void SendA();
    void Close();
}

public interface BSenderReceiver {
    void SendB();
    void Close();
}

Then create a composite class

public class ABSenderReceiver : ASenderReceiver, BSenderReceiver {
    private readonly ASenderReceiver A;
    private readonly BSenderReceiver B;

    public void SendA() {
        A.SendA();
   }

    public void SendB() {
        B.SendB();
   } 

    public void A.Close() {
        A.Close();
   } 

    public void B.Close() {
        B.Close();
   } 
} 

Then pass the new class around in your code.

6
  • This is pretty cool. Because Close() is actually shared between all inheritors of SenderReceiver, I can just have A.Close() call B.Close() to reduce duplication. One question though: if D has an ABSenderReceiver member (let's call it S_AB) and I have two D derivatives (D1 and D2) and D1 needs to SendA and SendB but D2 only needs to SendA, now D2.S_AB.SendB() exists but doesn't make any sense...right? Did I miss something? Commented Apr 24, 2017 at 16:06
  • While this is a nice "hack" if you have no other options, it should really be pointed out that this design "sucks" almost as bad as the initial design because what happens when you need a CSenderReceiver, DSenderReceiver etc.... If this is the only place then no big deal, but it is seldom the case where designs like this have only one place where the designers didn't box themselves into corners forcing these types of hacks.
    – Dunk
    Commented Apr 24, 2017 at 23:02
  • @Dunk One issue we're facing is: there's a base SenderReceiver class (for common functionality as well as abstract methods). There's the ...Plus class that sends additional types of messages. For SenderReceiver and ...Plus, we have Tcp and Udp derived classes (because things like "Setup" are common to all Tcp and Udp children). This is unfortunate because If we need a new ...PlusExtra, we have to derive two more Tcp/Udp classes. I don't see a way around this because interfaces can't have bodies. In the immortal words of Flight of the Conchords, "Be more constructive with your feedback, please" Commented Apr 25, 2017 at 11:49
  • @MarkLodato - I think my feedback is constructive for people who have the option to redesign or are in the process of designing. If they come upon this post they might actually think this is a good idea to incorporate in the design when the fact is that it is nothing but an ugly hack working around a poor design to begin with. BTW, your Sender receiver class has zero need to know if it is sending over TCP, UDP, Serial, RF or quantum entanglement.
    – Dunk
    Commented Apr 25, 2017 at 13:55
  • @Dunk Cool, now we're getting somewhere. Who SHOULD know? Should the D classes know the protocol and hand that to the SenderReceiver class? If so, should D also know that a SenderReceiver for TCP needs to do Begin/EndConnect on the client side and Begin/EndAccept on the server side while for UDP it just needs the Socket to Bind? Should there be a class in between D and SenderReceiver that knows this? I'm looking for advice on how to architect this because I KNOW it's sub optimal :-) Commented Apr 25, 2017 at 14:09

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.