分配分支条件大小太高

Assignment Branch Condition size is too high

我正在制作采用多行字符串(日志)并将新字符串写入数组的方法。

def task_2(str)
  result = []
  str.each_line do |x|
    ip = x[/^.* - -/]
    datetime = x[/[\[].*[\]]/]
    address = x[/T .* H/]
    if !ip.nil? && !datetime.nil? && !address.nil?
      result << datetime[1..-2] + ' FROM: ' + ip[0..-4] + 'TO:' + address[1..-3]
    end
  end
  result
end

我需要它通过默认配置的 rubocop 分析,但它给出 AbcSize 18.68/15 而且我确定是因为 if..end 声明,但我该如何重写它?

日志示例:

10.6.246.103 - - [23/Apr/2018:20:30:39 +0300] "POST /test/2/messages HTTP/1.1" 200 48 0.0498
10.6.246.101 - - [23/Apr/2018:20:30:42 +0300] "POST /test/2/run HTTP/1.1" 200 - 0.2277

我不使用 rubocop,但我确实用这些数据测试了以下内容:

data = <<FILE
10.6.246.103 - - [23/Apr/2018:20:30:39 +0300] "POST /test/2/messages HTTP/1.1" 200 48 0.0498
10.6.246.101 - - [23/Apr/2018:20:30:42 +0300] "POST /test/2/run HTTP/1.1" 200 - 0.2277
12.55.123.255 - - Hello
FILE

使用 String#gsub!Enumerable#select报告 AbcSize 为 3

def task_2(str)
  str.each_line.select do |x|
    # Without named groups 
    # x.gsub!(/\A([\d+\.\d+]+).*(?<=\[)(.*)(?=\]).*(?<=\s)((?:\/\w+)*?)(?=\s).*\z/m,
    # ' FROM  TO ')
    x.gsub!(/\A(?<ip>[\d+\.\d+]+).*(?<=\[)(?<date_time>.*)(?=\]).*(?<=\s)(?<address>(?:\/\w+)*?)(?=\s).*\z/m,
      '\k<date_time> FROM \k<ip> TO \k<address>')
  end
end


task_2(data)
# => ["23/Apr/2018:20:30:39 +0300 FROM 10.6.246.103 TO /test/2/messages", 
#      "23/Apr/2018:20:30:42 +0300 FROM 10.6.246.101 TO /test/2/run"]

这里我们使用 String#gsub! 和模式替换,如果不进行替换,它将 return nil 从而从 Enumerable#select 中拒绝它。

类似的解决方案,尽管效率可能较低,使用 String#matchEnumerable#mapArray#compact报告 AbcSize 为 7.14

def task_2(str)
  str.each_line.map do |x|
    match = x.match(/\A(?<ip>[\d+\.\d+]+).*(?<=\[)(?<date_time>.*)(?=\]).*(?<=\s)(?<address>(?:\/\w+)*?)(?=\s)/)
    "#{match['date_time']} FROM #{match['ip']} TO #{match['address']}" if match
  end.compact
end

这里我们使用String#match提取匹配数据,然后确认匹配,如果匹配则输出所需的格式。不匹配的字符串将输出 nil,因此我们 compact Array 删除 nil 值。

另一种选择可以是一次性 scan 整个 String 并分解匹配组:(报告 AbcSize 为 5)

def task_2(str)
  str.scan(/^([\d+\.\d+]+).*(?<=\[)(.*)(?=\]).*(?<=\s)((?:\/\w+)*?)(?=\s).*$/)
    .map {|a| "#{a[1]} FROM #{a[0]} TO #{a[2]}"}
end

可以通过

使最后一个低至2.24
 def task_2(str)
  r = []
  str.scan(/^([\d+\.\d+]+).*(?<=\[)(.*)(?=\]).*(?<=\s)((?:\/\w+)*?)(?=\s).*$/) do |ip, date_time, address | 
    r << "#{date_time} FROM #{ip} TO #{address}"
  end
  r
end
def task_2(str)
  result = []
  str.each_line do |x|
    ip = x[/^.* - -/]
    datetime = x[/[\[].*[\]]/]
    address = x[/T .* H/]
    if ip && datetime && address
      result << datetime[1..-2] + ' FROM: ' + ip[0..-4] + 'TO:' + address[1..-3]
    end
  end
  result
end

有!variable.nil?是多余的。基本上,你在这里检查存在,所以#present?方法就足够了,但是任何不是 nil 或 false 的值都被认为是 false,所以为了更符合习惯,最好只使用我在 if 语句中使用的形式。这解决了 ABS 问题。

ABC 大小的计算方法如下:

√(assignments² + branches² + conditionals²)

先来看看作业:

result = []
ip = x[/^.* - -/]
datetime = x[/[\[].*[\]]/]
address = x[/T .* H/]

这给我们留下了 4 个作业。

接下来是树枝。为此,我不得不提到大多数运算符都是方法(因此计入分支)例如 1 + 1 也可以写成 1.+(1) + 是整数上的方法。这同样适用于 string[regex],可以写成 string.[](regex) [] 是字符串上的一种方法。 !value 可以写成 value.!@ !@ 是所有对象的方法。

