L'entité de domaine viole-t-elle le principe de responsabilité unique?


13

La responsabilité unique (raison du changement) d'une entité devrait être de s'identifier de manière unique, en d'autres termes, sa responsabilité doit être identifiable.

Livre DDD d'Eric Evan, p. 93:

la responsabilité la plus fondamentale des entités est d'établir une continuité afin que le comportement soit clair et prévisible. Ils le font mieux s'ils sont tenus à l'écart. Plutôt que de se concentrer sur les attributs ou même le comportement, dépouillez la définition de l'objet Entity jusqu'aux caractéristiques les plus intrinsèques, en particulier celles qui l'identifient ou sont couramment utilisées pour le trouver ou le faire correspondre. Ajoutez uniquement les comportements essentiels au concept et aux attributs requis par ce comportement.

Au-delà de cela, cherchez à supprimer le comportement et les attributs dans d'autres objets associés à l'entité de base. Au-delà des problèmes d'identité, les entités ont tendance à assumer leurs responsabilités en coordonnant les opérations des objets qu'elles possèdent.

1.

... dépouille la définition de l'objet ENTITY jusqu'aux caractéristiques les plus intrinsèques, en particulier celles qui l'identifient ou sont couramment utilisées pour le trouver ou le faire correspondre. Ajoutez uniquement les comportements essentiels au concept ...

Une fois qu'une entité se voit attribuer un ID unique , son identité est établie et je suppose donc qu'une telle entité n'a besoin d'aucun comportement pour maintenir son identité ou pour l' aider à s'identifier . Ainsi, je ne comprends pas ce genre de comportement est l' auteur se référant à ( en plus findet match opérations ) avec « un comportement qui est essentiel au concept »?

2.

... dépouille la définition de l'objet ENTITY jusqu'aux caractéristiques les plus intrinsèques, en particulier celles qui l'identifient ou sont couramment utilisées pour le trouver ou le faire correspondre. ... Au-delà de cela, cherchez à supprimer le comportement et les attributs dans d'autres objets associés au noyau ENTITY.

Donc, tout comportement qui n'aide pas à identifier l'entité, mais nous caractériserions toujours ce comportement comme étant une caractéristique intrinsèque de cette entité (c'est-à-dire que les aboiements sont intrinsèques aux chiens, le vol est intrinsèque aux avions, la ponte est intrinsèque aux oiseaux .. .), devrait être placé dans d'autres objets associés à cette entité (exemple: nous devrions mettre le comportement d'aboiement dans un objet associé à une entité chien)?

3.

Au-delà de cela, cherchez à supprimer le comportement et les attributs dans d'autres objets associés au noyau ENTITY.

a) MyEntitydélègue les responsabilités A_respet B_respaux objets aet b, respectivement.

Même si la plupart A_respet le B_resptravail se fait par aet bcas, les clients sont toujours servis A_respet B_resppar MyEntity, ce qui signifie que du point de vue du client les deux responsabilités appartiennent MyEntity. Ainsi, ne pas que cela veut dire MyEntityaussi a A_respet B_respresponsabilités et en tant que telle ne respecte pas SRP ?

b) Même si nous supposons que A_respet B_respn'appartiennent pas MyEntity, a MyEntitytoujours la responsabilité AB_respde coordonner les opérations des objets aet b. Donc, ne MyEntityviole pas la SRP car au moins il a deux responsabilités - s'identifier de manière unique et aussi AB_resp?

class MyEntity
{
    private A a = ...
    private B b = ...


    public A GetA()
    { ... }

    public B GetB()
    { ... }

    /* coordinates operations of objects a and b */
    public int AworkB()
    { ... }
}

/* A encapsulates a single responsibility resp_A*/
/* A is value object */
class A
{ ... }

/* B encapsulates a single responsibility resp_B*/
/* B is value object */
class B
{ ... }

MISE À JOUR:

1.

