Ist das Erstellen von Unterklassen für bestimmte Instanzen eine schlechte Praxis?


37

Betrachten Sie das folgende Design

public class Person
{
    public virtual string Name { get; }

    public Person (string name)
    {
        this.Name = name;
    }
}

public class Karl : Person
{
    public override string Name
    {
        get
        {
            return "Karl";
        }
    }
}

public class John : Person
{
    public override string Name
    {
        get
        {
            return "John";
        }
    }
}

Denken Sie, dass hier etwas nicht stimmt? Für mich sollten Karl- und John-Klassen nur Instanzen anstelle von Klassen sein, da sie genau die gleichen sind wie:

Person karl = new Person("Karl");
Person john = new Person("John");

Warum sollte ich neue Klassen erstellen, wenn Instanzen ausreichen? Die Klassen fügen der Instanz nichts hinzu.


17
Leider finden Sie dies in vielen Produktionscodes. Räumen Sie auf, wenn Sie können.
Adam Zuckerman

14
Dies ist ein großartiges Design - wenn ein Entwickler sich für jede Änderung von Geschäftsdaten unentbehrlich machen möchte und keine Probleme hat, rund um die Uhr verfügbar zu sein ;-)
Doc Brown,

1
Hier ist eine Art verwandte Frage, bei der das OP das Gegenteil von konventioneller Weisheit zu glauben scheint. programmers.stackexchange.com/questions/253612/…
Dunk

11
Entwerfen Sie Ihre Klassen in Abhängigkeit von Verhalten und nicht von Daten. Für mich fügen die Unterklassen der Hierarchie kein neues Verhalten hinzu.
Songo

2
Dies ist bei anonymen Unterklassen (z. B. Ereignishandlern) in Java weit verbreitet, bevor Lambdas zu Java 8 gelangten (was mit unterschiedlicher Syntax dasselbe ist).
Marczellm

Antworten:


77

Es ist nicht erforderlich, für jede Person spezifische Unterklassen zu haben.

Du hast recht, das sollten stattdessen Instanzen sein.

Ziel der Unterklassen: Erweiterung der Elternklassen

Unterklassen werden verwendet, um die von der übergeordneten Klasse bereitgestellten Funktionen zu erweitern . Zum Beispiel haben Sie vielleicht:

  • Eine Elternklasse, Batterydie Power()etwas kann und eine VoltageEigenschaft haben kann,

  • Und eine Unterklasse RechargeableBattery, die Power()und erben kann Voltage, aber auch Recharge()d sein kann.

Beachten Sie, dass Sie eine Instanz von RechargeableBatteryclass als Parameter an jede Methode übergeben können, die a Batteryals Argument akzeptiert . Dies nennt man Liskov-Substitutionsprinzip , eines der fünf SOLID-Prinzipien. In ähnlicher Weise kann ich in der Praxis zwei wiederaufladbare AA-Batterien einsetzen, wenn mein MP3-Player zwei AA-Batterien akzeptiert.

Beachten Sie, dass es manchmal schwierig ist, zu bestimmen, ob Sie ein Feld oder eine Unterklasse verwenden müssen, um einen Unterschied zwischen etwas darzustellen. Wenn Sie zum Beispiel mit AA-, AAA- und 9-Volt-Batterien umgehen müssen, würden Sie drei Unterklassen erstellen oder eine Aufzählung verwenden? „Ersetzen Sie die Unterklasse durch Felder“ in Refactoring von Martin Fowler, Seite 241, gibt Ihnen möglicherweise Anregungen und Hinweise zum Wechseln von einer zur anderen.

In Ihrem Beispiel, Karlund Johnerweitern Sie nichts, noch bieten sie einen zusätzlichen Wert: Sie können die gleiche Funktionalität mit Personclass direkt haben. Mehr Codezeilen ohne zusätzlichen Wert zu haben, ist niemals gut.

Ein Beispiel für einen Business Case

Was könnte ein Geschäftsfall sein, bei dem es tatsächlich sinnvoll wäre, eine Unterklasse für eine bestimmte Person zu erstellen?

Angenommen, wir erstellen eine Anwendung, mit der Personen verwaltet werden, die in einem Unternehmen arbeiten. Die Anwendung verwaltet auch Berechtigungen, sodass die Buchhalterin Helen nicht auf das SVN-Repository zugreifen kann, die beiden Programmierer Thomas und Mary jedoch nicht auf buchhaltungsbezogene Dokumente.

