SOLIDNY, unikając domen anemicznych, wstrzykiwanie zależności?


33

Chociaż może to być pytanie agnostyczne w języku programowania, interesują mnie odpowiedzi dotyczące ekosystemu .NET.

Oto scenariusz: załóżmy, że musimy opracować prostą aplikację konsolową dla administracji publicznej. Aplikacja dotyczy podatku od pojazdów. Mają (tylko) następujące reguły biznesowe:

1.a) Jeśli pojazd jest samochodem, a ostatni podatek zapłacił jego właściciel 30 dni temu, wówczas właściciel musi zapłacić ponownie.

1.b) Jeśli pojazd jest motocyklem, a ostatni podatek zapłacony przez właściciela był 60 dni temu, wówczas właściciel musi zapłacić ponownie.

Innymi słowy, jeśli masz samochód, musisz płacić co 30 dni lub jeśli masz motocykl, musisz płacić co 60 dni.

W przypadku każdego pojazdu w systemie aplikacja powinna przetestować te reguły i wydrukować te pojazdy (numer rejestracyjny i dane właściciela), które ich nie spełniają.

Chcę to:

2.a) Zgodne z zasadami SOLID (szczególnie zasada otwarta / zamknięta).

To, czego nie chcę, to (tak myślę):

2.b) Domena anemiczna, dlatego logika biznesowa powinna wchodzić do jednostek biznesowych.

Zacząłem od tego:

public class Person
// You wanted a banana but what you got was a gorilla holding the banana and the entire jungle.
{
    public string Name { get; set; }

    public string Surname { get; set; }
}

public abstract class Vehicle
{
    public string PlateNumber { get; set; }

    public Person Owner { get; set; }

    public DateTime LastPaidTime { get; set; }

    public abstract bool HasToPay();
}

public class Car : Vehicle
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 30;
    }
}

public class Motorbike : Vehicle
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 60;
    }
}

public class PublicAdministration
{
    public IEnumerable<Vehicle> GetVehiclesThatHaveToPay()
    {
        return this.GetAllVehicles().Where(vehicle => vehicle.HasToPay());
    }

    private IEnumerable<Vehicle> GetAllVehicles()
    {
        throw new NotImplementedException();
    }
}

class Program
{
    static void Main(string[] args)
    {
        PublicAdministration administration = new PublicAdministration();
        foreach (var vehicle in administration.GetVehiclesThatHaveToPay())
        {
            Console.WriteLine("Plate number: {0}\tOwner: {1}, {2}", vehicle.PlateNumber, vehicle.Owner.Surname, vehicle.Owner.Name);
        }
    }
}

2.a: Zagwarantowana jest zasada otwarcia / zamknięcia; jeśli chcą podatku rowerowego, który po prostu dziedziczysz od Vehicle, wówczas zastępujesz metodę HasToPay i gotowe. Zasada otwarta / zamknięta spełniona poprzez proste dziedziczenie.

„Problemy” to:

3.a) Dlaczego pojazd musi wiedzieć, czy musi zapłacić? Czy nie dotyczy to administracji publicznej? Jeśli przepisy administracji publicznej dotyczące zmiany podatku samochodowego, dlaczego samochód musi się zmienić? Myślę, że powinieneś zapytać: „dlaczego więc umieściłeś metodę HasToPay w pojeździe?”, A odpowiedź brzmi: ponieważ nie chcę testować typu pojazdu (typof) w administracji publicznej. Czy widzisz lepszą alternatywę?

3.b) Może zapłata podatku jest w końcu problemem dla pojazdu, a może mój początkowy projekt Person-Vehicle-Car-Motor-Motor-PublicAdministration jest po prostu błędny, lub potrzebuję filozofa i lepszego analityka biznesowego. Rozwiązaniem może być: przenieś metodę HasToPay do innej klasy, którą możemy nazwać TaxPayment. Następnie tworzymy dwie klasy pochodne TaxPayment; CarTaxPayment i MotorbikeTaxPayment. Następnie dodajemy abstrakcyjną właściwość Payment (typu TaxPayment) do klasy Vehicle i zwracamy odpowiednią instancję TaxPayment z klas Car i Motorbike:

public abstract class TaxPayment
{
    public abstract bool HasToPay();
}

public class CarTaxPayment : TaxPayment
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 30;
    }
}

public class MotorbikeTaxPayment : TaxPayment
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 60;
    }
}

public abstract class Vehicle
{
    public string PlateNumber { get; set; }

    public Person Owner { get; set; }

    public DateTime LastPaidTime { get; set; }

    public abstract TaxPayment Payment { get; }
}

public class Car : Vehicle
{
    private CarTaxPayment payment = new CarTaxPayment();
    public override TaxPayment Payment
    {
        get { return this.payment; }
    }
}

public class Motorbike : Vehicle
{
    private MotorbikeTaxPayment payment = new MotorbikeTaxPayment();
    public override TaxPayment Payment
    {
        get { return this.payment; }
    }
}

W ten sposób wywołujemy stary proces:

    public IEnumerable<Vehicle> GetVehiclesThatHaveToPay()
    {
        return this.GetAllVehicles().Where(vehicle => vehicle.Payment.HasToPay());
    }

Ale teraz ten kod nie będzie się kompilował, ponieważ w CarTaxPayment / MotorbikeTaxPayment / TaxPayment nie ma członka LastPaidTime. Zaczynam widzieć CarTaxPayment i MotorbikeTaxPayment bardziej jak „implementacje algorytmów” niż podmioty gospodarcze. Jakoś teraz te „algorytmy” potrzebują wartości LastPaidTime. Jasne, możemy przekazać wartość konstruktorowi TaxPayment, ale to naruszyłoby hermetyzację pojazdu lub obowiązki, czy jakkolwiek nazywają to ewangeliści OOP, prawda?

3.c) Załóżmy, że mamy już obiekt Entity Framework ObjectContext (który korzysta z naszych obiektów domeny; Osoba, Pojazd, Samochód, Motocykl, Administracja publiczna). Jak byś zrobił, aby uzyskać odwołanie ObjectContext z metody PublicAdministration.GetAllVehicles, a tym samym wdrożyć funkcjonalność?

3.d) Jeśli potrzebujemy również tego samego odwołania ObjectContext dla CarTaxPayment.HasToPay, ale innego dla MotorbikeTaxPayment.HasToPay, kto jest odpowiedzialny za „wstrzykiwanie” lub w jaki sposób przekazałbyś te odniesienia do klas płatności? W takim przypadku, unikając anemicznej domeny (a tym samym żadnych obiektów usługowych), nie doprowadzasz nas do katastrofy?

Jakie są opcje projektowania dla tego prostego scenariusza? Najwyraźniej przykłady, które pokazałem, są „zbyt skomplikowane” dla łatwego zadania, ale jednocześnie chodzi o przestrzeganie zasad SOLID i zapobieganie domenom anemicznym (których zniszczenie zlecono).

Odpowiedzi:


15

Powiedziałbym, że ani osoba, ani pojazd nie powinny wiedzieć, czy płatność jest należna.

ponowne sprawdzenie domeny problemowej

Jeśli spojrzysz na problematyczną domenę „osoby posiadające samochody”: Aby kupić samochód, masz jakąś umowę, która przenosi własność z jednej osoby na drugą, ani samochód, ani osoba nie zmieniają się w tym procesie. W twoim modelu zupełnie tego brakuje.

eksperyment myślowy

Zakładając, że istnieje małżeństwo, mężczyzna jest właścicielem samochodu i płaci podatki. Teraz mężczyzna umiera, jego żona dziedziczy samochód i zapłaci podatki. Jednak wasze talerze pozostaną takie same (przynajmniej w moim kraju będą). To wciąż ten sam samochód! Powinieneś być w stanie modelować to w swojej domenie.

=> Własność

Samochód, który nie jest własnością nikogo, jest nadal samochodem, ale nikt nie będzie płacił podatków. W rzeczywistości nie płacisz podatków za samochód, ale za przywilej posiadania samochodu . Tak więc moja propozycja brzmiałaby:

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

    public string Surname { get; set; }

    public List<Ownership> getOwnerships();

    public List<Vehicle> getVehiclesOwned();
}