Dans ce contexte, le comportement fait référence au comportement sémantique. Par exemple, une propriété sur une classe (c'est-à-dire un attribut sur un objet de domaine) qui est utilisée pour l'identifier de manière unique a un comportement. Bien que cela ne soit pas représenté directement dans le code. Le comportement attendu est qu'il n'y aura pas de valeurs en double pour cette propriété.

Donc, dans le code, nous n'aurions presque jamais besoin de mettre en œuvre un comportement (c'est-à-dire une opération) qui maintiendrait en quelque sorte l'identité de l'entité, car comme vous l'avez expliqué, un tel comportement n'existe que comme concept dans un modèle de domaine (sous la forme d'un attribut ID de une entité), mais lorsque nous traduisons cet attribut ID en code, une partie de sa sémantique est perdue (c'est-à-dire la partie qui s'assure implicitement que la valeur ID est unique est perdue)?

2.

De plus, une propriété telle que Age n'a pas de contexte en dehors d'une entité personne, et en tant que telle, n'a aucun sens de se déplacer vers un autre objet ... Cependant, ces informations pourraient facilement être stockées dans un emplacement séparé que l'identifiant unique, d'où la référence confuse au comportement. L'âge peut être une valeur chargée paresseuse.

a) Si la Agepropriété est chargée paresseusement, alors nous pouvons l'appeler un comportement, même si sémantiquement Agen'est qu'un attribut?

3.

Vous pourriez facilement avoir des opérations spécifiques à l'adresse telles que la vérification qu'il s'agit d'une adresse valide. Vous ne le savez peut-être pas au moment de la conception, mais tout ce concept consiste à décomposer les objets en leurs plus petites parties

Bien que je convienne que nous perdrions le contexte en nous déplaçant Agedans un objet différent, le contexte ne serait pas perdu si nous déplacions une DateOfBirthpropriété dans un objet différent, mais nous ne le déplaçons généralement pas.

Quelle est la principale raison pour laquelle nous irions Addressdans un autre objet, mais pas DateOfBirth? Parce que DateOfBirthc'est plus intrinsèque à l' Personentité ou parce qu'il y a moins de chances que quelque part dans le futur nous ayons besoin de définir des opérations spécifiques à DateOfBirth?

4. Je dois dire que je ne sais pas encore si MyEntitya aussi A_respet B_respresponsabilités et pourquoi MyEntityaussi avoir AB_respn'est pas considéré comme une violation du SRP

EULERFX

1)

Les comportements auxquels l'auteur fait référence sont des comportements associés à l'entité. Ce sont les comportements qui modifient l'état de l'entité

a) Si je vous comprends bien, vous dites que l' entité ne devrait contenir que les comportements qui modifient ses attributs (c'est-à-dire son état )?

b) Et qu'en est-il des comportements qui ne modifient pas nécessairement l' état de l'entité , mais qui sont toujours considérés comme étant une caractéristique intrinsèque de cette entité (exemple: aboyer serait une caractéristique intrinsèque d'une Dogentité, même s'il ne modifiait pas État du chien )? Faut-il inclure ces comportements dans une entité ou les déplacer vers d'autres objets?

2)

En ce qui concerne le déplacement du comportement vers d'autres objets, l'auteur se réfère spécifiquement aux objets de valeur.

Bien que ma citation ne l'inclue pas, mais l'auteur mentionne dans le même paragraphe que, dans certains cas, les comportements (et attributs ) seront également déplacés dans d' autres entités (bien que je comprenne les avantages du déplacement des comportements vers les VO)

3) En supposant que MyEntity(voir la question 3. dans mon message d'origine) ne viole pas la SRP, dirions-nous qu'une responsabilité de MyEntitycomprend également, entre autres:

une. A_resp + B_resp + AB_resp ( AB_respcoordonne les objets aet b)

ou

b. AB_resp + délégation A_respet B_respaux objets ( aet b) associés à MyEntity?

4) Livre DDD d'Eric Evan, p. 94:

CustomerID est le seul et unique identifiant de l'entité client (figure 5.5), mais le numéro de téléphone et l'adresse sont souvent utilisés pour trouver ou faire correspondre un client. Le nom ne définit pas l'identité d'une personne, mais il est souvent utilisé dans le cadre de sa détermination.

Dans cet exemple, les attributs de téléphone et d'adresse ont été transférés dans Client, mais sur un projet réel, ce choix dépendrait de la manière dont les clients du domaine sont généralement mis en correspondance ou distingués. Par exemple, si un client a plusieurs numéros de téléphone de contact à des fins différentes, le numéro de téléphone n'est pas associé à l'identité et doit rester avec le contact commercial.

une)

