Table of content
- Code style
- Systematic and Effective Testing
- Documentation
- Java 22
- Maven project manager
- JUnit
- AssertJ
- Jacoco
- Jqwik
- Mockito
- Selenium
Published:
Common job descriptions for someone who writes code are programmer, software developer, or software engineer. They are all describing the same job. The variety in names hints at the similarities and differences between software engineering and other engineering. Engineering aims to build something that serves a function, a product, or an artifact.
Most engineering activities involve a design that is separately developed from the product. There are three inter-related main ways in which software engineering differs:
In fact, it’s more common to hear about the software process vs. the software product in software engineering than design vs. product.
Software Analysis and Testing are very closely related to Software Quality. There are many desirable software qualities in software engineering: correctness, reliability (relative correctness), robustness (reasonable behavior even in circumstances that were not anticipated in the requirements), performance, usability (ease of use), verifiability (we will cover testing in this course), security, maintainability, reusability, portability, understandability (although some applications are less clear than others the design and implementation should not further affect it), interoperability, productivity, timeliness, visibility (transparency of the process, amenable for external examination).
When addressing software quality it’s also useful to consider the different parties: users, developers, and managers because some of the software qualities are visible to the user (we call these external qualities) and some are only visible to the developers of the system (we call these internal qualities). A user wants a product to be reliable, efficient, and easy to use. The developer wants it to be verifiable, maintainable, portable, and extensible, the manager wants the process to be productive, predictable, and easy to control.
Furthermore, while the process is for the most part not visible to the user, the product has user-visible components such as executable code and user manual, and intermediate components (work products or artifacts) such as requirements, design documents, test data, etc. These intermediate components can also be visible to other types of users (other than the end-users).
We conclude that there is a high overlap between external and internal qualities, product and process qualities which makes it hard to standardize it. There are several software quality models that share the same essential aspects but differ in emphasis and organization.
Some general principles to achieve the software qualities discussed prior are:
Code style is connected to many desirable software qualities of code, including correctness. Let’s look at this code:
class BoardComputer {
CruiseControl cruiseControl;
void authorize(User user) {
Objects.requireNonNull(user);
if (user.isUnknown())
cruiseControl.logUnauthorizedAccessAttempt();
if (user.isAstronaut())
cruiseControl.grantAccess(user);
if (user.isCommander())
cruiseControl.grantAccess(user);
cruiseControl.grantAdminAccess(user);
}
}
Note that the last line of code is not part of the body of the last if
statement and in fact, gets executed every time the method authorize
is invoked. For this particular application, this goes beyond correctness and presents a high security risk. The fix is to add curly braces around the body like so:
class BoardComputer {
CruiseControl cruiseControl;
void authorize(User user) {
Objects.requireNonNull(user);
if (user.isUnknown()){
cruiseControl.logUnauthorizedAccessAttempt();
}
if (user.isAstronaut()){
cruiseControl.grantAccess(user);
}
if (user.isCommander()){
cruiseControl.grantAccess(user);
cruiseControl.grantAdminAccess(user);
}
}
}
One may also conclude that the three cases are mutually exclusive and combine them in an if-else-if
structure like so:
class BoardComputer {
CruiseControl cruiseControl;
void authorize(User user) {
Objects.requireNonNull(user);
if (user.isUnknown()){
cruiseControl.logUnauthorizedAccessAttempt();
}
else if (user.isAstronaut()){
cruiseControl.grantAccess(user);
}
else if (user.isCommander()){
cruiseControl.grantAccess(user);
cruiseControl.grantAdminAccess(user);
}
}
}
However, note that there is an asymmetry in the code from the fact that we are mixing authorizing code with non-authorizing code. We can increase the understandability of our code by separating them out like so:
class BoardComputer {
CruiseControl cruiseControl;
void authorize(User user) {
Objects.requireNonNull(user);
if (user.isUnknown()) {
cruiseControl.logUnauthorizedAccessAttempt();
return;
}
if (user.isAstronaut()) {
cruiseControl.grantAccess(user);
} else if (user.isCommander()) {
cruiseControl.grantAccess(user);
cruiseControl.grantAdminAccess(user);
}
}
}
Published:
One of the examples from last class used an enum
structure:
private Result analyzeOrganic(Sample sample) {
if (microscope.isHumanoid(sample) == false) {
return Result.ALIEN;
} else {
return Result.HUMANOID;
}
}
An enumeration is a pre-defined set of values, for example:
public enum Result {
HUMANOID, INORGANIC, ALIEN
}
The advantage to using an enumeration is that when you declare a variable, a parameter or return type of an enumeration type then all the values (stored in the variable, passed to the parameter or returned) must be in the predefined set of the enumeration. For example, analyzeOrganic
can only return three values.
Additionally, you can add attributes and constructor to enumerations just like with classes likes so:
public enum Result {
HUMANOID(75),
INORGANIC(40),
ALIEN(100);
final int coolnessIndex;
Result(int newCI){
this.coolnessIndex = newCI;
}
}
Warm-up Problem: How can we improve the quality of this code snippet:
class CruiseControl {
private double targetSpeedKmh;
void setPreset(int speedPreset) {
if (speedPreset == 2) {
setTargetSpeedKmh(16944);
} else if (speedPreset == 1) {
setTargetSpeedKmh(7667);
} else if (speedPreset == 0) {
setTargetSpeedKmh(0);
}
}
void setTargetSpeedKmh(double speed) {
targetSpeedKmh = speed;
}
}
Numbers without an apparent meaning that steer the program are called magic numbers. We can dispel their magic and thus improve the quality of the code by making them constants:
class CruiseControl {
static final int STOP_PRESET = 0;
static final int PLANETARY_SPEED_PRESET = 1;
static final int CRUISE_SPEED_PRESET = 2;
static final double CRUISE_SPEED_KMH = 16944;
static final double PLANETARY_SPEED_KMH = 7667;
static final double STOP_SPEED_KMH = 0;
private double targetSpeedKmh;
void setPreset(int speedPreset) {
if (speedPreset == CRUISE_SPEED_PRESET) {
setTargetSpeedKmh(CRUISE_SPEED_KMH);
} else if (speedPreset == PLANETARY_SPEED_PRESET) {
setTargetSpeedKmh(PLANETARY_SPEED_KMH);
} else if (speedPreset == STOP_PRESET) {
setTargetSpeedKmh(STOP_SPEED_KMH);
}
}
void setTargetSpeedKmh(double speed) {
targetSpeedKmh = speed;
}
}
Can we do better?
Note that nothing restricts the user to call setPreset
with values other than the three values it checks for and the function terminates without error. We can make the semantics of this function more transperant for outside usage by using an enum
construct like so:
class CruiseControl {
private double targetSpeedKmh;
void setPreset(SpeedPreset speedPreset) {
Objects.requireNonNull(speedPreset);
setTargetSpeedKmh(speedPreset.speedKmh);
}
void setTargetSpeedKmh(double speedKmh) {
targetSpeedKmh = speedKmh;
}
}
enum SpeedPreset {
STOP(0), PLANETARY_SPEED(7667), CRUISE_SPEED(16944);
final double speedKmh;
SpeedPreset(double speedKmh) {
this.speedKmh = speedKmh;
}
}
Published:
Conventions are rules that are widely agreed upon by professionals. Used approapriately and consistently, they can help with the readability of the code.
Rule 1: Remove excessive comments for example:
class Inventory {
// Fields (we only have one)
List<Supply> supplies = new ArrayList<>(); // The list of supplies.
// Methods
int countContaminatedSupplies() {
// TODO: check if field is already initialized (not null)
int contaminatedCounter = 0; // the counter
// No supplies => no contamination
for (Supply supply : supplies) { // begin FOR
if (supply.isContaminated()) {
contaminatedCounter++; // increment counter!
} // End IF supply is contaminated
}// End FOR
// Returns the number of contaminated supplies.
return contaminatedCounter; // Handle with care!
}
} // End of Inventory class
Becomes:
class Inventory {
List<Supply> supplies = new ArrayList<>();
int countContaminatedSupplies() {
if (supplies == null || supplies.isEmpty()) {
// No supplies => no contamination
return 0;
}
int contaminatedCounter = 0;
for (Supply supply : supplies) {
if (supply.isContaminated()) {
contaminatedCounter++;
}
}
return contaminatedCounter;
}
}
Rule 2: Remove code in comments for example:
class LaunchChecklist {
List<String> checks = Arrays.asList(
"Cabin Leak",
// "Communication", // Do we actually want to talk to Houston?
"Engine",
"Hull",
// "Rover", // We won't need it, I think...
"OxygenTank"
//"Supplies"
);
Status prepareLaunch(Commander commander) {
for (String check : checks) {
boolean shouldAbortTakeoff = commander.isFailing(check);
if (shouldAbortTakeoff) {
//System.out.println("REASON FOR ABORT: " + item);
return Status.ABORT_TAKE_OFF;
}
}
return Status.READY_FOR_TAKE_OFF;
}
}
Becomes:
class LaunchChecklist {
List<String> checks = Arrays.asList(
"Cabin Leak",
"Engine",
"Hull",
"OxygenTank"
);
Status prepareLaunch(Commander commander) {
for (String check : checks) {
boolean shouldAbortTakeoff = commander.isFailing(check);
if (shouldAbortTakeoff) {
return Status.ABORT_TAKE_OFF;
}
}
return Status.READY_FOR_TAKE_OFF;
}
}
Rule 3: Replace comments with constants for example:
enum SmallDistanceUnit {
CENTIMETER,
INCH;
double getConversionRate(SmallDistanceUnit unit) {
if (this == unit) {
return 1; // identity conversion rate
}
if (this == CENTIMETER && unit == INCH) {
return 0.393701; // one centimeter in inch
} else {
return 2.54; // one inch in centimeters
}
}
}
Becomes:
enum SmallDistanceUnit {
CENTIMETER,
INCH;
static final double INCH_IN_CENTIMETERS = 2.54;
static final double CENTIMETER_IN_INCHES = 1 / INCH_IN_CENTIMETERS;
static final int IDENTITY = 1;
double getConversionRate(SmallDistanceUnit unit) {
if (this == unit) {
return IDENTITY;
}
if (this == CENTIMETER && unit == INCH) {
return CENTIMETER_IN_INCHES;
} else {
return INCH_IN_CENTIMETERS;
}
}
}
Rule 4: Replace comments with utility methods for example:
class FuelSystem {
List<Double> tanks = new ArrayList<>();
int getAverageTankFillingPercent() {
double sum = 0;
for (double tankFilling : tanks) {
sum += tankFilling;
}
double averageFuel = sum / tanks.size();
// round to integer percent
return Math.toIntExact(Math.round(averageFuel * 100));
}
}
Becomes:
class FuelSystem {
List<Double> tanks = new ArrayList<>();
int getAverageTankFillingPercent() {
double sum = 0;
for (double tankFilling : tanks) {
sum += tankFilling;
}
double averageFuel = sum / tanks.size();
return roundToIntegerPercent(averageFuel);
}
static int roundToIntegerPercent(double value) {
return Math.toIntExact(Math.round(value * 100));
}
}
Rule 5: Do document implementation decisions using a pattern like this:
/*
* In the context of [USE CASE],
* facing [CONCERN]
* we decided for [OPTION]
* to achieve [QUALITY],
* accepting [DOWNSIDE].
*/
For example:
class Inventory {
private List<Supply> list = new ArrayList<>();
void add(Supply supply) {
list.add(supply);
Collections.sort(list);
}
boolean isInStock(String name) {
// fast implementation
return Collections.binarySearch(list, new Supply(name)) != -1;
}
}
Becomes:
boolean isInStock(String name) {
/*
* In the context of checking availability of supplies by name,
* facing severe performance issues with >1000 supplies
* we decided to use the binary search algorithm
* to achieve item retrieval within 1 second,
* accepting that we must keep the supplies sorted.
*/
return Collections.binarySearch(list, new Supply(name)) != -1;
}
Rule 6: Use examples in your documentation for instance:
class Supply {
/**
* The code universally identifies a supply.
*
* It follows a strict format, beginning with an S (for supply), followed
* by a five digit inventory number. Next comes a backslash that
* separates the country code from the preceding inventory number. This
* country code must be exactly two capital letters standing for one of
* the participating nations (US, EU, RU, CN). After that follows a dot
* and the actual name of the supply in lowercase letters.
*/
static final Pattern CODE =
Pattern.compile("^S\\d{5}\\\\(US|EU|RU|CN)\\.[a-z]+$");
}
Becomes:
class Supply {
/**
* The expression universally identifies a supply code.
*
* Format: "S<inventory-number>\<COUNTRY-CODE>.<name>"
*
* Valid examples: "S12345\US.pasta", "S08342\CN.wrench",
* "S88888\EU.laptop", "S12233\RU.brush"
*
* Invalid examples:
* "R12345\RU.fuel" (Resource, not supply)
* "S1234\US.light" (Need five digits)
* "S01234\AI.coconut" (Wrong country code. Use US, EU, RU, or CN)
* " S88888\EU.laptop " (Trailing whitespaces)
*/
static final Pattern SUPPLY_CODE =
Pattern.compile("^S\\d{5}\\\\(US|EU|RU|CN)\\.[a-z]+$");
}
Published:
Published:
class Network {
ObjectInputStream inputStream;
InterCom interCom;
void listen() throws IOException, ClassNotFoundException {
while (true) {
Object signal = inputStream.readObject();
CrewMessage crewMessage = (CrewMessage) signal;
interCom.broadcast(crewMessage);
}
}
}
With this improvement becomes:
class Network {
ObjectInputStream inputStream;
InterCom interCom;
void listen() throws IOException, ClassNotFoundException {
while (true) {
Object signal = inputStream.readObject();
if (signal instanceof CrewMessage) {
CrewMessage crewMessage = (CrewMessage) signal;
interCom.broadcast(crewMessage);
}
}
}
}
class CruiseControl {
static final double SPEED_OF_LIGHT_KMH = 1079252850;
static final double SPEED_LIMIT = SPEED_OF_LIGHT_KMH;
private double targetSpeedKmh;
void setTargetSpeedKmh(double speedKmh) {
if (speedKmh < 0) {
throw new IllegalArgumentException();
} else if (speedKmh <= SPEED_LIMIT) {
targetSpeedKmh = speedKmh;
} else {
throw new IllegalArgumentException();
}
}
}
Becomes:
class CruiseControl {
static final double SPEED_OF_LIGHT_KMH = 1079252850;
static final double SPEED_LIMIT = SPEED_OF_LIGHT_KMH;
private double targetSpeedKmh;
void setTargetSpeedKmh(double speedKmh) {
if (speedKmh < 0 || speedKmh > SPEED_LIMIT) {
throw new IllegalArgumentException();
}
targetSpeedKmh = speedKmh;
}
}
class TransmissionParser {
static Transmission parse(String rawMessage) {
if (rawMessage != null
&& rawMessage.length() != Transmission.MESSAGE_LENGTH) {
throw new IllegalArgumentException("Bad message received!");
}
String rawId = rawMessage.substring(0, Transmission.ID_LENGTH);
String rawContent = rawMessage.substring(Transmission.ID_LENGTH);
try {
int id = Integer.parseInt(rawId);
String content = rawContent.trim();
return new Transmission(id, content);
} catch (Exception e) {
throw new IllegalArgumentException("Bad message received!");
}
}
}
The exception type in catch
is too general, we can first make it more specific:
class TransmissionParser {
static Transmission parse(String rawMessage) {
if (rawMessage != null &&
rawMessage.length() != Transmission.MESSAGE_LENGTH) {
throw new IllegalArgumentException("Bad message received!");
}
String rawId = rawMessage.substring(0, Transmission.ID_LENGTH);
String rawContent = rawMessage.substring(Transmission.ID_LENGTH);
try {
int id = Integer.parseInt(rawId);
String content = rawContent.trim();
return new Transmission(id, content);
} catch (NumberFormatException e) {
throw new IllegalArgumentException("Bad message received!");
}
}
}
We can communicate this information further using the message in the exception like so:
class TransmissionParser {
static Transmission parse(String rawMessage) {
if (rawMessage != null
&& rawMessage.length() != Transmission.MESSAGE_LENGTH) {
throw new IllegalArgumentException(
String.format("Expected %d, but got %d characters in '%s'",
Transmission.MESSAGE_LENGTH, rawMessage.length(),
rawMessage));
}
String rawId = rawMessage.substring(0, Transmission.ID_LENGTH);
String rawContent = rawMessage.substring(Transmission.ID_LENGTH);
try {
int id = Integer.parseInt(rawId);
String content = rawContent.trim();
return new Transmission(id, content);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
String.format("Expected number, but got '%s' in '%s'",
rawId, rawMessage));
}
}
}
You may also pass the cause chain to the exception like so:
class TransmissionParser {
static Transmission parse(String rawMessage) {
if (rawMessage != null
&& rawMessage.length() != Transmission.MESSAGE_LENGTH) {
throw new IllegalArgumentException(
String.format("Expected %d, but got %d characters in '%s'",
Transmission.MESSAGE_LENGTH, rawMessage.length(),
rawMessage));
}
String rawId = rawMessage.substring(0, Transmission.ID_LENGTH);
String rawContent = rawMessage.substring(Transmission.ID_LENGTH);
try {
int id = Integer.parseInt(rawId);
String content = rawContent.trim();
return new Transmission(id, content);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
String.format("Expected number, but got '%s' in '%s'",
rawId, rawMessage), e);
}
}
}
Published:
[Guide for scientific reporting]
[Colgate’s Guide for how to paraphrase] (pages 17-18)
Published:
Published:
[10-17-2024 Worksheet] [10-17-2024 Answers]
Published:
Published: