Results 1 to 9 of 9

Thread: [RESOLVED] Long methods

  1. #1

    Thread Starter
    Pro Grammar chris128's Avatar
    Join Date
    Jun 2007
    Location
    England
    Posts
    7,604

    Resolved [RESOLVED] Long methods

    I understand that it is good practice to break up your application's code into separate blocks - so for example you would not just have your entire code in a button click event, you would break up the parts of the procedure into methods that call each other. Normally I follow this practice but in my current little project (creating an SMTP server) I am finding that difficult. I'll explain...

    I have a method that is called each time a remote server connects to my SMTP server and tries to pass a message to it, this method needs to read the data that the remote server has sent and then respond to the remote server appropriately and store certain parts of the data the remote server sent (ie the sender address of the email message, the recipient address, the body of the email etc). Now you would think that this is quite easy to separate into different methods as you would just have one function that reads the data, then a quick IF statement to see what the command was that the remote server sent, and then a separate procedure for each of the possible commands that takes an appropriate action.

    However, because the actions of each of these parts all need to access so many things that are declared in the original method, it seems pointless just passing these items around through parameters in functions/subs just for the sake of the code looking a bit neater. Its kind of hard to explain but it seems to me that without either declaring all these things at class level (which looks pretty naff to me) or passing loads of object references around between each method, then there isnt really any obvious way of separating this code.

    Maybe its just me - maybe this method is no where near what you would consider "too long" and maybe the amount of objects I would need to pass to each method if I separated it all would not be considered anything out of the ordinary but just have a look at the code below and see what you think... As you can see, I have defined separate procedures for some things, such as the WriteToClientStream and SMTPSendMessage but I'm struggling to find any more things that are self contained enough to split out

    vb.net Code:
    1. ''' <summary>
    2. ''' Handles the session and command processing for each connected client
    3. ''' </summary>
    4. ''' <param name="AcceptedTcpClient">The TcpClient object that represents the connected client</param>
    5. Private Sub ManageSession(ByVal AcceptedTcpClient As Object)
    6.  
    7.         Dim RemoteClient As TcpClient = DirectCast(AcceptedTcpClient, TcpClient)
    8.         Dim RemoteIP As String = RemoteClient.Client.RemoteEndPoint.ToString
    9.         Dim ClientStream As NetworkStream = RemoteClient.GetStream
    10.  
    11.         'Respond to the connection request and raise the ClientConnected event
    12.         WriteToClientStream(ClientStream, "220 Prototype SMTP Server , ready at " & Now.ToString)
    13.         RaiseEvent ClientConnected(New SMTPSessionEventArgs(RemoteIP, String.Empty, String.Empty))
    14.  
    15.  
    16.         'Flags that are used to track the current progress of the session
    17.         Dim QuitCommandReceived As Boolean = False
    18.         Dim InDataCommand As Boolean = False
    19.         Dim MessageSent As Boolean = False
    20.  
    21.         'Email attributes populated by received SMTP commands
    22.         Dim SenderAddress As String = String.Empty
    23.         Dim RecipientAddresses As New List(Of String)
    24.         Dim Subject As String = String.Empty
    25.         Dim MessageBody As String = String.Empty
    26.  
    27.         'Will contain the string of data received from the remote client
    28.         Dim ReceivedStringBuilder As New StringBuilder
    29.  
    30.         Do While QuitCommandReceived = False
    31.             Do
    32.                 Dim size As Integer = 0
    33.                 Dim DataAsBytes(1024) As Byte
    34.                 Do While ClientStream.DataAvailable
    35.                     size = ClientStream.Read(DataAsBytes, 0, DataAsBytes.Length)
    36.                     Dim ShortDataAsString As String = System.Text.Encoding.ASCII.GetString(DataAsBytes, 0, size)
    37.                     ReceivedStringBuilder.Append(ShortDataAsString)
    38.                 Loop
    39.  
    40.                 If InDataCommand Then
    41.                     If ReceivedStringBuilder.ToString.EndsWith(vbCrLf & "." & vbCrLf) Then
    42.                         WriteToClientStream(ClientStream, "250 Message queued for delivery.")
    43.                         MessageBody = ReceivedStringBuilder.ToString.Remove(ReceivedStringBuilder.Length - 3, 3)
    44.                         SMTPSendMessage(SenderAddress, RecipientAddresses, Subject, MessageBody, RemoteIP)
    45.                         InDataCommand = False
    46.                         SenderAddress = String.Empty
    47.                         RecipientAddresses.Clear()
    48.                         Subject = String.Empty
    49.                         MessageBody = String.Empty
    50.                         MessageSent = True
    51.                         Exit Do
    52.                     End If
    53.                 ElseIf InDataCommand = False Then
    54.                     If ReceivedStringBuilder.ToString.EndsWith(vbCrLf) Then
    55.                         Exit Do
    56.                     End If
    57.                 End If
    58.             Loop 'Start listening for new data on the stream again
    59.  
    60.             If MessageSent Then
    61.                 MessageSent = False
    62.             Else
    63.                 Dim CommandEntered As String = String.Empty
    64.                 CommandEntered = ReceivedStringBuilder.ToString
    65.                 RaiseEvent CommandReceived(New SMTPSessionEventArgs(RemoteIP, CommandEntered, String.Empty))
    66.  
    67.                 '-------- HELO Command ---------
    68.                 If String.Compare(CommandEntered, "HELO" & vbCrLf, True) = 0 Then
    69.                     WriteToClientStream(ClientStream, "250 HELO From " & RemoteClient.Client.LocalEndPoint.ToString)
    70.  
    71.                     '--- EHLO Command ---
    72.                 ElseIf CommandEntered.StartsWith("EHLO ", StringComparison.CurrentCultureIgnoreCase) Or String.Compare(CommandEntered, "EHLO" & vbCrLf, True) = 0 Then
    73.                     WriteToClientStream(ClientStream, "250 OK")
    74.  
    75.                     '--- MAIL FROM Command ---
    76.                 ElseIf CommandEntered.StartsWith("MAIL FROM:", StringComparison.CurrentCultureIgnoreCase) Then
    77.                     If Not SenderAddress = String.Empty Then
    78.                         WriteToClientStream(ClientStream, "503 5.5.2 Sender already specified")
    79.                     Else
    80.                         Dim RequestedSenderAddress As String = CommandEntered.Substring(CommandEntered.IndexOf(":") + 1).Trim
    81.                         If RequestedSenderAddress.Contains("@") = False Then
    82.                             WriteToClientStream(ClientStream, "501 5.5.4 Invalid Address")
    83.                         Else
    84.                             SenderAddress = RequestedSenderAddress
    85.                             WriteToClientStream(ClientStream, "250 2.1.0 " & SenderAddress & "....Sender OK")
    86.                         End If
    87.                     End If
    88.  
    89.                     '--- RCPT TO Command ---
    90.                 ElseIf CommandEntered.StartsWith("RCPT TO:", StringComparison.CurrentCultureIgnoreCase) Then
    91.                     If SenderAddress = String.Empty Then
    92.                         WriteToClientStream(ClientStream, "503 5.5.2 Need Mail From: first")
    93.                     Else
    94.                         Dim RequestedRecipientAddress As String = CommandEntered.Substring(CommandEntered.IndexOf(":") + 1).Trim
    95.                         If RequestedRecipientAddress.Contains("@") = False Then
    96.                             WriteToClientStream(ClientStream, "501 5.5.4 Invalid Address")
    97.                         Else
    98.                             RecipientAddresses.Add(RequestedRecipientAddress)
    99.                             WriteToClientStream(ClientStream, "250 2.1.5 " & RequestedRecipientAddress)
    100.                         End If
    101.                     End If
    102.  
    103.                     '--- DATA Command ---
    104.                 ElseIf String.Compare(CommandEntered, "DATA" & vbCrLf, True) = 0 Then
    105.                     If SenderAddress = String.Empty Then
    106.                         WriteToClientStream(ClientStream, "503 5.5.2 Need mail command.")
    107.                     ElseIf RecipientAddresses.Count < 1 Then
    108.                         WriteToClientStream(ClientStream, "503 5.5.2 Need Rcpt command.")
    109.                     Else
    110.                         WriteToClientStream(ClientStream, "354 Start mail input; end with <CRLF>.<CRLF>")
    111.                         InDataCommand = True
    112.                     End If
    113.  
    114.                     '--- AUTH LOGIN Command ---
    115.                     ElseIf String.Compare(CommandEntered, "AUTH LOGIN" & vbCrLf, True) = 0 Then
    116.                     'Havent got this far yet...
    117.  
    118.                     '--- RSET Command ---
    119.                 ElseIf String.Compare(CommandEntered, "RSET" & vbCrLf, True) = 0 Then
    120.                     SenderAddress = String.Empty
    121.                     RecipientAddresses.Clear()
    122.                     Subject = String.Empty
    123.                     MessageBody = String.Empty
    124.                     InDataCommand = False
    125.                     WriteToClientStream(ClientStream, "250 2.0.0 Resetting")
    126.                     '--- QUIT Command ---
    127.                 ElseIf String.Compare(CommandEntered, "QUIT" & vbCrLf, True) = 0 Then
    128.                     QuitCommandReceived = True
    129.                     RemoteClient.Close()
    130.                     '--- Unknown Command ---
    131.                 Else
    132.                     WriteToClientStream(ClientStream, "500 5.3.3 Unknown Command: " & CommandEntered)
    133.                 End If
    134.             End If
    135.  
    136.             'Clear out the command string ready for next command
    137.             ReceivedStringBuilder.Remove(0, ReceivedStringBuilder.Length)
    138.  
    139.         Loop 'Start processing commands that are sent from the client again (unless QUIT has been sent)
    140. End Sub
    My free .NET Windows API library (Version 2.2 Released 12/06/2011)

    Blog: cjwdev.wordpress.com
    Web: www.cjwdev.co.uk


  2. #2

    Thread Starter
    Pro Grammar chris128's Avatar
    Join Date
    Jun 2007
    Location
    England
    Posts
    7,604

    Re: Long methods

    Oh and if you are wondering why the AcceptedTcpClient parameter of the method is just of type Object, its because this is how the method is called:

    vb.net Code:
    1. 'When a new client connection is accepted, start the ManageSession method on a new ThreadPool thread
    2. 'and then loop back to get ready for new client to connect
    3. Do While DoListen
    4.       Threading.ThreadPool.QueueUserWorkItem(AddressOf ManageSession, TListener.AcceptTcpClient())
    5. Loop

    I would have just added this as an edit to the original post but apparently that makes the post too long

    Oh and one random question - is a Sub/Function that is Private still called a Method or is Method just a term for public Subs and Functions?
    My free .NET Windows API library (Version 2.2 Released 12/06/2011)

    Blog: cjwdev.wordpress.com
    Web: www.cjwdev.co.uk


  3. #3
    Super Moderator jmcilhinney's Avatar
    Join Date
    May 2005
    Location
    Sydney, Australia
    Posts
    111,221

    Re: Long methods

    I haven't read all your code but the first place I can see that could be refactored would be lines 35 to 37:
    vb.net Code:
    1. ReceivedStringBuilder.Append(ReadDataString(ClientStream))
    vb.net Code:
    1. Private Function ReadDataString(ByVal stream As Stream) As String
    2.     Dim data(1023) As Byte
    3.  
    4.     stream.Read(data, 0, data.Length)
    5.  
    6.     Return System.Text.Encoding.ASCII.GetString(data)
    7. End Function
    Notice that I use 1023 as the upper bound of the array, so the Length is 1024.

    That's a private method.

    You could pass in a Byte array to avoid recreating an array each call but, unless that's going to make a significant difference to the performance then I'd suggest not. You should strive to write code that is as efficient as possible but you should also strive to write code that is as maintainable as possible too, so you need to balance the two.
    Last edited by jmcilhinney; Aug 16th, 2009 at 09:06 PM.
    Why is my data not saved to my database? | MSDN Data Walkthroughs
    VBForums Database Development FAQ
    My CodeBank Submissions: VB | C#
    My Blog: Data Among Multiple Forms (3 parts)
    Beginner Tutorials: VB | C# | SQL

  4. #4
    Super Moderator Shaggy Hiker's Avatar
    Join Date
    Aug 2002
    Location
    Idaho
    Posts
    40,106

    Re: Long methods

    I haven't read through the code, either, but it happens. Heck, the bulk of it is just an ElseIf ladder with relatively small blocks. Nothing surprising about that.

    My point is that you make it sound like the code is less right because it is a long method. As long as the code works, it is only less right if there is an alternative way to write the same thing such that it is more efficient. When it comes to Select Case and ElseIf ladders, that generally doesn't happen.
    My usual boring signature: Nothing

  5. #5

    Thread Starter
    Pro Grammar chris128's Avatar
    Join Date
    Jun 2007
    Location
    England
    Posts
    7,604

    Re: Long methods

    Quote Originally Posted by Shaggy Hiker View Post
    I haven't read through the code, either, but it happens. Heck, the bulk of it is just an ElseIf ladder with relatively small blocks. Nothing surprising about that.

    My point is that you make it sound like the code is less right because it is a long method. As long as the code works, it is only less right if there is an alternative way to write the same thing such that it is more efficient. When it comes to Select Case and ElseIf ladders, that generally doesn't happen.
    Thanks thats kind of why I posted it, because I thought it might just be me thinking it is bad when its not really. I appreciate the code is a bit long for everyone to go through each line so thanks for the comments anyway.

    JMC - thanks for that suggestion, I did consider doing that before but I wasnt sure if passing the network stream object around a lot was perhaps going to be detrimental to performance. Guess I should give it a go and see what happens through.
    That brings me on to another question about how passing reference types by value works, but I think that belongs in a new thread..

    So anyway, the general concencus then is that I could probably refactor a few little bits but in general it doesnt look like the method is really unnecessarily long?
    My free .NET Windows API library (Version 2.2 Released 12/06/2011)

    Blog: cjwdev.wordpress.com
    Web: www.cjwdev.co.uk


  6. #6
    Super Moderator jmcilhinney's Avatar
    Join Date
    May 2005
    Location
    Sydney, Australia
    Posts
    111,221

    Re: Long methods

    Passing any variable by value always works the same way: it copies the value. If that variable is a reference type then you're simply copying a reference, which is exactly why reference types exist. This allows you to pass large objects around without making multiple copies of the object.
    Why is my data not saved to my database? | MSDN Data Walkthroughs
    VBForums Database Development FAQ
    My CodeBank Submissions: VB | C#
    My Blog: Data Among Multiple Forms (3 parts)
    Beginner Tutorials: VB | C# | SQL

  7. #7
    Super Moderator Shaggy Hiker's Avatar
    Join Date
    Aug 2002
    Location
    Idaho
    Posts
    40,106

    Re: Long methods

    I'd say that the answer is Yes.
    My usual boring signature: Nothing

  8. #8
    PowerPoster techgnome's Avatar
    Join Date
    May 2002
    Posts
    34,687

    Re: Long methods

    So anyway, the general concencus then is that I could probably refactor a few little bits but in general it doesn't look like the method is really unnecessarily long?
    I'd agree with that too. I've seen worse, and when you mentioned that it was long, I was surprised to find it weighing at 140 lines. That's typical. I've seen routines that weigh in at 500+ (and sadly with no way to break it out further)... I didn't read all of the code, but scanned through it... and it seems like it's already about as tight as it is going to get.

    -tg
    * I don't respond to private (PM) requests for help. It's not conducive to the general learning of others.*
    * I also don't respond to friend requests. Save a few bits and don't bother. I'll just end up rejecting anyways.*
    * How to get EFFECTIVE help: The Hitchhiker's Guide to Getting Help at VBF - Removing eels from your hovercraft *
    * How to Use Parameters * Create Disconnected ADO Recordset Clones * Set your VB6 ActiveX Compatibility * Get rid of those pesky VB Line Numbers * I swear I saved my data, where'd it run off to??? *

  9. #9

    Thread Starter
    Pro Grammar chris128's Avatar
    Join Date
    Jun 2007
    Location
    England
    Posts
    7,604

    Re: Long methods

    Cool, well thanks for the comments guys just my lack of real world developer experience showing through I guess :P
    My free .NET Windows API library (Version 2.2 Released 12/06/2011)

    Blog: cjwdev.wordpress.com
    Web: www.cjwdev.co.uk


Posting Permissions

  • You may not post new threads
  • You may not post replies
  • You may not post attachments
  • You may not edit your posts
  •  



Click Here to Expand Forum to Full Width