예외를 던지거나 null을 반환할지 여부를 제어하는 ​​매개 변수-좋은 방법?


24

나는 종종 실패시 예외를 던지거나 null이 반환되는지 여부를 제어하는 ​​추가 부울 매개 변수가있는 메소드 / 함수를 발견합니다.

어떤 경우에 더 좋은 선택인지에 대한 논의가 이미 있으므로 여기에 중점을 두지 마십시오. 예를 들어 매직 값 반환, 예외 발생 또는 실패시 거짓 반환을 참조하십시오 .

대신 두 가지 방법 모두를 지원해야 할 이유가 있다고 가정 해 봅시다.

개인적으로 나는 그러한 방법이 두 가지로 나뉘어 야한다고 생각합니다. 하나는 실패시 예외를 던지고 다른 하나는 실패시 null을 반환합니다.

그래서 어느 것이 더 낫습니까?

A : $exception_on_failure매개 변수가있는 한 가지 방법 .

/**
 * @param int $id
 * @param bool $exception_on_failure
 *
 * @return Item|null
 *   The item, or null if not found and $exception_on_failure is false.
 * @throws NoSuchItemException
 *   Thrown if item not found, and $exception_on_failure is true.
 */
function loadItem(int $id, bool $exception_on_failure): ?Item;

B : 두 가지 방법이 있습니다.

/**
 * @param int $id
 *
 * @return Item|null
 *   The item, or null if not found.
 */
function loadItemOrNull(int $id): ?Item;

/**
 * @param int $id
 *
 * @return Item
 *   The item, if found (exception otherwise).
 *
 * @throws NoSuchItemException
 *   Thrown if item not found.
 */
function loadItem(int $id): Item;

편집 : C : 다른 것?

많은 사람들이 다른 옵션을 제안하거나 A와 B 모두에 결함이 있다고 주장합니다. 그러한 제안이나 의견은 환영받으며 관련성 있고 유용합니다. 완전한 답변에는 그러한 추가 정보가 포함될 수 있지만 서명 / 행동을 변경하기위한 매개 변수가 좋은 아이디어인지에 대한 주요 질문도 다룰 것입니다.

노트

누군가 궁금한 경우 : 예제는 PHP로되어 있습니다. 그러나이 질문은 PHP 또는 Java와 다소 유사한 언어에 적용됩니다.


5
PHP로 작업해야하고 능숙하지만, 그저 나쁜 언어이고 표준 라이브러리가 여기 저기 있습니다. 참조를 위해 eev.ee/blog/2012/04/09/php-a-fractal-of-bad-design 을 읽으십시오 . 따라서 일부 PHP 개발자가 나쁜 습관을들이는 것은 놀라운 일이 아닙니다. 그러나 나는이 특별한 디자인 냄새를 아직 보지 못했습니다.
Polygnome

주어진 답변에서 필수 포인트가 누락되었습니다. 언제든지 하나를 사용할 것입니다. 발견되지 않은 항목이 예상치 않고 오류 (공황 시간)로 간주되는 경우 예외 방법을 사용합니다. 누락 된 항목이 가능성이없고 오류가 아니라 정상적인 흐름 제어에 포함시킬 가능성이있는 경우 Try ... 메소드를 사용합니다.
Martin Maat


1
@Polygnome 당신은 맞습니다. 표준 라이브러리 함수는 어디에나 있고, 많은 코드가 존재하지 않으며, 요청 당 하나의 프로세스 문제가 있습니다. 그러나 각 버전마다 나아지고 있습니다. 타입과 객체 모델은 기대할 수있는 모든 것을 수행합니다. 제네릭, 공존 및 공분산, 서명을 지정하는 콜백 유형 등을보고 싶습니다.이 중 많은 부분이 논의 중이거나 구현 방법에 있습니다. 표준 라이브러리 문제가 쉽게 사라지지는 않지만 전반적인 방향이 좋다고 생각합니다.
donquixote

4
한 가지 제안 : 대신에 귀하에게 적합한 loadItemOrNull(id)지 고려 loadItemOr(id, defaultItem)하십시오. item이 문자열 또는 숫자 인 경우 종종 수행합니다.
user949300

답변:


35