CustomerID est le seul et unique identifiant de l'entité client (figure 5.5), mais le numéro de téléphone et l'adresse sont souvent utilisés pour trouver ou faire correspondre un client. Le nom ne définit pas l'identité d'une personne, mais il est souvent utilisé dans le cadre de sa détermination.

La citation indique que seuls les attributs associés à l' identité doivent rester dans une entité . Je suppose que l'auteur signifie que l' entité ne doit contenir que les attributs qui sont souvent utilisés pour rechercher ou faire correspondre cette entité , alors que TOUS les autres attributs doivent être déplacés?

b) Mais comment / où déplacer les autres attributs ? Par exemple (l'hypothèse ici est que l' attribut d'adresse n'est pas utilisé pour rechercher ou faire correspondre Customer et donc nous voulons retirer l' attribut d'adresseCustomer ):

si au lieu d'avoir Customer.Address(de type string) nous créons une propriété Customer.Addressde type Address, avons-nous déplacé l' attribut d'adresse dans un objet VO associé (qui est de type Address) ou dirions-nous qu'il Customercontient toujours un attribut d'adresse ?

c)

Dans cet exemple, les attributs de téléphone et d'adresse ont été transférés dans Client, mais sur un projet réel, ce choix dépendrait de la manière dont les clients du domaine sont généralement mis en correspondance ou distingués. Par exemple, si un client a plusieurs numéros de téléphone de contact à des fins différentes, le numéro de téléphone n'est pas associé à l'identité et doit rester avec le contact commercial.

Ce n'est pas l'auteur qui a tort ici, car si nous supposons que chacun des nombreux numéros de téléphone de contact qui Customern'appartiennent qu'à ce particulier Customer, je dirais que ces numéros de téléphone sont associés à l' identité autant que lorsqu'ils Customern'avaient qu'un seul numéro de téléphone. ?

5)

La raison pour laquelle l'auteur suggère de supprimer l'entité est que lorsque l'on crée initialement une entité Client, il y a une tendance à la remplir avec n'importe quel attribut que l'on peut penser être associé à un client. Il s'agit d'une approche centrée sur les données qui néglige les comportements conduisant finalement à un modèle de domaine anémique.

Hors sujet, mais je pensais que le modèle de domaine anémique résulte du déplacement d'un comportement hors d'une entité , tandis que votre exemple remplit une entité avec beaucoup d' attributs , ce qui entraînerait Customerun trop grand nombre de comportements (car nous inclurions probablement aussi dans Customerles comportements qui modifier ces attributs supplémentaires ) et donc en violation de SRP?

Merci


2
je recommande fortement la série de vidéos de code propre de robert martins, cleancoders.com. Il explique en détail comment les différents principes peuvent soit causer des problèmes, soit s'équilibrer. Sinon, je pense qu'une partie de la formule de votre exemple consisterait à examiner la durée pendant laquelle l'objet Personne est concerné. si c'est pour un court laps de temps comme un achat, alors l'adresse de facturation utilisée pour l'achat en fera partie et ne pourra pas être modifiée. si c'est pour un compte de bibliothèque, alors l'adresse devrait pouvoir changer.
cartalot

2
Je pense que cette question pourrait violer le SRP ...;)
IntelliData

Réponses:


6

