Skip to content

Commit

Permalink
fix: remove misalignment of answer and preference terms (#65)
Browse files Browse the repository at this point in the history
  • Loading branch information
zorn authored Aug 28, 2024
1 parent db48b33 commit 4a9b309
Show file tree
Hide file tree
Showing 3 changed files with 67 additions and 23 deletions.
78 changes: 61 additions & 17 deletions docs/ubiquitous_language.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,36 +2,80 @@

## Domain Terms

* Ballot Owner -- The user who created and can administer the ballot.
* Voter -- A user who is encouraged to answer the question presented on the ballot.
* Ballot -- A user creates a ballot when they want to present a question to a potential voter and capture ranked answers.
* Question -- A phrase that inspires a voter to provide a collection of ranked answers.
* Possible Answers -- When building a question on a ballot, a ballot owner creates a comma-separated list of possible answers for the ballot question.
* Ranked Answer -- The captured ranked answer provided by a voter for a question.
* **Ballot Owner** -- The user who created and can administer the ballot.
* **Voter** -- A user who is encouraged to answer the question presented on the
ballot with their preferences.
* **Ballot** -- A user creates a ballot when they want to present a question to
a potential voter and capture ranked answers (preferences).
* **Question** -- A phrase that inspires a voter to provide a collection of
ranked answers.
* **Possible Answers** -- When building a question on a ballot, a ballot owner
creates a comma-separated list of possible answers for the ballot question.
* First Preference—When a user comes to have their vote captured, we ask for
their preferences. (See `Sharp Edges #1` below for more info.)
* **Ranked Answer** -- The captured ranked answer provided by a voter for a
question.

## Sharp Edges

1. In the code, we have a schema for `Flick.RankedVoting.RankedAnswer`, and when
a user is creating a ballot, we ask them for `Possible Answers (comma
separated)`. However, when capturing a vote, we ask those users for their
`First Preference`, `Second Preference`, etcetera. Under the hood, these
"preferences" are stored as `RankedAnswers` which feels like an unfortunate
misalignment of terms. Currently, the presentation perspective is worth the
cost of this misalignment, but feedback is welcome.

## Software Terms

### Structs, Values, and Entities

* Struct -- When we need to represent a complex domain concept that Elixir primitives can not represent, we often lean on [Elixir Structs] and [Ecto Schemas] to build those concepts.
* Entity -- Many domain concepts are not defined primarily by their attributes but rather by a thread of continued identity; these are called entities. Entities typically change over time, and equality is based on that identity, not its attributes.
* Value Object -- When you only care about the attributes of a domain concept, classify it as a value object. These value objects describe things but have no identity in and of themselves. Generally, value objects do not change over time.
* **Struct** -- When we need to represent a complex domain concept that Elixir
primitives can not represent, we often lean on [Elixir Structs] and [Ecto
Schemas] to build those concepts.
* **Entity** -- Many domain concepts are not defined primarily by their
attributes but rather by their continued identity; these are called entities.
Entities typically change over time, and equality is based on identity, not
attributes.
* **Value Object** -- When you only care about the attributes of a domain
concept, classify it as a value object. These value objects describe things
but have no identity in and of themselves. Generally, value objects do not
change over time.

> **Aside:** Working in the Elixir environment, we tend to say `Value` over `Value Object` since that distinction is needed only for object-oriented languages.
> **Aside:** Working in the Elixir environment, we tend to say `Value` over
> `Value Object` since that distinction is needed only for object-oriented
> languages.
[Elixir Structs]: https://hexdocs.pm/elixir/main/structs.html
[Ecto Schemas]: https://hexdocs.pm/ecto/Ecto.html#module-schema

#### Is an `Address` a value object? It depends.
#### An example: Is an `Address` an entity or a value object? It depends.

Whether or not any domain concept should consider an entity or a value object depends entirely on its usage. An example from [Domain-Driven Design]:
Whether or not any domain concept should consider an entity or a value object
depends entirely on its usage. An example from [Domain-Driven Design]:

> In software for a mail-order company, an address is needed to confirm the credit card, and to address the parcel. But if a roommate also orders from the same company, it is not important to realize they are in the same location. Address is a VALUE OBJECT.
> In software for a mail-order company, an address is needed to confirm the
> credit card, and to address the parcel. But if a roommate also orders from the
> same company, it is not important to realize they are in the same location.
> Address is a VALUE OBJECT.
>
> In software for the postal service, intended to organize delivery routes, the country could be formed into a hierarchy of regions, cities, postal zones, and blocks, terminating in individual addresses. These address objects would derive their zip code from their parent in the hierarchy, and if the postal service decided to reassign postal zones, all the addresses within would go along for the ride. Here, Address is an ENTITY.
> In software for the postal service, intended to organize delivery routes, the
> country could be formed into a hierarchy of regions, cities, postal zones, and
> blocks, terminating in individual addresses. These address objects would
> derive their zip code from their parent in the hierarchy, and if the postal
> service decided to reassign postal zones, all the addresses within would go
> along for the ride. Here, Address is an ENTITY.
>
> In software for an electric utility company, an address corresponds to a destination for the company's lines and service. If roommates each called to order electrical service, the company would need to realize it. Address is an ENTITY. Alternatively, the model could associate utility service with a "dwelling," an ENTITY with an attribute of address. Then Address would be a VALUE OBJECT.
> In software for an electric utility company, an address corresponds to a
> destination for the company's lines and service. If roommates each called to
> order electrical service, the company would need to realize it. Address is an
> ENTITY. Alternatively, the model could associate utility service with a
> "dwelling," an ENTITY with an attribute of address. Then Address would be a
> VALUE OBJECT.
>
> Tracking the identity of ENTITIES is essential, but attaching identity to other objects can hurt system performance, add analytical work, and muddle the model by making all objects look the same.
> Tracking the identity of ENTITIES is essential, but attaching identity to
> other objects can hurt system performance, add analytical work, and muddle the
> model by making all objects look the same.
[Domain-Driven Design]: https://www.goodreads.com/book/show/179133.Domain_Driven_Design
[Domain-Driven Design]:
https://www.goodreads.com/book/show/179133.Domain_Driven_Design
4 changes: 2 additions & 2 deletions lib/flick/ranked_voting/vote.ex
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ defmodule Flick.RankedVoting.Vote do
value = get_field(changeset, :value)

if Map.get(ranked_answer_frequencies, value) > 1 and value not in ["", nil] do
add_error(changeset, :value, gettext("answers must not be duplicated"))
add_error(changeset, :value, gettext("duplicates are not allowed"))
else
changeset
end
Expand Down Expand Up @@ -168,7 +168,7 @@ defmodule Flick.RankedVoting.Vote do
value = get_field(changeset, :value)

if value in ["", nil] do
add_error(changeset, :value, gettext("first answer is required"))
add_error(changeset, :value, gettext("can't be blank"))
else
changeset
end
Expand Down
8 changes: 4 additions & 4 deletions test/flick/ranked_voting_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -309,12 +309,12 @@ defmodule Flick.RankedVotingTest do

test "success: a vote can contain an optional `full_name` value", ~M{published_ballot} do
published_ballot_id = published_ballot.id

assert {:ok, %Vote{ballot_id: ^published_ballot_id, full_name: "John Doe"}} =
RankedVoting.create_vote(published_ballot, %{
"ranked_answers" => [%{"value" => "Sushi"}],
"full_name" => "John Doe"
})

end

test "failure: a vote should not include an answer value that is not present in the ballot",
Expand Down Expand Up @@ -351,9 +351,9 @@ defmodule Flick.RankedVotingTest do
tacos = Enum.at(ranked_answers_changesets, 1)
pizza_2 = Enum.at(ranked_answers_changesets, 2)

assert "answers must not be duplicated" in errors_on(pizza_1).value
assert "duplicates are not allowed" in errors_on(pizza_1).value
assert %{} == errors_on(tacos)
assert "answers must not be duplicated" in errors_on(pizza_2).value
assert "duplicates are not allowed" in errors_on(pizza_2).value
end

test "failure: a vote needs to include at least one ranked answer", ~M{published_ballot} do
Expand All @@ -369,7 +369,7 @@ defmodule Flick.RankedVotingTest do
assert {:error, changeset} = RankedVoting.create_vote(published_ballot, attrs)
%Ecto.Changeset{changes: %{ranked_answers: ranked_answers_changesets}} = changeset
first_ranked_answer = Enum.at(ranked_answers_changesets, 0)
assert "first answer is required" in errors_on(first_ranked_answer).value
assert "can't be blank" in errors_on(first_ranked_answer).value
end

test "failure: a vote can not be created for an unpublished ballot" do
Expand Down

0 comments on commit 4a9b309

Please sign in to comment.