Jimmy, der Big Boss (Gründer und CEO des Unternehmens), hat ganz bestimmte Privilegien, die niemand hat. Er kann beispielsweise das gesamte System herunterfahren oder eine Person entlassen. Du hast die Idee.

Das schlechteste Modell für eine solche Anwendung sind Klassen wie:

          Das Klassendiagramm zeigt die Klassen Helen, Thomas, Mary und Jimmy sowie deren gemeinsame Eltern: die abstrakte Personenklasse.

weil Code-Duplizierung sehr schnell auftreten wird. Selbst im einfachen Beispiel von vier Mitarbeitern duplizieren Sie den Code zwischen den Klassen von Thomas und Mary. Dadurch werden Sie aufgefordert, eine gemeinsame übergeordnete Klasse zu erstellen Programmer. Da Sie möglicherweise auch mehrere Buchhalter haben, werden Sie wahrscheinlich auch eine AccountantKlasse erstellen .

          Das Klassendiagramm zeigt eine abstrakte Person mit drei Kindern: einen abstrakten Buchhalter, einen abstrakten Programmierer und Jimmy.  Buchhalter hat eine Unterklasse Helen und Programmierer hat zwei Unterklassen: Thomas und Mary.

Sie bemerken jetzt, dass es Helennicht sehr nützlich ist, die Klasse zu haben , und auch nicht, Thomasund Mary: Der größte Teil Ihres Codes funktioniert sowieso auf der höheren Ebene - auf der Ebene der Buchhalter, Programmierer und Jimmy. Dem SVN-Server ist es egal, ob es Thomas oder Mary ist, die auf das Protokoll zugreifen müssen. Er muss nur wissen, ob es sich um einen Programmierer oder einen Buchhalter handelt.

if (person is Programmer)
{
    this.AccessGranted = true;
}

So entfernen Sie am Ende die Klassen, die Sie nicht verwenden:

          Das Klassendiagramm enthält die Unterklassen Accountant, Programmer und Jimmy mit der gemeinsamen übergeordneten Klasse: abstract Person.

"Aber ich kann Jimmy so lassen, wie er ist, denn es würde immer nur einen CEO geben, einen großen Boss - Jimmy", denken Sie. Darüber hinaus wird Jimmy häufig in Ihrem Code verwendet, was tatsächlich eher so aussieht, und nicht wie im vorherigen Beispiel:

if (person is Jimmy)
{
    this.GiveUnrestrictedAccess(); // Because Jimmy should do whatever he wants.
}
else if (person is Programmer)
{
    this.AccessGranted = true;
}

Das Problem bei diesem Ansatz ist, dass Jimmy immer noch von einem Bus angefahren werden kann und es einen neuen CEO geben würde. Oder der Verwaltungsrat entscheidet, dass Mary so großartig ist, dass sie eine neue CEO sein sollte, und Jimmy würde zum Verkäufer degradiert. Jetzt müssen Sie Ihren gesamten Code durchgehen und alles ändern.


6
@DocBrown: Ich bin oft überrascht zu sehen, dass kurze Antworten, die nicht falsch, aber nicht tief genug sind, eine viel höhere Punktzahl haben als Antworten, die das Thema ausführlich erläutern. Wahrscheinlich lesen zu viele Leute nicht gern viel, deshalb sehen sehr kurze Antworten für sie attraktiver aus. Trotzdem habe ich meine Antwort bearbeitet, um zumindest eine Erklärung zu liefern.
Arseni Mourzenko

3
Kurze Antworten werden in der Regel zuerst gepostet. Frühere Beiträge erhalten mehr Stimmen.
Tim B

1
@TimB: Normalerweise beginne ich mit mittelgroßen Antworten und bearbeite sie dann so, dass sie sehr vollständig sind (hier ein Beispiel , da es wahrscheinlich etwa fünfzehn Revisionen gab, von denen nur vier angezeigt wurden). Ich bemerkte, dass je länger die Antwort im Laufe der Zeit wird, desto weniger Gegenstimmen erhält sie; Eine ähnliche Frage, die kurz bleibt, zieht mehr positive Stimmen an.
Arseni Mourzenko

Stimmen Sie mit @DocBrown überein. Zum Beispiel würde eine gute Antwort so etwas wie "Unterklasse für Verhalten , nicht für Inhalt" sagen und auf einen Verweis verweisen, den ich in diesem Kommentar nicht tun werde.
user949300

2
Falls im letzten Absatz nicht klar war: Auch wenn Sie nur eine Person mit uneingeschränktem Zugriff haben, sollten Sie diese Person als Instanz einer separaten Klasse definieren, die den uneingeschränkten Zugriff implementiert. Das Überprüfen auf die Person selbst führt zu einer Code-Daten-Interaktion und kann eine ganze Dose Würmer öffnen. Was passiert zum Beispiel, wenn der Verwaltungsrat beschließt, ein Triumvirat zum CEO zu ernennen? Wenn Sie keine Klasse "Uneingeschränkt" hatten, müssten Sie nicht nur den vorhandenen CEO aktualisieren, sondern auch 2 weitere Prüfungen für die anderen Mitglieder hinzufügen. Wenn Sie es hatten, setzen Sie sie einfach auf "Uneingeschränkt".
Nzall

15

Es ist albern, diese Art von Klassenstruktur zu verwenden, nur um Details zu variieren, bei denen es sich offensichtlich um einstellbare Felder für eine Instanz handeln sollte. Dies ist jedoch speziell für Ihr Beispiel.

Es Personist mit ziemlicher Sicherheit eine schlechte Idee, verschiedene Klassen zu erstellen. Sie müssen Ihr Programm ändern und jedes Mal neu kompilieren, wenn eine neue Person Ihre Domain betritt. Dies bedeutet jedoch nicht, dass das Erben von einer vorhandenen Klasse, sodass eine andere Zeichenfolge zurückgegeben wird, manchmal nicht sinnvoll ist.

Der Unterschied besteht darin, wie Sie erwarten, dass die von dieser Klasse dargestellten Entitäten variieren. Es wird fast immer davon ausgegangen, dass Benutzer kommen und gehen, und von fast jeder seriösen Anwendung, die sich mit Benutzern befasst, wird erwartet, dass sie Benutzer zur Laufzeit hinzufügen und entfernen kann. Wenn Sie jedoch Dinge wie unterschiedliche Verschlüsselungsalgorithmen modellieren, ist die Unterstützung eines neuen wahrscheinlich ohnehin eine große Änderung, und es ist sinnvoll, eine neue Klasse zu erfinden, deren myName()Methode einen anderen String zurückgibt (und deren perform()Methode vermutlich etwas anderes tut).


12

Warum sollte ich neue Klassen erstellen, wenn Instanzen ausreichen?

In den meisten Fällen würden Sie nicht. Ihr Beispiel ist wirklich ein guter Fall, in dem dieses Verhalten keinen echten Mehrwert bringt.

Es verstößt auch gegen das Open Closed-Prinzip , da die Unterklassen im Grunde keine Erweiterung sind, sondern das Innenleben der Eltern verändern. Darüber hinaus ist der öffentliche Konstruktor des übergeordneten Elements jetzt in den Unterklassen irritierend und die API wurde somit weniger verständlich.

Manchmal ist es jedoch praktischer und weniger zeitaufwendig, nur ein übergeordnetes Element mit einem komplexen Konstruktor in Unterklassen zu unterteilen, wenn Sie immer nur eine oder zwei spezielle Konfigurationen haben, die häufig im gesamten Code verwendet werden. In solchen Sonderfällen kann ich an einem solchen Ansatz nichts Falsches erkennen. Nennen wir es Konstruktor currying j / k


Ich bin nicht sicher, ob ich dem OCP-Paragraphen zustimme. Wenn eine Klasse einen Eigenschaftensetzer ihren Nachkommen aussetzt, sollte die Eigenschaft - vorausgesetzt, sie folgt guten Entwurfsprinzipien - nicht Teil ihres Innenlebens sein
Ben Aaronson

@BenAaronson Solange das Verhalten der Eigenschaft selbst nicht geändert wird, verstößt du nicht gegen OCP, aber hier tun sie es. Natürlich können Sie diese Eigenschaft in ihrem Innenleben verwenden. Aber Sie sollten bestehendes Verhalten nicht ändern! Sie sollten das Verhalten nur erweitern, nicht aber durch Vererbung ändern. Natürlich gibt es zu jeder Regel Ausnahmen.
Falcon

