avoid_implementing_value_types 규칙: ==를 오버라이딩하는 클래스를 implements로 구현하지 않기

avoid_implementing_value_types 규칙: ==를 오버라이딩하는 클래스를 implements로 구현하지 않기

avoid_implementing_value_types==를 오버라이딩하는 클래스를 implements로 구현하지 말라고 알려주는 린트(lint) 규칙이에요. implements로 구현하면 ==의 메서드 본문을 상속받지 않아서, ==가 지켜야 하는 계약(equivalence relation)을 따르기가 사실상 불가능해져요.

출처: avoid_implementing_value_types

본문

설명

==를 오버라이딩하는 클래스를 implements로 구현하지 마세요.

== 연산자는 계약상 동치 관계(equivalence relation)여야 해요. 즉 모든 객체 o1o2에 대해 대칭적으로, o1 == o2o2 == o1이 둘 다 true이거나 둘 다 false여야 해요.

NOTE: Dart에는 진짜 *값 타입(value type)*이 없어서, 대신 ==를 구현하는 클래스를 값 타입을 식별하는 *대리(proxy)*로 간주해요.

implements를 쓰면 ==의 메서드 본문을 상속받지 않아서, ==의 계약을 따르기가 사실상 불가능해져요. ==를 오버라이딩하는 클래스는 보통 mock이나 fake를 만들지 않고도 테스트에서 직접 쓸 수 있는 경우가 많아요. 예를 들어 Size라는 클래스가 있다고 해 볼게요.

class Size {
  final int inBytes;
  const Size(this.inBytes);

  @override
  bool operator ==(Object other) => other is Size && other.inBytes == inBytes;

  @override
  int get hashCode => inBytes.hashCode;
}

BAD:

class CustomSize implements Size {
  final int inBytes;
  const CustomSize(this.inBytes);

  int get inKilobytes => inBytes ~/ 1000;
}

BAD:

import 'package:test/test.dart';
import 'size.dart';

class FakeSize implements Size {
  int inBytes = 0;
}

void main() {
  test('should not throw on a size >1Kb', () {
    expect(() => someFunction(FakeSize()..inBytes = 1001), returnsNormally);
  });
}

GOOD:

class ExtendedSize extends Size {
  ExtendedSize(int inBytes) : super(inBytes);

  int get inKilobytes => inBytes ~/ 1000;
}

GOOD:

import 'package:test/test.dart';
import 'size.dart';

void main() {
  test('should not throw on a size >1Kb', () {
    expect(() => someFunction(Size(1001)), returnsNormally);
  });
}

활성화

avoid_implementing_value_types 규칙을 활성화하려면 analysis_options.yaml 파일의 linter > rulesavoid_implementing_value_types를 추가해요.

linter:
  rules:
    - avoid_implementing_value_types

YAML map 문법으로 린터 규칙을 설정한다면 linter > rulesavoid_implementing_value_types: true를 추가해요.

linter:
  rules:
    avoid_implementing_value_types: true

더 알아보기