让我们数一数分支。

str.each_line
x[/^.* - -/]
x[/[\[].*[\]]/]
x[/T .* H/]
!ip.nil? # counts for 2 (! and .nil?)
!datetime.nil? # counts for 2 (! and .nil?)
!address.nil? # counts for 2 (! and .nil?)
result << ...
datetime[1..-2]
ip[0..-4]
address[1..-3]
+ # 4 times in result << ... + ... + ....

这给我们留下了 18 个分支。

最后要计算的是条件句。由于 Ruby 使用 &&|| 运算符的短路,它们将计入条件。

if
&& # 2 times

这给我们留下了 3 个条件。

√(4² + 18² + 3²) ≈ 18.68

现在我们了解了数字的来源,我们可以尝试减少它。减少 ABC 大小的最简单方法是减少数字最大的东西,因为这个数字是平方的。在您的情况下,这些是分支机构。您已经发现了问题所在。

if !ip.nil? && !datetime.nil? && !address.nil?
  result << datetime[1..-2] + ' FROM: ' + ip[0..-4] + 'TO:' + address[1..-3]
end

可以简化为:

if ip && datetime && address
  result << "#{datetime[1..-2]} FROM: #{ip[0..-4]}TO:#{address[1..-3]}"
end

一共拿走10个树枝。 3 次 !something.nil?(算 2 次,因为 !.nil? 都计入分支)和 4 次 +.

留给你:

√(4² + 8² + 3²) ≈ 9.43

任何时候我 运行 进入 ABC 太高(或类似的 complexity/length 警告),我很快就会把方法砍掉。您的可读性、可测试性和可维护性几乎总是会提高。

最快的方法是将循环体或条件提取到新方法中。根据需要重复,直到您可以一口气阅读每个方法。

同样,如果您有大型复杂的 conditionals/loop 构造,也请将其提取到新方法中。

将这两种策略组合足够多次,可以将任何方法简化为大致两个方法调用。在某些情况下,这可能有点过分热心……但永远不会太远。

这是您可以将该策略应用于您的代码的一种方法:

def task_2(str)
  result = []

  str.each_line do |x|
    ip, datetime, address = parse_line(x)

    if [ip, datetime, address].all?
      result << "#{datetime[1..-2]} FROM: #{ip[0..-4]} TO: #{address[1..-3]}"
    end
  end

  result
end

def parse_line(x)
  ip = x[/^.* - -/]
  datetime = x[/[\[].*[\]]/]
  address = x[/T .* H/]
  return [ip, datetime, address]
end

s =<<EOF
123.123.123.999 - - [2009-12-31 13:13:13] T www.google.com H"
456.456.456.999 - - [2009-12-31 13:13:13] 404"
678.678.678.999 - - [2009-12-31 13:13:13] T www.amazon.com H"
EOF

puts task_2(s)

产生输出:

2009-12-31 13:13:13 FROM: 123.123.123.999  TO:  www.google.com
2009-12-31 13:13:13 FROM: 678.678.678.999  TO:  www.amazon.com

如果你想走得更远,你可以将 each_line 的主体拉出到一个新方法,process_line,等等。如果你创建了一个 class,你可以避免混乱的(在我看来)多值 returns。

这是一个使用命名捕获组很方便的问题。

R = /
    (?=                       # begin a positive lookahead
      (?<ip>.*\s-\s-)         # match the string in a capture group named 'ip' 
    )                         # end positive lookahead
    (?=                       # begin a positive lookahead
      .*                      # match any number of characters
      (?<datetime>[\[].*[\]]) # match the string in a capture group named 'datetime'
    )                         # end positive lookahead
    (?=                       # begin a positive lookahead
      .*                      # match any number of characters
      (?<address>T\s.*\sH)    # match the string in a capture group named 'address' 
    )                         # end positive lookahead
    /x                        # free-spacing regex definition mode

def task_2(str)
  str.each_line.with_object([]) do |s, result|
    m = str.match(R)
    result << m[:datetime][1..-2] + ' FROM: ' + m[:ip][0..-4] +
              'TO:' + m[:address][1..-3] unless m.nil?      
  end
end

str =<<_
123.123.123.999 - - [2009-12-31 13:13:13] T www.google.com H"
456.456.456.999 - - [2009-12-31 13:13:13] 404"
678.678.678.999 - - [2009-12-31 13:13:13] T www.amazon.com
_
task_2 str
  #=> ["2009-12-31 13:13:13 FROM: 123.123.123.999 TO: www.google.com",
  #    "2009-12-31 13:13:13 FROM: 123.123.123.999 TO: www.google.com",
  #    "2009-12-31 13:13:13 FROM: 123.123.123.999 TO: www.google.com"] 

正则表达式约定俗成如下

R = /(?=(?<ip>\A.* - -))(?=.*(?<datetime>[\[].*[\]]))(?=.*(?<address>T .* H))/

请注意,在以自由间距模式编写正则表达式时,我在这里有空格的地方有空白字符 (\s)。那是因为在自由间距模式下,在计算表达式之前会去除空格。或者,可以通过将空格括在字符 类 ([ ]).

中以自由间距模式保留空格。