맞습니다. 몇 가지 이유로 두 가지 방법이 훨씬 좋습니다.

  1. Java에서 잠재적으로 예외를 발생 시키는 메소드서명 에는이 예외가 포함됩니다. 다른 방법은 그렇지 않습니다. 그것은 한 쪽에서 다른 쪽에서 무엇을 기대해야 하는지를 명확하게합니다.

  2. 메소드의 서명이 예외에 대해 아무 것도 알려주지 않는 C #과 같은 언어에서는 공개 메소드를 계속 문서화해야하며 해당 문서에는 예외가 포함됩니다. 단일 방법을 문서화하는 것은 쉽지 않습니다.

    귀하의 예는 완벽합니다. 두 번째 코드의 주석이 훨씬 명확 해 보이며“발견 된 경우 품목 (예외 제외)”을“항목”으로 줄여야합니다 .— 예외가있을 수 있습니다. 당신이 그것에 대한 설명은 자명하다.

  3. 단일 메소드의 경우 런타임에 부울 매개 변수의 값을 토글하려는 경우가 거의 없으며 , 그렇게하면 호출자가 두 경우 ( null응답 예외) 를 처리해야 함을 의미합니다 ), 코드를 필요한 것보다 훨씬 어렵게 만듭니다. 선택은 런타임이 아니라 코드를 작성할 때 이루어 지므로 두 가지 방법이 완벽합니다.

  4. .NET Framework와 같은 일부 프레임 워크 는이 상황에 대한 규칙설정 했으며 제안한 것처럼 두 가지 방법으로 해결합니다. 유일한 차이점은 그들이 패턴의 이완에 의해 설명 사용할 수 있다는 것입니다 그의 대답 그래서, int.Parse(string): int동안 예외가 발생 int.TryParse(string, out int): bool하지 않습니다. 이 명명 규칙은 .NET 커뮤니티에서 매우 강력하며 코드가 설명하는 상황과 일치 할 때마다 따라야합니다.


1
감사합니다. 이것이 제가 찾던 답변입니다! 그 목적은 Drupal CMS ( drupal.org/project/drupal/issues/2932772)에 대한 토론을 시작한 것이 었으며 개인적 선호에 관한 것이 아니라 다른 사람들의 의견을 반영하여 뒷받침하고 싶습니다.
donquixote

4
적어도 .NET의 오랜 전통은 두 가지 방법을 사용하지 않고 올바른 작업에 적합한 선택을하는 것입니다. 예외적 인 상황에서 예외는 예상되는 제어 흐름을 처리하지 않습니다. 문자열을 정수로 구문 분석하지 못했습니까? 예상치 못한 상황이나 예외적 인 상황은 아닙니다. int.Parse()정말 나쁜 디자인 결정으로 간주됩니다. 바로 .NET 팀이 앞서 구현 한 이유 TryParse입니다.
Voo

