/**
* Copyright (C) 2022 by Martin Robillard. See https://codesample.info/about.html
*/
package e2.chapter2;
/**
* Implementation of a playing card. This class yields immutable objects.
*/
public class {
Rank aRank;
private Suit aSuit;
/**
* Creates a new card object.
*
* @param pRank The rank of the card.
* @param pSuit The suit of the card.
* @pre pRank != null
* @pre pSuit != null
*/
public Card(Rank pRank, Suit pSuit) {
pRank != null && pSuit != null;
aRank = pRank;
aSuit = pSuit;
}
/**
* @return The rank of the card.
*/
public Rank getRank() {
return aRank;
}
/**
* @return The suit of the card.
*/
public Suit getSuit() {
return aSuit;
}
}
The idea of encapsulation in software design is to enclose the implementation of concepts (e.g., the workings of a playing card) inside a "capsule", or boundary (or interface), with only a minimal amount of contact points. Good encapsulation has many benefits, such as:
The idea of encapsulation in software design is to enclose the implementation of concepts (e.g., the workings of a playing card) inside a "capsule", or boundary (or interface), with only a minimal amount of contact points. Good encapsulation has many benefits, such as:
The Card
class is immutable: once a Card
object is created, the values it encapsulates
can't change.
Immutable objects are typically easier to use: because it is impossible to modify their state once they are created, it is only necessary to check that their state is valid during their creation by the constructor.
The Java language does not have a keyword to declare that a class is intended to be immutable. Developers need to the class to prevent its objects from being mutable, and ideally document this intent.
The Card
class is immutable: once a Card
object is created, the values it encapsulates
can't change.
Immutable objects are typically easier to use: because it is impossible to modify their state once they are created, it is only necessary to check that their state is valid during their creation by the constructor.
The Java language does not have a keyword to declare that a class is intended to be immutable. Developers need to the class to prevent its objects from being mutable, and ideally document this intent.
By reviewing the code, checking for escaping references, modifications to fields, etc.
A common mistake is assuming that a class is immutable because all of
its fields are declared final
. This is not a guarantee: the final
keyword prevents re-assigning the value of the field, but it does not
prevent changing the state of the already assigned object.
By reviewing the code, checking for escaping references, modifications to fields, etc.
A common mistake is assuming that a class is immutable because all of
its fields are declared final
. This is not a guarantee: the final
keyword prevents re-assigning the value of the field, but it does not
prevent changing the state of the already assigned object.
A precondition is a condition that client code should respect.
Otherwise, there is no guarantee about the behavior of the method,
or even how the related objects (e.g., the created Card
object)
will behave in the future.
A precondition is a condition that client code should respect.
Otherwise, there is no guarantee about the behavior of the method,
or even how the related objects (e.g., the created Card
object)
will behave in the future.
Design by Contract is a software design paradigm that helps avoid . In a nutshell, it establishes "contracts", indicated by preconditions, that client code must respect. In return, the component guarantees the proper behavior of a method and related objects. Taken another way, if client code doesn't respect the contract, there is no guarantee about what the method will do. It could corrupt the state of the current object, do nothing, throw an exception, etc. This behavior could even change in the future.
Design by Contract is a software design paradigm that helps avoid . In a nutshell, it establishes "contracts", indicated by preconditions, that client code must respect. In return, the component guarantees the proper behavior of a method and related objects. Taken another way, if client code doesn't respect the contract, there is no guarantee about what the method will do. It could corrupt the state of the current object, do nothing, throw an exception, etc. This behavior could even change in the future.
Another programming paradigm, in which you try to account for all
possible cases and have a consistent behavior for each case. This
paradigm typically involves a lot of input validation, like null
checks, which add additional noise to the code and decreases
readability.
For example, the single assert
statement would be replaced with
if (pRank == null && pSuit == null)
{
throw new NullPointerException(); // or some other behavior
}
Another programming paradigm, in which you try to account for all
possible cases and have a consistent behavior for each case. This
paradigm typically involves a lot of input validation, like null
checks, which add additional noise to the code and decreases
readability.
For example, the single assert
statement would be replaced with
if (pRank == null && pSuit == null)
{
throw new NullPointerException(); // or some other behavior
}
If we have public accessors for private fields, then why not set the fields public in the first place?
By creating methods, you allow yourself to change the implementation of the fields without changing the of the class (i.e., its "capsule"), to add additional computation in the accessors, to lazily populate the fields, etc. Even changing the name of a field could cause compilation errors in other components if the field was public.
Thus, private fields with associated public accessor are a common design decision.
However, you shouldn't blindly create a public accessor for every field: only do so if obtaining the associated information is for your abstraction.
If we have public accessors for private fields, then why not set the fields public in the first place?
By creating methods, you allow yourself to change the implementation of the fields without changing the of the class (i.e., its "capsule"), to add additional computation in the accessors, to lazily populate the fields, etc. Even changing the name of a field could cause compilation errors in other components if the field was public.
Thus, private fields with associated public accessor are a common design decision.
However, you shouldn't blindly create a public accessor for every field: only do so if obtaining the associated information is for your abstraction.
For example, it is natural to ask what the rank and suit of a playing card is, even if there wasn't a distinct field for these values.
For example, it is natural to ask what the rank and suit of a playing card is, even if there wasn't a distinct field for these values.
Here, the "class interface" refers to its set of public members
(methods, fields, nested classes, etc.). This concept is different from
that of the interface
construct (with the associated keyword) in Java.
Here, the "class interface" refers to its set of public members
(methods, fields, nested classes, etc.). This concept is different from
that of the interface
construct (with the associated keyword) in Java.
If all public methods of a class never leave an object in a corrupted state, then you have a stronger guarantee that the object won't be misused.
Similarly, complex interactions with specific constraints can be encoded in simple methods, so that client code doesn't risk missing some of these constraints.
If all public methods of a class never leave an object in a corrupted state, then you have a stronger guarantee that the object won't be misused.
Similarly, complex interactions with specific constraints can be encoded in simple methods, so that client code doesn't risk missing some of these constraints.
As long as the public members of a class stay the same, their implementation can change, for example to optimize the memory footprint of an algorithm, without affecting the rest of the code.
As long as the public members of a class stay the same, their implementation can change, for example to optimize the memory footprint of an algorithm, without affecting the rest of the code.
@pre
tags indicate preconditions. They should generally be associated
with assert
statements early in the body of the method, as it's the
case here.
@pre
tags indicate preconditions. They should generally be associated
with assert
statements early in the body of the method, as it's the
case here.
Card
objects use two fields to define exactly which cards they
represent, one of type Rank
, and the other of type Suit
. It may
seem unnecessary, or "overkill", to create two additional types for such a
simple concept, but it is generally preferable to seemingly more
efficient .
Creating new types for very simple concepts isn't bad design. Quite the opposite: classes that are easy to read and understand help developers quickly understand the components of your code.
Card
objects use two fields to define exactly which cards they
represent, one of type Rank
, and the other of type Suit
. It may
seem unnecessary, or "overkill", to create two additional types for such a
simple concept, but it is generally preferable to seemingly more
efficient .
Creating new types for very simple concepts isn't bad design. Quite the opposite: classes that are easy to read and understand help developers quickly understand the components of your code.
Alternatively, a playing card could be represented only with an int
ranging from 1 to 52, or by even by a String
naming the card (e.g.,
"ace of spades"
). However, this would make the rest of the class
harder to implement, and more error-prone: To get the rank or suit of a
card stored internally as an int
or a String
, you would need to
remember which procedure you used to translate the rank and suit of a
card into the single numeric or textual value, and parse this value
accordingly.
These choices are examples of the antipattern, where we use primitive types to represent unrelated abstract concepts.
In contrast, Rank
and Suit
are so trivial that they're actually hard
to misinterpret and misuse.
Alternatively, a playing card could be represented only with an int
ranging from 1 to 52, or by even by a String
naming the card (e.g.,
"ace of spades"
). However, this would make the rest of the class
harder to implement, and more error-prone: To get the rank or suit of a
card stored internally as an int
or a String
, you would need to
remember which procedure you used to translate the rank and suit of a
card into the single numeric or textual value, and parse this value
accordingly.
These choices are examples of the antipattern, where we use primitive types to represent unrelated abstract concepts.
In contrast, Rank
and Suit
are so trivial that they're actually hard
to misinterpret and misuse.
See the Rank
or Suit
code examples for a description of the
primitive obsession antipattern.
See the Rank
or Suit
code examples for a description of the
primitive obsession antipattern.
Giving the private
visibility to fields (i.e., they are only visible
within the class that declares them) is generally a good choice. It
strengthens encapsulation through , and prevents
client code from making unwanted modifications directly to the state of
an object that might leave it in a corrupted state.
Giving the private
visibility to fields (i.e., they are only visible
within the class that declares them) is generally a good choice. It
strengthens encapsulation through , and prevents
client code from making unwanted modifications directly to the state of
an object that might leave it in a corrupted state.
Information hiding refers to the principle of leaking the least amount of information about the implementation of a class outside that class. Client code shouldn't have to know the internal workings of an object to properly use its methods.
Information hiding also helps maintain the code: if other components don't rely on some implementation details of a class, we can probably change this implementation (e.g., to fix a bug) without breaking the other components.
Information hiding refers to the principle of leaking the least amount of information about the implementation of a class outside that class. Client code shouldn't have to know the internal workings of an object to properly use its methods.
Information hiding also helps maintain the code: if other components don't rely on some implementation details of a class, we can probably change this implementation (e.g., to fix a bug) without breaking the other components.
A first step to have good is to decide on the abstractions of your problem domain that will be represented as (Java) types.
A playing card is one such concept. It maps to a clear element from the problem domain, that we will want to manipulate in our program.
Chapter 2, insight #1
Use classes to define how domain concepts are represented in code, as opposed to encoding instances of these concepts as values of primitive types (an antipattern called Primitive Obsession)
A first step to have good is to decide on the abstractions of your problem domain that will be represented as (Java) types.
A playing card is one such concept. It maps to a clear element from the problem domain, that we will want to manipulate in our program.
Chapter 2, insight #1
Use classes to define how domain concepts are represented in code, as opposed to encoding instances of these concepts as values of primitive types (an antipattern called Primitive Obsession)
assert
statements indicate that the developer who wrote this code
assumes that the assertion is (i.e., evaluates to) true
.
In this case, it encodes a of the constructor, namely that its arguments shouldn't be null. This is an example of the paradigm.
To notify client code about this precondition, the Javadoc comment
describes it using @pre
tags.
assert
statements indicate that the developer who wrote this code
assumes that the assertion is (i.e., evaluates to) true
.
In this case, it encodes a of the constructor, namely that its arguments shouldn't be null. This is an example of the paradigm.
To notify client code about this precondition, the Javadoc comment
describes it using @pre
tags.