1
So ist jede Verwendung von virtuellen Mitgliedern eine OCP-Verletzung?
Ben Aaronson

3
@Falcon Es ist nicht erforderlich, eine neue Klasse abzuleiten, um sich wiederholende komplexe Konstruktoraufrufe zu vermeiden. Erstellen Sie einfach Fabriken für die gängigen Fälle und parametrisieren Sie die kleinen Variationen.
Keen

@BenAaronson Ich würde sagen, virtuelle Mitglieder mit einer tatsächlichen Implementierung sind eine solche Verletzung. Abstrakte Mitglieder oder Methoden, die nichts tun, sind gültige Erweiterungspunkte. Wenn Sie sich den Code einer Basisklasse ansehen, wissen Sie im Grunde, dass er unverändert ausgeführt wird, anstatt dass jede nicht endgültige Methode ein Kandidat dafür ist, durch etwas ganz anderes ersetzt zu werden. Oder anders ausgedrückt: Die Art und Weise, wie sich eine vererbende Klasse auf das Verhalten der Basisklasse auswirken kann, ist explizit und auf "additiv" beschränkt.
Millimoose

8

Wenn dies das Ausmaß der Praxis ist, dann stimme ich zu, dass dies eine schlechte Praxis ist.

Wenn sich John und Karl unterschiedlich verhalten, ändern sich die Dinge ein wenig. Es könnte sein, dass persones eine Methode gibt, cleanRoom(Room room)bei der sich herausstellt, dass John ein großartiger Mitbewohner ist und sehr effektiv putzt, aber Karl putzt das Zimmer nicht und nicht sehr viel.

In diesem Fall ist es sinnvoll, sie als eigene Unterklasse mit dem definierten Verhalten zu definieren. Es gibt bessere Wege, um dasselbe zu erreichen (zum Beispiel eine CleaningBehaviorKlasse), aber zumindest ist dies kein schrecklicher Verstoß gegen die OO-Prinzipien.


Kein anderes Verhalten, nur andere Daten. Vielen Dank.
Ignacio Soler Garcia

6

In einigen Fällen wissen Sie, dass es nur eine bestimmte Anzahl von Instanzen gibt, und dann wäre es in Ordnung (obwohl meiner Meinung nach hässlich), diesen Ansatz zu verwenden. Es ist besser, eine Aufzählung zu verwenden, wenn die Sprache dies zulässt.

Java Beispiel:

public enum Person
{
    KARL("Karl"),
    JOHN("John")

    private final String name;

    private Person(String name)
    {
        this.name = name;
    }

    public String getName()
    {
        return this.name;
    }
}

6

Viele Leute haben bereits geantwortet. Ich dachte, ich würde meine persönliche Perspektive geben.


Ich habe einmal an einer App gearbeitet (und mache es immer noch), mit der Musik erstellt wird.

Die App hatte eine abstrakte ScaleKlasse mit mehreren Unterklassen: CMajor, DMinoretc. Scalesah so etwas wie so:

public abstract class Scale {
    protected Note[] notes;
    public Scale() {
        loadNotes();
    }
    // .. some other stuff ommited
    protected abstract void loadNotes(); /* subclasses put notes in the array
                                          in this method. */
}

Die Musikgeneratoren arbeiteten mit einer bestimmten ScaleInstanz, um Musik zu generieren. Der Benutzer würde eine Skala aus einer Liste auswählen, aus der Musik erzeugt werden soll.

Eines Tages kam mir eine coole Idee in den Sinn: Warum nicht dem Benutzer erlauben, seine eigenen Skalen zu erstellen? Der Benutzer wählt Notizen aus einer Liste aus, drückt eine Taste, und der Liste der verfügbaren Skalen wird eine neue Skala hinzugefügt.

Aber das konnte ich nicht. Das lag daran, dass alle Maßstäbe bereits zur Kompilierungszeit festgelegt sind - da sie als Klassen ausgedrückt werden. Dann traf es mich:

Es ist oft intuitiv, in Über- und Unterklassen zu denken. Fast alles kann durch dieses System ausgedrückt werden: Ober- Personund Unterklassen Johnund Mary; Ober- Carund Unterklassen Volvound Mazda; Superklasse Missileund Unterklassen SpeedRocked, LandMineund TrippleExplodingThingy.

Es ist sehr natürlich, auf diese Weise zu denken, besonders für die Person, die für OO relativ neu ist.

Wir sollten uns jedoch immer daran erinnern, dass Klassen Vorlagen sind und dass Objekte in diese Vorlagen eingegossen werden . Sie können beliebige Inhalte in die Vorlage einfügen und so unzählige Möglichkeiten schaffen.

Es ist nicht die Aufgabe der Unterklasse, die Vorlage auszufüllen. Es ist die Aufgabe des Objekts. Die Aufgabe der Unterklasse besteht darin, die eigentliche Funktionalität hinzuzufügen oder die Vorlage zu erweitern .

Aus diesem Grund hätte ich eine konkrete ScaleKlasse mit einem Note[]Feld erstellen und Objekte diese Vorlage ausfüllen lassen sollen . möglicherweise durch den Konstruktor oder so. Und irgendwann tat ich es auch.

Denken Sie jedes Mal, wenn Sie eine Vorlage in einer Klasse entwerfen (z. B. ein leeres Note[]Element, das ausgefüllt werden muss, oder ein String nameFeld, dem ein Wert zugewiesen werden muss), daran, dass es Aufgabe der Objekte dieser Klasse ist, die Vorlage auszufüllen ( oder möglicherweise diejenigen, die diese Objekte erstellen). Unterklassen sollen Funktionen hinzufügen, nicht Vorlagen ausfüllen.


Sie könnten versucht sein, eine "Superklasse Person, Unterklassen Johnund Mary" Art von System zu erstellen , wie Sie es getan haben, weil Sie die Formalität mögen, die Ihnen dies bringt.

Auf diese Weise können Sie einfach sagen Person p = new Mary(), anstatt Person p = new Person("Mary", 57, Sex.FEMALE). Es macht die Dinge organisierter und strukturierter. Wie wir bereits sagten, ist es kein guter Ansatz, für jede Kombination von Daten eine neue Klasse zu erstellen, da dies den Code für nichts aufbläht und Sie in Bezug auf die Laufzeitfähigkeiten einschränkt.

Hier ist eine Lösung: Verwenden Sie eine Basisfabrik, möglicherweise sogar eine statische. Wie so:

public final class PersonFactory {
    private PersonFactory() { }

    public static Person createJohn(){
        return new Person("John", 40, Sex.MALE);
    }

    public static Person createMary(){
        return new Person("Mary", 57, Sex.FEMALE);
    }

    // ...
}

Auf diese Weise können Sie leicht die Voreinstellungen verwenden, die mit dem Programm geliefert werden Person mary = PersonFactory.createMary(), aber Sie behalten sich auch das Recht vor, neue Personen dynamisch zu entwerfen, zum Beispiel für den Fall, dass Sie dem Benutzer dies erlauben möchten . Z.B:

// .. requesting the user for input ..
String name = // user input
int age = // user input
Sex sex = // user input, interpreted
Person newPerson = new Person(name, age, sex);

Oder noch besser: Mach so etwas:

public final class PersonFactory {
    private PersonFactory() { }

    private static Map<String, Person> persons = new HashMap<>();
    private static Map<String, PersonData> personBlueprints = new HashMap<>();

    public static void addPerson(Person person){
        persons.put(person.getName(), person);
    }

    public static Person getPerson(String name){
        return persons.get(name);
    }

    public static Person createPerson(String blueprintName){
        PersonData data = personBlueprints.get(blueprintName);
        return new Person(data.name, data.age, data.sex);
    }

    // .. or, alternative to the last method
    public static Person createPerson(String personName){
        Person blueprint = persons.get(personName);
        return new Person(blueprint.getName(), blueprint.getAge(), blueprint.getSex());
    }

}

public class PersonData {
    public String name;
    public int age;
    public Sex sex;

    public PersonData(String name, int age, Sex sex){
        this.name = name;
        this.age = age;
        this.sex = sex;
    }
}

Ich wurde weggetragen. Ich denke, Sie haben die Idee.