1
우리가 처리하는 사용 사례에 대해 생각하는 대신 두 가지 방법을 제공하는 것이 쉬운 방법입니다. 수행중인 작업에 대해 생각하지 말고 API의 모든 사용자에게 책임을 져야합니다. 물론 그것은 작동하지만 좋은 API 디자인의 특징 (또는 문제에 대한 모든 디자인이 아니다 읽기 예. 조엘의 포획을이에 .
Voo

@Voo 유효한 포인트. 실제로 두 가지 방법을 제공하면 발신자가 선택할 수 있습니다. 아마도 일부 매개 변수 값의 경우 항목이 존재할 것으로 예상되는 반면 다른 매개 변수 값의 경우 존재하지 않는 항목이 일반적인 경우로 간주됩니다. 구성 요소는 항목이 항상 있어야하는 한 응용 프로그램과 존재하지 않는 항목이 정상으로 간주되는 다른 응용 프로그램에서 사용됩니다. 아마도 발신자는 전화 ->itemExists($id)하기 전에 전화를 ->loadItem($id)겁니다. 아니면 과거에 다른 사람들이 선택한 것일 수도 있고 당연히 받아 들여야합니다.
donquixote

1
이유 1b : 일부 언어 (Haskell, Swift)는 서명에 널 입력 가능을 요구합니다. 특히 Haskell은 nullable 값 ( data Maybe a = Just a | Nothing)에 대해 완전히 다른 유형을 갖습니다 .
존 드보락

9

흥미로운 변형은 Swift 언어입니다. Swift에서는 함수를 "throwing"으로 선언합니다. 즉, 오류를 던질 수 있습니다 (Java 예외와 비슷하지만 동일하지는 않음). 호출자는이 함수를 다음 네 가지 방법으로 호출 할 수 있습니다.

에이. try / catch 문에서 예외를 포착하고 처리합니다.

비. 호출자가 스스로 던지기로 선언되면 호출하면 오류가 발생하고 호출자가 던진 오류로 바뀝니다.

기음. 함수 호출을 표시하면 try!"이 호출이 발생하지 않을 것"이라는 의미입니다. 호출 된 함수 발생하면 응용 프로그램이 중단 된 것입니다.

디. 함수 호출을 표시하면 try?"함수를 던질 수 있다는 것을 알고 있지만 어떤 오류가 정확하게 발생하는지는 신경 쓰지 않습니다"라는 의미입니다. 이렇게하면 함수의 반환 값이 " T" 유형에서 " " 유형으로 변경되고 optional<T>예외가 발생하면 nil을 반환합니다.

(a)와 (d)는 귀하가 요구하는 경우입니다. 함수가 nil을 반환 하고 예외를 던질 수 있다면 어떨까요? 이 경우 반환 값의 유형은 optional<R>일부 유형 R의 경우 " "가되고 (d)이 값을 " optional<optional<R>>"로 변경합니다. 이는 약간 비밀스럽고 이해하기 어렵지만 완전히 괜찮습니다. 그리고 Swift 함수는을 반환 optional<Void>할 수 있으므로 Vd를 반환하는 함수를 던지는 데 (d)를 사용할 수 있습니다.


2

나는 bool TryMethodName(out returnValue)접근 방식을 좋아한다 .

메소드가 성공했는지 여부와 try catch 블록에 예외 발생 메소드를 랩핑하는 이름 지정 일치에 대한 결정적인 표시를 제공합니다.

null 만 반환하면 실패했는지 또는 반환 값이 합당한 지 여부를 알 수 없습니다.

예 :

//throws exceptions when error occurs
Item LoadItem(int id);

//returns false when error occurs
bool TryLoadItem(int id, out Item item)

사용법 예 (out은 초기화되지 않은 참조로 전달 함을 의미 함)

Item i;
if(TryLoadItem(3, i))
{
    Print(i.Name);
}
else
{
    Print("unable to load item :(");
}

미안, 여기서 잃어 버렸어 bool TryMethodName(out returnValue)원래 예에서 " 접근법" 은 어떻게 보 입니까?
donquixote

예를 추가하도록 수정 됨
Ewan

1
따라서 반환 값 대신 참조 기준 매개 변수를 사용하므로 성공에 대한 반환 값이 있습니까? 이 "out"키워드를 지원하는 프로그래밍 언어가 있습니까?
donquixote

2
예, 죄송합니다. 'out'은 c #에만 해당합니다.
Ewan

1
항상 그렇지는 않다. 그러나 항목 3 null이면 어떻게됩니까? 오류가 있었는지 여부를 알 수 없음
Ewan

2

일반적으로 부울 매개 변수는 함수를 두 개로 나눌 수 있음을 나타내며 일반적으로 부울을 전달하지 않고 항상 분할해야합니다.


1
나는 자격이나 뉘앙스를 추가하지 않고 이것을 규칙으로 제외하지 않을 것입니다. 부울을 인수로 제거하면 다른 위치에 if / else 인 경우 바보를 작성해야하므로 변수가 아닌 작성된 코드로 데이터를 인코딩해야합니다.
Gerard

@ 제라드 예를 들어 주시겠습니까?
Max

@Max 부울 변수가있는 경우 b: 호출하는 대신 다른 인수 또는 언어에 삼항 연산자와 퍼스트 클래스 함수 / 방법이있는 경우 와 같이 foo(…, b);작성 if (b) then foo_x(…) else foo_y(…);하고 반복해야 ((b) ? foo_x : foo_y)(…)합니다. 그리고 이것은 심지어 프로그램의 여러 곳에서 반복 될 수도 있습니다.
BlackJack

@BlackJack 잘 그래, 난 당신과 동의 할 수도 있습니다. 나는 예를 생각했다. if (myGrandmaMadeCookies) eatThem() else makeFood()그래, 나는 이것과 같은 다른 기능을 감쌀 것이다 checkIfShouldCook(myGrandmaMadeCookies). 언급 한 뉘앙스가 원래 질문에서와 같이 함수의 작동 방식에 대한 부울을 전달하는지 여부와 관련이 있는지 궁금합니다. 대신 우리는 모델에 대한 정보를 전달합니다. 내가 준 할머니의 예.
Max

1
내 의견에 언급 된 뉘앙스는 "항상해야한다"를 나타냅니다. "a, b 및 c가 적용 가능한 경우 [...]"로 바꾸십시오. 언급 한 "규칙"은 훌륭한 패러다임이지만 유스 케이스에서 매우 변형됩니다.
Gerard

1

Clean Code는 부울 플래그 인수가 호출 사이트에서 불투명하기 때문에 부울 플래그 인수를 피하는 것이 좋습니다.

loadItem(id, true) // does this throw or not? We need to check the docs ...

별도의 방법으로 표현 이름을 사용할 수 있습니다.

loadItemOrNull(id); // meaning is obvious; no need to refer to docs

1

A 또는 B를 사용해야하는 경우 Arseni Mourzenkos answer에 명시된 이유로 두 가지 방법으로 B를 사용 합니다.

그러나 다른 방법 1이 있습니다 . Java에서는이라고 Optional하지만 언어가 아직 유사한 클래스를 제공하지 않는 경우 고유 한 유형을 정의 할 수있는 다른 언어에 동일한 개념을 적용 할 수 있습니다.

다음과 같이 메소드를 정의하십시오.

Optional<Item> loadItem(int id);

당신의 방법에서 당신 Optional.of(item)이 항목을 발견하면 반환하거나 당신 Optional.empty()이하지 않은 경우에 반환 합니다. 당신은 2를 던지지 않으며 결코 돌아 오지 않습니다 null.

메소드의 클라이언트의 경우와 null같지만 누락 된 항목의 경우를 생각해야한다는 큰 차이가 있습니다. 사용자는 결과가 없을 수도 있다는 사실을 단순히 무시할 수 없습니다. Optional명시 적 조치 에서 항목을 가져 오려면 다음 이 필요합니다.

  • loadItem(1).get()NoSuchElementException찾은 항목 을 던지 거나 반환합니다.
  • 또한이 loadItem(1).orElseThrow(() -> new MyException("No item 1"))반환이 경우 사용자 정의 예외를 사용하는 Optional비어 있습니다.
  • loadItem(1).orElse(defaultItem)발견 된 항목 또는 예외를 던지지 않고 전달 된 항목 (또는 defaultItem있을 수도 있음 null)을 반환합니다 .
  • loadItem(1).map(Item::getName)Optional있는 경우 항목 이름과 함께 다시를 반환합니다 .
  • 더 많은 메소드가 있습니다. Javadoc for를Optional 참조하십시오 .

장점은 항목이없고 여전히 단일 메소드제공 하면 클라이언트 코드에 따라 달라집니다 .


(1) 나는이 같은 추측 try?gnasher729s 대답 하지만 스위프트를 모르는로 완전히 나에게 분명하지 않다 그리고 그것은 특정 스위프트에 그래서 언어 기능이 될 것으로 보인다.

2 다른 이유로, 예를 들어 데이터베이스와 통신하는 데 문제가있어 항목이 있는지 여부를 모르는 경우 예외가 발생할 수 있습니다.


0

두 가지 방법을 사용하는 것에 동의하는 또 다른 요점으로, 이는 구현하기가 매우 쉽습니다. PHP의 구문을 모르기 때문에 C #으로 예제를 작성하겠습니다.

public Widget GetWidgetOrNull(int id) {
    // All the lines to get your item, returning null if not found
}

public Widget GetWidget(int id) {
    var widget = GetWidgetOrNull(id);

    if (widget == null) {
        throw new WidgetNotFoundException();
    }

    return widget;
}

0

이 방법으로 두 가지 방법을 선호합니다. 값을 반환한다고 말할뿐만 아니라 정확히 어떤 값을 반환하는지 알려줍니다.

public int GetValue(); // If you can't get the value, you'll throw me an exception
public int GetValueOrDefault(); // If you can't get the value, you'll return me 0, since it's the default for ints

TryDoSomething ()도 잘못된 패턴이 아닙니다.

public bool TryParse(string value, out int parsedValue); // If you return me false, I won't bother checking parsedValue.

나는 데이터베이스 상호 작용에서 전자를 보는 데 더 익숙하고 후자는 구문 분석에 더 많이 사용됩니다.

다른 사람들이 이미 말했듯이 플래그 매개 변수는 일반적으로 코드 냄새입니다 (코드 냄새는 무언가 잘못하고 있다는 것을 의미하지 않으며 잘못했을 가능성이 있습니다). 정확하게 이름을 지정하지만 때로는 해당 플래그를 사용하는 방법을 알기 어려운 뉘앙스가 있으며 메소드 본문 내부를 살펴볼 수 있습니다.


-1

다른 사람들은 달리 제안하지만, 오류를 처리하는 방법을 나타내는 매개 변수가있는 방법을 사용하는 것이 두 가지 사용 시나리오에 대해 별도의 방법을 사용해야하는 것보다 낫습니다. 이 바람직 할 수 있지만에 또한 에 대한 별개의 엔트리 포인트가 래퍼 방법의 코드 중복을 피할 모두 사용 패턴을 제공 할 수있는 진입 점을 가지고, "오류를 반환 오류 표시"용도 대 "오류가 발생합니다." 간단한 예로, 함수가있는 경우 :

Thing getThing(string x);
Thing tryGetThing (string x);

x는 괄호로 묶은 것처럼 작동하는 함수를 원하므로 두 개의 래퍼 함수 세트를 작성해야합니다.

Thing getThingWithBrackets(string x)
{
  getThing("["+x+"]");
}
Thing tryGetThingWithBrackets (string x);
{
  return tryGetThing("["+x+"]");
}

오류를 처리하는 방법에 대한 인수가있는 경우 하나의 랩퍼 만 필요합니다.

Thing getThingWithBrackets(string x, bool returnNullIfFailure)
{
  return getThing("["+x+"]", returnNullIfFailure);
}

랩퍼가 충분히 단순하면 코드 복제가 나쁘지는 않지만 코드를 변경해야 할 경우 코드를 복제하면 변경 사항이 복제됩니다. 추가 인수를 사용하지 않는 진입 점을 추가하도록 선택한 경우에도 :

Thing getThingWithBrackets(string x)
{
   return getThingWithBrackets(x, false);
}
Thing tryGetThingWithBrackets(string x)
{
   return tryGetThingWithBrackets(x, true);
}

하나의 방법으로 모든 "비즈니스 로직"을 유지할 수 있습니다.이 방법은 실패시 수행 할 작업을 나타내는 인수를 허용합니다.


맞습니다. 데코레이터와 래퍼, 심지어 일반적인 구현조차도 두 가지 방법을 사용할 때 약간의 코드 복제가 발생합니다.
donquixote

예외 발생 여부와 상관없이 버전을 별도의 패키지에 넣고 래퍼를 일반화 하시겠습니까?
Will Crawford

-1

예외가 발생했는지 여부를 제어하는 ​​플래그가 없어야한다고 설명하는 모든 답변이 있다고 생각합니다. 그들의 조언은 그 질문을 할 필요가 있다고 느끼는 사람에게 나의 조언이 될 것입니다.

그러나 나는이 규칙에 의미있는 예외가 있음을 지적하고 싶었다. 수백 명의 다른 사람들이 사용할 API 개발을 시작하기에 충분하다면 그러한 플래그를 제공 할 수 있습니다. API를 작성할 때 순수 주의자에게만 쓰는 것이 아닙니다. 당신은 진정한 욕구를 가진 실제 고객에게 글을 쓰고 있습니다. 당신의 일의 일부는 API로 그것들을 만족시키는 것입니다. 그것은 프로그래밍 문제 라기보다는 사회적 문제가되고 때로는 사회적 문제가 스스로 해결책으로서 이상적인 솔루션을 지시하는 경우도있다.

API를 사용하는 두 명의 사용자가 있는데 그 중 하나는 예외를 원하고 다른 하나는 그렇지 않습니다. 이러한 상황이 발생할 수있는 실제 사례는 개발 및 임베디드 시스템에서 모두 사용되는 라이브러리입니다. 개발 환경에서 사용자는 어디에서나 예외를 원할 것입니다. 예기치 않은 상황을 처리하는 데 예외가 많이 사용됩니다. 그러나 실시간 제약 조건을 분석하기가 너무 어렵 기 때문에 많은 임베디드 상황에서 금지되어 있습니다. 실제로 함수를 실행하는 데 걸리는 평균 시간뿐만 아니라 개별 재미에 걸리는 시간을 신경 쓰면 임의의 위치에서 스택을 푸는 아이디어는 매우 바람직하지 않습니다.

나를위한 실제 사례 : 수학 라이브러리. 수학 라이브러리 Vector에 같은 방향으로 단위 벡터를 얻는 것을 지원 하는 클래스 가 있으면 크기로 나눌 수 있어야합니다. 크기가 0이면 0으로 나누기 상황입니다. 개발에서 당신은 이것을 잡기를 원합니다 . 당신은 정말로 0으로 둘러싸인 놀라운 나누기를 원하지 않습니다. 예외를 던지는 것은 매우 인기있는 해결책입니다. 그것은 거의 일어나지 않습니다 (정말 예외적 인 행동입니다). 그렇다면 그것을 알고 싶습니다.

임베디드 플랫폼에서는 이러한 예외를 원하지 않습니다. 확인하는 것이 더 합리적 if (this->mag() == 0) return Vector(0, 0, 0);입니다. 사실, 나는 이것을 실제 코드에서 본다.

이제 사업의 관점에서 생각하십시오. API를 사용하는 두 가지 다른 방법을 가르쳐보십시오.

// Development version - exceptions        // Embeded version - return 0s
Vector right = forward.cross(up);          Vector right = forward.cross(up);
Vector localUp = right.cross(forward);     Vector localUp = right.cross(forward);
Vector forwardHat = forward.unit();        Vector forwardHat = forward.tryUnit();
Vector rightHat = right.unit();            Vector rightHat = right.tryUnit();
Vector localUpHat = localUp.unit()         Vector localUpHat = localUp.tryUnit();

이것은 대부분의 답변에 대한 의견을 만족 시키지만 회사 관점에서는 바람직하지 않습니다. 임베디드 개발자는 한 가지 스타일 의 코딩 을 배워야 하고 개발 개발자는 다른 스타일 을 배워야합니다. 이것은 다소 성 가시고 사람들을 한 가지 사고 방식으로 만듭니다. 잠재적으로 더 나쁜 것은 : 임베디드 버전에 예외 발생 코드가 포함되어 있지 않다는 것을 어떻게 증명할 수 있습니까? 던지는 버전은 값 비싼 Failure Review Board가 찾을 때까지 코드의 어딘가에 숨겨져 쉽게 섞일 수 있습니다.

반면에 예외 처리를 켜거나 끄는 플래그가 있으면 두 그룹 모두 정확히 동일한 스타일의 코딩을 배울 수 있습니다. 실제로, 대부분의 경우 다른 그룹 프로젝트에서 한 그룹의 코드를 사용할 수도 있습니다. 이 코드를 사용하는 경우 unit()실제로 0으로 나누기에서 발생하는 일에 신경 쓰면 테스트를 직접 작성해야합니다. 따라서 나쁜 결과를 얻는 임베디드 시스템으로 이동하면 그 동작은 "정상적"입니다. 이제 비즈니스 관점에서 코더를 한 번 훈련하면 비즈니스의 모든 부분에서 동일한 API와 스타일을 사용합니다.

대부분의 경우 다른 사람들의 조언을 따르고 싶습니다. tryGetUnit()예외를 던지지 않는 함수에 유사하거나 유사한 관용구를 사용 하고 대신 null과 같은 센티넬 값을 반환하십시오. 사용자가 단일 프로그램 내에서 예외 처리 및 비 예외 처리 코드를 나란히 사용할 수 있다고 생각되면 tryGetUnit()표기법을 사용하십시오. 그러나 비즈니스의 현실이 깃발을 사용하는 것이 더 나을 수있는 코너 케이스가 있습니다. 당신이 그 코너 케이스에 자신을 발견하면, 길가에 "규칙"을 던져 두려워하지 마십시오. 그것이 규칙의 목적입니다!

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.