public abstract class Vehicle
{
    public string PlateNumber { get; set; }

    public Ownership currentOwnership { get; set; }

}

public class Car : Vehicle {}

public class Motorbike : Vehicle {}

public abstract class Ownership
{
    public Ownership(Vehicle vehicle, Owner owner);

    public Vehicle Vehicle { get;}

    public Owner CurrentOwner { get; set; }

    public abstract bool HasToPay();

    public DateTime LastPaidTime { get; set; }

   //Could model things like payment history or owner history here as well
}

public class CarOwnership : Ownership
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 30;
    }
}

public class MotorbikeOwnership : Ownership
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 60;
    }
}

public class PublicAdministration
{
    public IEnumerable<Ownership> GetVehiclesThatHaveToPay()
    {
        return this.GetAllOwnerships().Where(HasToPay());
    }

}

Jest to dalekie od idealnego i prawdopodobnie nawet nie zdalnie poprawnego kodu C #, ale mam nadzieję, że masz pomysł (i ktoś może to wyczyścić). Jedynym minusem jest to, że prawdopodobnie potrzebujesz fabryki lub czegoś, aby stworzyć poprawne CarOwnershipmiędzy Cara aPerson


Myślę, że sam pojazd powinien rejestrować zapłacone podatki, a osoby powinny rejestrować podatki zapłacone za wszystkie swoje pojazdy. Kod podatkowy można zapisać tak, aby jeśli podatek od samochodu został zapłacony 1 czerwca i został sprzedany 10 czerwca, nowy właściciel może zostać obciążony podatkiem 10 czerwca, 1 lipca lub 10 lipca (a nawet jeśli jest to podatek obecnie jeden sposób może się zmienić). Jeśli reguła 30-dniowa jest stała dla samochodu, nawet przez zmiany własności, ale samochody, których nikt nie jest winien, nie mogą płacić podatków, to sugerowałbym, że w przypadku zakupu samochodu, który był nieposiadany przez 30 lub więcej dni, płatność podatku ...
supercat

... zostać zarejestrowanym od fikcyjnej osoby, która nie była właścicielem samochodu, tak że w momencie sprzedaży zobowiązanie podatkowe wyniesie zero lub trzydzieści dni, niezależnie od tego, jak długo pojazd był nie posiadany.
supercat,

4

Myślę, że w takiej sytuacji, w której chcesz, aby reguły biznesowe istniały poza obiektami docelowymi, testowanie typu jest więcej niż dopuszczalne.

Oczywiście, w wielu sytuacjach powinieneś użyć patern Strategii i pozwolić obiektom same sobie poradzić, ale nie zawsze jest to pożądane.

W prawdziwym świecie urzędnik sprawdzałby typ pojazdu i na tej podstawie sprawdzał dane podatkowe. Dlaczego twoje oprogramowanie nie powinno stosować się do tej samej reguły busienss?


Ok, powiedz, że mnie przekonałeś, a dzięki metodzie GetVehiclesThatHaveToPay sprawdzamy typ pojazdu. Kto powinien być odpowiedzialny za LastPaidTime? Czy LastPaidTime dotyczy pojazdu, czy administracji publicznej? Bo jeśli to pojazd, to czy logika biznesowa nie powinna znajdować się w pojeździe? Co możesz mi powiedzieć o pytaniu 3.c?

@DavidRobertJones - Najlepiej byłoby wyszukać ostatnią płatność w dzienniku historii płatności na podstawie prawa jazdy. Płatność podatku jest kwestią podatkową, a nie kwestią pojazdu.
Erik Funkenbusch

5
@DavidRobertJones Myślę, że mylisz się z własną metaforą. To, co masz, nie jest rzeczywistym pojazdem, który musi wykonywać wszystkie czynności, które robi pojazd. Zamiast tego nazwałeś, dla uproszczenia, pojazdem podatkowym (lub czymś podobnym) pojazdem. Twoja cała domena to podatki. Nie martw się tym, co robią rzeczywiste pojazdy. I, jeśli to konieczne, porzuć metaforę i wymyśl bardziej odpowiednią.

3

