First thing to do when you want to properly abstract the log lines is to give the individual elements names and proper types rather than pseudo-indices through names (i.e. entry1, entry2, etc.). So we'll have:
case class LogEntry(
time: java.util.Date,
userId: Int,
host: String
)
The strictness of the typing depends on your exact use case (note that host
here is not a java.net.InetAddress
).
When a new column gets added to your log files, there are two things that you might want to preserve:
- Functions that expected old-style LogEntries
- Treatment of old log files even after new functionality is added.
Case 1 is easy. Just add the field to the case class:
case class LogEntry(
time: java.util.Date,
userId: Int,
host: String,
port: Int
)
Since you have names, you could even change the order of the fields and the old code would still work as the fields it expects are still here. Since you do not want to handle old log files anymore, just adapt the read-in code.
Case 2 is a bit trickier: You'll have to reflect the fact that you'll have old and new log entries in your running application. You can either:
Make additional fields optional and give default value:
case class LogEntry( time: java.util.Date, userId: Int, host: String, port: Option[Int] = None )
Reflect versions of log formats in hierarchy (note: you should not inherit from case classes, so you'll have to make normal classes out of them):
class LogEntry( val time: java.util.Date, val userId: Int, val host: String ) class ExtendedLogEntry( time: java.util.Date, userId: Int, host: String, val port: Int ) extends LogEntry(time, userId, host)
Option 1 lets the code that treats log entries handle the difference between the entry types (i.e. all functions are still of type LogEntry => ?
). Further also note that it might allow inconsistent log entries (add two fields, both not present in format 1, required in format 2, you could still set one of them to None
).
Option 2 lets the calling code handle the difference between the entry types. If an advanced function needs an ExtendedLogEntry
, it's type will be ExtendedLogEntry => _
, so you cannot supply a LogEntry
to that function (alternative: pattern match in the function and provide default / fallback behavior). Further, it prevents the inconsistency that can occur with option 1.
Concerning read-in: Option 1 will automatically set the default argument when reading old-style log files, in Option 2, old-style log files will result in LogEntry
as before. So in both cases, old read-in code does not need to be changed, but potentially adapted in order to detect new-style or old-style logs and produce appropriate entries. (Note that this is potentially a problem with Option 2 as the style of the log is statically enforced through the typing system if you do not want to cast).