Dans ce contexte, le comportement fait référence au comportement sémantique. Par exemple, une propriété sur une classe (c'est-à-dire un attribut sur un objet de domaine) qui est utilisée pour l'identifier de manière unique a un comportement. Bien que cela ne soit pas représenté directement dans le code. Le comportement attendu est qu'il n'y aura pas de valeurs en double pour cette propriété. Quelque chose comme une adresse qui peut avoir sa propre identité, mais qui n'existe pas en dehors du contexte d'une entité personne doit toujours être déplacé dans son propre objet. Promouvant ainsi l'entité en une racine agrégée.

De plus, une propriété telle que Age n'a pas de contexte en dehors d'une entité personne et, en tant que telle, n'a aucun sens de se déplacer vers un objet différent. Le contexte serait perdu et vous pouvez donc déterminer en toute sécurité qu'il s'agit d'une valeur essentielle pour l'entité-personne. Vous ne pourriez pas localiser la valeur autrement. Cependant, ces informations pourraient facilement être stockées dans un emplacement séparé qui est l'identifiant unique, d'où la référence confuse au comportement . L'âge peut être une valeur chargée paresseuse.

Donc, pour répondre à votre question. Non, cela ne viole pas le principe de responsabilité unique. Il est clair qu'une personne ne devrait avoir que des informations sur une personne, et non quelque chose comme l'adresse, qui est plus complexe et liée à une personne, devrait exister en tant que sa propre entité.

Vous pourriez facilement avoir des opérations spécifiques à l'adresse telles que la vérification qu'il s'agit d'une adresse valide. Vous ne le savez peut-être pas au moment de la conception, mais tout ce concept consiste à décomposer les objets en leurs plus petites parties afin que quelque chose comme ça soit relativement simple lorsqu'il est fait après coup.

Mise à jour: 1) Dans la plupart des cas, cette validation d'identité est effectuée lors de l'enregistrement d'un objet dans un magasin de données. Ce qui signifie que le code représentant la validation d'entité existe, mais il existe ailleurs. Il existe généralement avec le code chargé d'émettre la valeur d'identité. C'est pourquoi je déclare que l'unicité n'est pas représentée directement dans le code de l'entité.

2) L'énoncé correct serait qu'il Ages'agit d'un attribut qui a un comportement. Vous devrez documenter le fait que Age est chargé paresseusement afin qu'un développeur consommant cette propriété puisse prendre une décision précise sur la façon de consommer cette propriété

3) DateOfBirthest généralement un objet différent; Un objet date qui a déjà des opérations prédéfinies dessus. Dans certaines langues, l'objet date a déjà un modèle de domaine entier défini dessus. Par exemple, en c #, vous pouvez spécifier le fuseau horaire, si la date est UTC, ajoutez et soustrayez des dates pour obtenir un intervalle de temps. Donc, votre hypothèse sur le déplacement DateOfBirthserait correcte.

4) Si la seule chose qui se MyEntityproduit est la délégation et la cooridination, alors non, cela ne viole pas le SRP. Sa seule responsabilité est de déléguer et de coordonner et est appelée le modèle de façade


Pourriez-vous regarder la mise à jour que j'ai faite?
EdvRusj

Mis à jour ma réponse
Charles Lambert

4

Très bonne question. Le SRP ne devrait pas être pris de manière aussi litigieuse. L'identification / recherche n'est pas la responsabilité de l'entité en ce qui concerne le SRP. Quelqu'un d'autre est responsable de lui donner un identifiant (à savoir le magasin) et de le rechercher (à savoir le référentiel ).

Le but principal d'une entité est de représenter les concepts découverts par le modèle. La seule différence entre une entité et un objet de valeur est que l'entité a une signification au-delà de ses attributs non identifiants. Par exemple, si une personne change de nom, elle est toujours la même personne, juste avec un nom différent.


1

Une fois qu'une entité se voit attribuer un ID unique, son identité est établie et je suppose donc qu'une telle entité n'a besoin d'aucun comportement pour maintenir son identité ou pour l'aider à s'identifier. Ainsi, je ne comprends pas à quel type de comportement l'auteur fait-il référence (outre les opérations de recherche et de correspondance) avec un "comportement essentiel au concept"?

Si l'identité est établie, alors oui, l'entité n'a besoin de rien d'autre pour être identifiée. Les comportements auxquels l'auteur fait référence sont des comportements associés à l'entité. Ce sont les comportements qui modifient l'état de l'entité. Par exemple, une Customerentité peut avoir un MakePreferredcomportement. La raison pour laquelle l'auteur suggère de supprimer l'entité est que lorsque l'on crée initialement une Customerentité, il y a une tendance à la remplir avec n'importe quel attribut que l'on peut penser être associé à un client. Il s'agit d'une approche centrée sur les données qui néglige les comportements conduisant finalement à un modèle de domaine anémique.

En ce qui concerne le déplacement du comportement vers d'autres objets, l'auteur se réfère spécifiquement aux objets de valeur. La raison pour laquelle il est judicieux de déplacer le comportement vers les VO est que les VO sont généralement "plus petites" que les entités, donc plus ciblées. De plus, des aspects tels que l'immuabilité et la fermeture des opérations simplifient le raisonnement sur le code tout en le rendant plus SOLIDE .