To, czego nie chcę, to (tak myślę):

2.b) Domena anemiczna, co oznacza logikę biznesową wewnątrz podmiotów gospodarczych.

Masz to do tyłu. Domena anemiczna to taka, której logika znajduje się poza podmiotami gospodarczymi. Istnieje wiele powodów, dla których stosowanie dodatkowych klas do implementacji logiki jest złym pomysłem; i tylko kilka powodów, dla których powinieneś to zrobić.

Stąd powód, dla którego nazywa się to anemiczne: klasa nie ma w tym nic…

Co bym zrobił:

  1. Zmień swoją abstrakcyjną klasę pojazdu na interfejs.
  2. Dodaj interfejs ITaxPayment, który pojazdy muszą wdrożyć. Niech Twój HasToPay się tutaj zawiesi.
  3. Usuń klasy MotorbikeTaxPayment, CarTaxPayment, TaxPayment. Są to niepotrzebne komplikacje / bałagan.
  4. Każdy typ pojazdu (samochód, rower, samochód kempingowy) powinien wdrożyć co najmniej Pojazd, a jeśli jest opodatkowany, powinien również wdrożyć ITaxPayment. W ten sposób możesz rozróżnić między pojazdami, które nie są opodatkowane, a tymi, które są ... Zakładając, że taka sytuacja nawet istnieje.
  5. Każdy typ pojazdu powinien wdrożyć, w granicach samej klasy, metody zdefiniowane w twoich interfejsach.
  6. Dodaj także datę ostatniej płatności do interfejsu ITaxPayment.

Masz rację. Pierwotnie pytanie nie zostało sformułowane w ten sposób, usunąłem część i znaczenie się zmieniło. Teraz jest naprawione. Dzięki!

2

Dlaczego pojazd musi wiedzieć, czy musi zapłacić? Czy nie dotyczy to administracji publicznej?

Nie. Jak rozumiem, cała Twoja aplikacja dotyczy podatków. Tak więc troska o to, czy pojazd powinien zostać opodatkowany, nie jest już kwestią administracji publicznej, niż czymkolwiek innym w całym programie. Nie ma powodu, aby umieszczać to nigdzie indziej niż tutaj.

public class Car : Vehicle
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 30;
    }
}

public class Motorbike : Vehicle
{
    public override bool HasToPay()
    {
        return (DateTime.Today - this.LastPaidTime).TotalDays >= 60;
    }
}

Te dwa fragmenty kodu różnią się tylko stałą. Zamiast przesłonić całą funkcję HasToPay (), przesłoń anint DaysBetweenPayments() funkcję funkcję. Nazwij to zamiast używać stałej. Jeśli to wszystko, czym się różnią, porzuciłbym zajęcia.

Sugerowałbym również, że motocykl / samochód powinien być właściwością kategorii pojazdu, a nie podklasy. To daje większą elastyczność. Na przykład ułatwia zmianę kategorii motocykla lub samochodu. Oczywiście w prawdziwym życiu raczej nie można przerobić motocykli na samochody. Ale wiesz, że pojazd zostanie źle wprowadzony i trzeba go później zmienić.

Jasne, możemy przekazać wartość konstruktorowi TaxPayment, ale to naruszyłoby hermetyzację pojazdu lub obowiązki, czy jakkolwiek nazywają to ewangeliści OOP, prawda?

Nie całkiem. Obiekt, który celowo przekazuje swoje dane do innego obiektu jako parametr, nie stanowi dużego problemu związanego z enkapsulacją. Prawdziwą troską są inne obiekty sięgające do tego obiektu w celu wyciągnięcia danych lub manipulowania danymi wewnątrz obiektu.


1

Możesz być na dobrej drodze. Problem polega na tym, jak zauważyłeś, że w TaxPayment nie ma członka LastPaidTime . Właśnie tam należy, choć wcale nie musi być publicznie ujawniany:

public interface ITaxPayment
{
    // Your base TaxPayment class will know the 
    // next due date. But you can easily create
    // one which uses (for example) fixed dates
    bool IsPaymentDue();
}

public interface IVehicle
{
    string Plate { get; }
    Person Owner { get; }
    ITaxPayment LastTaxPayment { get; }
} 