Unterklassen sind nicht dazu gedacht, die von ihren Oberklassen festgelegten Vorlagen auszufüllen. Unterklassen sollen Funktionalität hinzufügen . Objekte sollen die Vorlagen ausfüllen, dafür sind sie da.

Sie sollten nicht für jede mögliche Kombination von Daten eine neue Klasse erstellen. (Genau wie ich nicht Scalefür jede mögliche Kombination von Notes eine neue Unterklasse hätte erstellen sollen).

Hier ist eine Richtlinie: Überlegen Sie beim Erstellen einer neuen Unterklasse, ob sie neue Funktionen hinzufügt, die in der Oberklasse nicht vorhanden sind. Wenn die Antwort auf diese Frage "Nein" lautet, versuchen Sie möglicherweise, die Vorlage der Superklasse auszufüllen. Erstellen Sie in diesem Fall einfach ein Objekt. (Und evtl. eine Factory mit 'Presets', um das Leben leichter zu machen).

Hoffentlich hilft das.


Dies ist ein hervorragendes Beispiel, vielen Dank.
Ignacio Soler Garcia

@ SoMoS Ich bin froh, dass ich helfen konnte.
Aviv Cohn

2

Dies ist eine Ausnahme von den 'sollte Instanzen sein' hier - obwohl etwas extrem.

Wenn Sie möchten, dass der Compiler Berechtigungen für den Zugriff auf Methoden erzwingt, basierend darauf, ob es sich um Karl oder John handelt (in diesem Beispiel), anstatt von der Methodenimplementierung zu überprüfen, für welche Instanz übergeben wurde, können Sie verschiedene Klassen verwenden. Durch die Durchsetzung verschiedene Klassen statt Instanziierung dann machen Sie Differenzierung Kontrollen zwischen ‚Karl‘ und ‚John‘ möglich bei der Kompilierung , anstatt Laufzeit und dies könnte nützlich sein - vor allem in einem Sicherheitskontext , wo der Compiler als Laufzeit traut mehr kann Code-Checks von (zum Beispiel) externen Bibliotheken.


2

Es wird als schlechte Praxis angesehen, Code dupliziert zu haben .

Dies liegt zumindest teilweise daran , dass Codezeilen und damit die Größe der Codebasis mit den Wartungskosten und Fehlern korrelieren .

Hier ist die relevante Logik des gesunden Menschenverstands für mich zu diesem bestimmten Thema:

1) Wenn ich dies ändern müsste, wie viele Orte müsste ich besuchen? Hier ist weniger besser.

2) Gibt es einen Grund, warum dies so gemacht wird? In Ihrem Beispiel könnte es keinen Grund dafür geben, aber in einigen Beispielen könnte es einen Grund dafür geben. Eine, die mir auf den ersten Blick einfällt, ist in einigen Frameworks wie Early Grails, in denen die Vererbung mit einigen Plugins für GORM nicht unbedingt gut funktioniert hat. Dünn gesät.

3) Ist es auf diese Weise sauberer und verständlicher? Wiederum ist dies nicht in Ihrem Beispiel der Fall - aber es gibt einige Vererbungsmuster, die sich eher dahingehend erstrecken, dass getrennte Klassen tatsächlich leichter zu pflegen sind (bestimmte Verwendungszwecke des Befehls oder Strategiemuster fallen mir ein).


1
ob es schlecht ist oder nicht, ist nicht in Stein gemeißelt. Es gibt beispielsweise Szenarien, in denen der zusätzliche Aufwand für die Objekterstellung und Methodenaufrufe die Leistung so beeinträchtigen kann, dass die Anwendung die Spezifikationen nicht mehr erfüllt.
Jwenting

Siehe Punkt 2. Sicher gibt es Gründe warum, gelegentlich. Das ist der Grund, warum Sie fragen müssen, bevor Sie den Code eines anderen ausnehmen.
Dcgregorya

0

Es gibt einen Anwendungsfall: das Bilden einer Referenz auf eine Funktion oder Methode als reguläres Objekt.

Sie können dies in Java GUI-Code viel sehen:

ActionListener a = new ActionListener() {
    public void actionPerformed(ActionEvent arg0) {
        /* ... */
    }
};

Der Compiler hilft Ihnen dabei, indem er eine neue anonyme Unterklasse erstellt.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.