Avec les VO, une entité sert comme une sorte d'ancre qui coordonne les différentes VO qui mettent en œuvre son comportement.

En ce qui concerne SRP, votre confusion n'est pas injustifiée. Un problème avec la mise en œuvre stéréotypée de la POO d'entités est la fusion de l'identité et de l'État. En effet, d'un point de vue comportemental, l'identité n'a rien à voir avec les comportements. En d'autres termes, l'identité d'une entité n'est requise pour aucun de ses comportements. Il existe des implémentations où cette confusion est éliminée, comme AggregateSource ou une approche fonctionnelle que je décris ici .

L'autre problème est que, dans une certaine mesure, la PÉR peut être une mesure qualitative. N'importe qui peut proposer une définition de responsabilité unique que certaines classes violent. On peut dire que la responsabilité de l'entité est de mettre en œuvre les comportements requis de cette entité. En ce sens, il a une seule responsabilité. De plus, lorsqu'une entité délègue des comportements à des VO constituantes, elle ne viole pas SRP. SRP n'interdit pas la composition du genre. Cela met en garde de réduire le couplage entre les objets au minimum absolu, de garder les interfaces aussi nues que possible, etc.

MISE À JOUR

a) Si je vous comprends bien, vous dites que l'entité ne devrait contenir que les comportements qui modifient ses attributs (c'est-à-dire son état)?

Oui, bien qu'il y ait des exceptions ...

b) Et qu'en est-il des comportements qui ne modifient pas nécessairement l'état de l'entité, mais qui sont toujours considérés comme étant une caractéristique intrinsèque de cette entité (exemple: aboyer serait une caractéristique intrinsèque d'une entité Chien, même si ce n'était pas le cas) modifier l'état du chien)? Faut-il inclure ces comportements dans une entité ou les déplacer vers d'autres objets?

Il est acceptable que les entités contiennent des méthodes d'usine pour créer des instances d'entités qui seraient en fait des entités enfants, mais où les références d'objet ne sont pas utilisées pour la traversée. Dans ce cas, l'entité enfant doit être conservée par le service d'application. Le service d'application utilise l'entité parent pour construire l'entité enfant.