Za każdym razem, gdy otrzymujesz płatność, tworzysz nowe wystąpienie ITaxPaymentzgodnie z bieżącymi zasadami, które mogą mieć zupełnie inne reguły niż poprzednie.


Chodzi o to, że należna płatność zależy od ostatniej płatności właściciela (różne pojazdy, różne terminy płatności). Skąd ITaxPayment wie, kiedy ostatni raz pojazd (lub właściciel) zapłacił za wykonanie obliczeń?

1

Zgadzam się z odpowiedzią @sebastiangeiger, że problem dotyczy raczej własności pojazdów niż pojazdów. Proponuję użyć jego odpowiedzi, ale z kilkoma zmianami. Chciałbym, aby Ownershipklasa była klasą ogólną:

public class Vehicle {}

public abstract class Ownership<T>
{
    public T Item { get; set; }

    public DateTime LastPaidTime { get; set; }

    public abstract TimeSpan PaymentInterval { get; }

    public virtual bool HasToPay
    {
        get { return (DateTime.Today - this.LastPaidTime) >= this.PaymentInterval; }
    }
}

I użyj dziedziczenia, aby określić interwał płatności dla każdego typu pojazdu. Sugeruję również użycie silnie typowanej TimeSpanklasy zamiast zwykłych liczb całkowitych:

public class CarOwnership : Ownership<Vehicle>
{
    public override TimeSpan PaymentInterval
    {
        get { return new TimeSpan(30, 0, 0, 0); }
    }
}

public class MotorbikeOwnership : Ownership<Vehicle>
{
    public override TimeSpan PaymentInterval
    {
        get { return new TimeSpan(60, 0, 0, 0); }
    }
}

Teraz twój rzeczywisty kod musi zajmować się tylko jedną klasą - Ownership<Vehicle>.

public class PublicAdministration
{
    List<Ownership<Vehicle>> VehicleOwnerships { get; set; }

    public IEnumerable<Vehicle> GetVehiclesThatHaveToPay()
    {
        return VehicleOwnerships.Where(x => x.HasToPay).Select(x => x.Item);
    }
}

0

Nie chcę ci tego łamać, ale masz domenę anemiczną , czyli logikę biznesową poza obiektami. Jeśli to jest to, co zamierzasz, jest w porządku, ale powinieneś przeczytać o powodach, aby tego uniknąć.

Staraj się nie modelować domeny problemu, ale modeluj rozwiązanie. Właśnie dlatego powstają równoległe hierarchie dziedziczenia i większa złożoność niż potrzebujesz.

Coś takiego wystarczy do tego przykładu:

public class Vehicle
{
    public Boolean isMotorBike()
    public string PlateNumber { get; set; }
    public String Owner { get; set; }
    public DateTime LastPaidTime { get; set; }
}

public class TaxPaymentCalculator
{
    public List<Vehicle> GetVehiclesThatHaveToPay(List<Vehicle> all)
}

Jeśli chcesz podążać za SOLIDEM, to świetnie, ale jak powiedział sam wujek Bob, są to tylko zasady inżynieryjne . Nie są przeznaczone do rygorystycznego stosowania, ale stanowią wytyczne, które należy wziąć pod uwagę.


4
Myślę, że twoje rozwiązanie nie jest szczególnie skalowalne. Musisz dodać nową wartość logiczną dla każdego rodzaju dodawanego pojazdu. To zły wybór.
Erik Funkenbusch

@Garrett Hall, podobało mi się, że usunąłeś klasę Person z mojej „domeny”. Nie miałbym tej klasy w pierwszej kolejności, przepraszam za to. Ale isMotorBike nie ma sensu. Które byłoby wdrożenie?

1
@DavidRobertJones: Zgadzam się z Mystere, dodanie isMotorBikemetody nie jest dobrym wyborem projektu OOP. To tak, jakby zastąpić polimorfizm kodem typu (choć boolowskim).
Groo

@MystereMan, można łatwo upuścić boole i użyćenum Vehicles
radarbob

+1. Staraj się nie modelować domeny problemu, ale modeluj rozwiązanie . Pithy . Pamiętnie powiedział. I zasady komentują. Widzę zbyt wiele prób ścisłej dosłownej interpretacji ; nonsens.
radarbob

0

Myślę, że masz rację, że okres płatności nie jest własnością rzeczywistego samochodu / motocykla. Ale jest to atrybut klasy pojazdu.

Chociaż możesz pójść dalej drogą posiadania if / select na typie obiektu, nie jest to zbyt skalowalne. Jeśli później dodasz samochód ciężarowy i Segway i pchasz rower, autobus i samolot (może się to wydawać mało prawdopodobne, ale nigdy nie wiesz o usługach publicznych), wówczas warunek staje się większy. A jeśli chcesz zmienić zasady dotyczące ciężarówki, musisz ją znaleźć na tej liście.

Dlaczego więc nie uczynić go dosłownie atrybutem i użyć refleksji, aby go wyciągnąć? Coś takiego:

[DaysBetweenPayments(30)]
public class Car : Vehicle
{
    // actual properties of the physical vehicle
}

[AttributeUsage(AttributeTargets.Class, Inherited = false)]
public class DaysBetweenPaymentsAttribute : Attribute, IPaymentRequiredCalculator
{
    private int _days;

    public DaysBetweenPaymentAttribute(int days)
    {
        _days = days;
    }

    public bool HasToPay(Vehicle vehicle)
    {
        return vehicle.LastPaid.AddDays(_days) <= DateTime.Now;
    }
}

Możesz również zastosować zmienną statyczną, jeśli jest to wygodniejsze.

Wady są

  • że będzie nieco wolniej i zakładam, że musisz przetworzyć wiele pojazdów. Jeśli jest to problem, mógłbym przyjąć bardziej pragmatyczny pogląd i trzymać się abstrakcyjnej własności lub podejścia opartego na interfejsie. (Chociaż rozważam również buforowanie według typu).

  • Podczas uruchamiania będziesz musiał sprawdzić, czy każda podklasa Vehicle ma atrybut IPaymentRequiredCalculator (lub zezwala na ustawienie domyślne), ponieważ nie chcesz, aby przetwarzał 1000 samochodów, a jedynie powodował awarię, ponieważ motocykl nie ma okresu płatności.

Plusem jest to, że jeśli później spotkasz miesiąc kalendarzowy lub roczną płatność, możesz po prostu napisać nową klasę atrybutów. To jest właśnie definicja OCP.


Głównym problemem jest to, że jeśli chcesz zmienić liczbę dni między płatnościami za pojazd, będziesz musiał odbudować i ponownie wdrożyć, a jeśli częstotliwość płatności różni się w zależności od właściwości jednostki (np. Wielkości silnika), będzie to trudno uzyskać elegancki sposób na to.
Ed James

@EdWoodcock: Myślę, że pierwszy problem dotyczy większości rozwiązań i naprawdę mnie to nie martwi. Jeśli chcą później tego poziomu elastyczności, dałbym im aplikację DB. W drugiej kwestii całkowicie się nie zgadzam. W tym momencie dodajesz pojazd jako opcjonalny argument do HasToPay () i ignorujesz go tam, gdzie go nie potrzebujesz.
pdr

Wydaje mi się, że zależy to od długoterminowych planów, jakie masz dla aplikacji, a także od konkretnych wymagań klienta, czy warto podłączyć DB, jeśli jeszcze nie istnieje (zakładam, że prawie całe oprogramowanie to DB - napędzane, o czym wiem, że nie zawsze tak jest). Jednak w drugim punkcie ważny kwalifikator jest elegancki ; Nie powiedziałbym, że dodanie dodatkowego parametru do metody atrybutu, który następnie pobiera instancję klasy, do której atrybut jest dołączony, jest szczególnie eleganckim rozwiązaniem. Ale znowu zależy to od wymagań klienta.
Ed James

@EdWoodcock: Właściwie to właśnie o tym myślałem i argument lastPaid będzie już musiał być własnością Vehicle. Mając na uwadze twój komentarz, warto zastąpić ten argument teraz, a nie później - będzie edytowany.
pdr
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.