3) En supposant que MyEntity (voir la question 3. dans mon message d'origine) ne viole pas SRP, dirions-nous qu'une responsabilité de MyEntity comprend entre autres également:

Vous envisagez la responsabilité du point de vue de la mise en œuvre. Considérez plutôt l'entité comme une sorte de boîte noire avec des responsabilités. La façon dont cela les gère ne vous intéresse pas en tant que personne regardant de l'extérieur. Le partage des responsabilités entre les VO ou même d'autres entités est une préoccupation de mise en œuvre.

La citation indique que seuls les attributs associés à l'identité doivent rester dans une entité. Je suppose que l'auteur signifie que l'entité ne doit contenir que les attributs qui sont souvent utilisés pour rechercher ou faire correspondre cette entité, alors que TOUS les autres attributs doivent être déplacés?

Plus précisément, les attributs qui ne sont pas requis pour le comportement ni la recherche ne doivent pas faire partie de l'entité. Pourquoi s'embêter? De plus, avec quelque chose comme le modèle de modèle de lecture , les entités n'ont besoin de rien au-delà des attributs requis pour le comportement.

si au lieu d'avoir Customer.Address (de type chaîne) nous créons une propriété Customer.Address de type Address, avons-nous déplacé l'attribut d'adresse dans un objet VO associé (qui est de type Address) ou dirions-nous que Customer contient toujours une adresse attribut?

Oui, en effet, il n'y a pas de différence entre une adresse chaîne et une adresse Adresse VO.

Ce n'est pas l'auteur qui a tort ici, car si nous supposons que chacun des nombreux numéros de téléphone de contact que le client appartient uniquement à ce client particulier, je dirais que ces numéros de téléphone sont associés à l'identité autant que lorsque le client n'avait que un numéro de téléphone?

Je ne suis pas à 100% sur l'intention de l'auteur, mais je pense qu'il décrit simplement comment les exigences de recherche d'entité peuvent modifier la façon dont l'entité et ses VO correspondantes sont des structures.

Hors sujet, mais je pensais que le modèle de domaine anémique résulte du déplacement d'un comportement hors d'une entité, tandis que votre exemple remplit une entité avec beaucoup d'attributs, ce qui entraînerait un comportement excessif du client (car nous inclurions probablement également dans le client le comportements qui modifient ces attributs supplémentaires) et donc en violation de SRP?

Beaucoup d'attributs n'impliquent pas beaucoup de comportement. En fait, cela suggère généralement le contraire. Beaucoup d'attributs avec des getters et des setters, mais pas de comportement d'encapsulation.


J'ai fait une mise à jour
EdvRusj

1

TL; DR: vous pensez trop à cela. Cependant, je me suis amusé à y penser avec vous. Alors attachez votre ceinture ....

La responsabilité unique (raison du changement) d'une entité devrait être de s'identifier de manière unique, en d'autres termes, sa responsabilité doit être identifiable.

Non, ce n'est pas vrai. La seule responsabilité d'une entité est la continuité.

L'identité est une conséquence émergente de la continuité. La modélisation de l'identité en tant qu'idée séparable est une optimisation des performances.

Voici un exemple: un patron de restaurant donne sa voiture au voiturier. Une heure plus tard, un patron du restaurant demande la voiture. Le voiturier devrait-il le donner?

Il est facile de dire que le voiturier doit remettre la voiture si le client est le "même". Mais qu'est-ce que cela signifie réellement? La bonne façon de déterminer cela est de commencer par le "maintenant" mécène, et de chercher en arrière dans l'histoire de ce mécène pour voir si le fait de donner la voiture au voiturier fait partie de cette histoire.

Bien sûr, nous ne pouvons pas faire cela. Nous avons du mal à suivre notre propre histoire avec précision, sans parler de l'histoire de quelque chose qui n'était pas avec nous tout le temps. Donc, au lieu d'utiliser l'histoire du mécène, nous prenons des raccourcis. Le client possède-t-il le talon de ticket qui a le même numéro que l'étiquette actuellement attachée aux clés? Est-ce que le permis de conduire dans le portefeuille du client correspond au nom sur le titre au DMV, l'image sur le permis de conduire ressemble-t-elle au visage du client? Etc.

En bref: au lieu de vérifier l'historique du client, nous vérifions l'état actuel du client, pour voir si l'état actuel est cohérent avec un historique qui s'étend sur la période entre l'arrivée de la voiture et la demande de retour.

Lors de la modélisation d'entités, nous utilisons une optimisation analogue. Nous donnons à toutes les entités les responsabilités communes de

  1. S'assurer que le début de l'historique inclut une affectation d'un identifiant immuable à l'état de l'objet
  2. Veiller à ce que l'état suivant comprenne toujours une copie fidèle de l'identifiant de l'état précédent.

Je ne décris pas ici une deuxième responsabilité de l'entité; l'entité est toujours responsable de la continuité - en s'assurant que l'histoire est un récit cohérent. Les responsabilités d'identification ne sont qu'un sous-ensemble commun à toutes les entités.

Nous n'avons pas encore d'application de l'unicité. Ce n'est pas possible au sein d'une seule entité, car l'unicité requiert l'accès à l'état de toutes les entités; où une seule entité n'a accès qu'à la sienne.

Encore une fois, la vérification de tous les identifiants à chaque fois n'est pas pratique, donc à la place, nous satisfaisons l'unicité de manière simple: le code qui génère l'identifiant suivant ne doit jamais se répéter.

En fin de compte, cela signifie que nous pouvons vérifier la continuité en testant l'équivalence de deux éléments d'état différents en mémoire, ce qui évite beaucoup de tracas en essayant d'interroger des graphiques acycliques.

Vous semblez également avoir confondu le principe de responsabilité unique (qui est une très bonne idée) avec un principe de responsabilité atomique. La décomposition d'une responsabilité en parties plus petites et plus faciles à gérer est compatible avec SRP.


-3

Eh bien, cela dépend de la façon dont vous voulez le voir.

Une autre façon est: "Le principe de responsabilité unique viole-t-il l'entité de domaine?"

Les deux sont des lignes directrices. Il n'y a aucun "principe" nulle part dans la conception de logiciels. Il existe cependant de bons et de mauvais modèles. Ces deux concepts peuvent être utilisés de différentes manières pour obtenir une bonne conception.


Votes bas inexpliqués == Fanboys SRP
h bob
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.