Graduate Program KB

Chapter 11: Systems

Table of Contents


How Would You Build a City?

  • Building/managing a city is too much for just one person
  • Cities contain teams which manage different aspects
  • Some people are responsible for the big picture, other for the small details
  • These levels of abstraction allow the individuals and components to work effectively together (without knowing all the parts)
  • How can we stay 'clean' at high levels of abstraction?

Separate Constructing a System from Using It

  • Just like a construction site, building and using are two very different processes (think about the hats 😉)
  • "Software systems should separate the startup process, when the application objects are constructed and the dependencies are 'wired' together, from the runtime logic that takes over after startup"
  • Separation of concerns (separate into units with minimal/no overlapping functionality)
  • Example of startup logic mixed with runtime logic:
public Service getService() {
    if(service == null)
        service = new MyServiceImpl(...); // Good enough default for most cases?
    return service;
}
  • 'get' service should never be initialising anything, only retrieving an existing service at runtime
  • This version is the Lazy Initialisation/Evaluation, in which we don't construct something unless we actually use it
  • However, we also require all the parameters to MyServiceImpl at startup, even if we do not use the object at runtime
  • The mentioned Test Double or Mock Object are similar to those in Jest as we have seen previously
  • How do we know that MyServiceImpl is the right object in all cases?

Separation of Main

  • To separate construction and use, we can move construction to main, or modules called by main
  • Now main will build the required objects, and pass them to the application

Factories

  • In something like an order processing system, it may be required that the application decides when an object gets created

Figure 11-1

  • LineItem instances add to an order
  • We can use an abstract factory, so that details of construction are separate from the application code, whilst maintaining functionality
  • Reminder: Clients will call creation methods of an abstract factory object, instead of creating products directly with a constructor call

Figure 11-2

Dependency Injection (DI)

MyService myService = (MyService)(jndiContext.lookup("NameOfMyService"));
  • True DI has a class which takes no steps to resolve dependencies
    • It provides setter methods/constructor arguments, to inject the dependencies
    • During construction, the DI container will use these dependencies
    • A DI container is a class that can instantiate and configure objects -Spring framework provides a DI container - https://spring.io/projects/spring-framework

Scaling Up

  • Continuing the analogy from before, as towns grow in size to cities, the roads will begin small, and gradually get widened over time, building will start small, and get taller
  • We wouldn't want to immediately jump to large roads/buildings, as there is no logical reason to, if it is not required yet
  • Systems also follow this methodology, in that they will evolve over time as requirements change
  • "Software systems are unique compared to physical systems. Their architectures can grow incrementally, if we maintain the proper separation of concerns"

Example of a poor architecture in which concerns are not separated:

  • Notes:
    • EJB (Enterprise JavaBean) is "a platform for building reusable, portable and scalable business applications in Java. A server-side software component that encapsulates the business logic of an application"
      • Entity Bean: "An in-memory representation of relational data, in other words, a table row"
// Listing 11-1: An EJB2 local interface for a Bank EJB
package com.example.banking;
import java.util.Collections;
import javax.ejb*;

public interface BankLocal extends java.ejb.EJBLocalObject {
    String getStreetAddr1() throws EJBException;
    String getStreetAddr2() throws EJBException;
    String getCity() throws EJBException;
    String getState() throws EJBException;
    String getZipCode() throws EJBException;
    void setStreetAddr1(String street1) throws EJBException;
    void setStreetAddr2(String street2) throws EJBException;
    void setCity(String city) throws EJBException;
    void setState(String state) throws EJBException;
    void setZipCode(String zip) throws EJBException;
    Collection getAccounts() throws EJBException;
    void setAccountS(Collection accounts) throws EJBException;
    void addAccount(AccountDTO accountDTO) throws EJBException;
}
  • Attributes for Bank address, and associated accounts (which would be handled by a separate Account EJB)
// Listing 11-2: The corresponding EJB2 Entity Bean Implementation
package com.example.banking;
import java.util.Collections;
import javax.ejb.*;
public abstract class Bank implements javax.ejb.EntityBean {
    // Business logic...
    public abstract String getStreetAddr1();
    public abstract String getStreetAddr2();
    public abstract String getCity();
    public abstract String getState();
    public abstract String getZipCode();
    public abstract void setStreetAddr1(String street1);
    public abstract void setStreetAddr2(String street2);
    public abstract void setCity(String city);
    public abstract void setState(String state);
    public abstract void setZipCode(String zip);
    public abstract Collection getAccounts();
    public abstract void setAccounts(Collection accounts);
    public void addAccount(AccountDTO accountDTO) {
        InitialContext context = new InitialContext();
        AccountHomeLocal accountHome = context.lookup("AccountHomeLocal");
        AccountLocal account = accountHome.create(accountDTO);
        Collection accounts = getAccounts();
        accounts.add(account);
 }
    // EJB container logic
    public abstract void setId(Integer id);
    public abstract Integer getId();
    public Integer ejbCreate(Integer id) { ... }
    public void ejbPostCreate(Integer id) { ... }
    // The rest had to be implemented but were usually empty:
    public void setEntityContext(EntityContext ctx) {}
    public void unsetEntityContext() {}
    public void ejbActivate() {}
    public void ejbPassivate() {}
    public void ejbLoad() {}
    public void ejbStore() {}
}
  • The business logic is tightly coupled to the EJB2 application 'container'
  • Makes it hard to test
  • Must provide lifecycle methods required by the container

Cross-Cutting Corners

  • You want to persist objects using the same strategy (naming conventions, file structure etc)
  • In practice, you spread similar code which implements the persistence strategy across many objects - cross-cutting concerns is used for concerns like this
  • EJB's architecture anticipated aspect-oriented programming (AOP)
  • In AOP, constructs named aspects determine which parts of the system should have their behaviour modified in a consistent way to align with a particular concern

Java Proxies

  • Suitable for simple situations like wrapping method calls in individual objects or classes
  • Below shows skeleton for a JDK proxy for persistence support in the Bank application
// Listing 11-3: JDK Proxy Example
// Bank.java (suppressing package names...)
import java.utils.*;
// The abstraction of a bank.
public interface Bank {
    Collection<Account> getAccounts();
    void setAccounts(Collection<Account> accounts);
}
// BankImpl.java
import java.utils.*;
// The “Plain Old Java Object” (POJO) implementing the abstraction.
public class BankImpl implements Bank {
    private List<Account> accounts;
    public Collection<Account> getAccounts() {
        return accounts;
    }
    public void setAccounts(Collection<Account> accounts) {
        this.accounts = new ArrayList<Account>();
        for (Account account: accounts) {
            this.accounts.add(account);
        }
    }
}
// BankProxyHandler.java
import java.lang.reflect.*;
import java.util.*;
// “InvocationHandler” required by the proxy API.
public class BankProxyHandler implements InvocationHandler {
    private Bank bank;
    public BankHandler (Bank bank) {
        this.bank = bank;
    }
    // Method defined in InvocationHandler
    public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
        String methodName = method.getName();
        if (methodName.equals("getAccounts")) {
            bank.setAccounts(getAccountsFromDatabase());
            return bank.getAccounts();
        } else if (methodName.equals("setAccounts")) {
            bank.setAccounts((Collection<Account>) args[0]);
            setAccountsToDatabase(bank.getAccounts());
            return null;
        } else {
        ...
        }
    }
    // Lots of details here:
    protected Collection<Account> getAccountsFromDatabase() { ... }
    protected void setAccountsToDatabase(Collection<Account> accounts) { ... }
}
// Somewhere else...
Bank bank = (Bank) Proxy.newProxyInstance(
 Bank.class.getClassLoader(),
 new Class[] { Bank.class },
 new BankProxyHandler(new BankImpl()));

These are all highlighted above

  • An interface Bank
  • Wrapped by the proxy
  • The proxy requires an invocationHandler

Pure Java AOP Frameworks

  • Proxies are used internally by many Java frameworks
  • Spring AOP is one of these (in which you write your business logic as Plain-Old Java Objects: POJO)

Typical Fragment of Spring Configuration File (app.xml)

<beans>
    ...
    <bean id="appDataSource"
    class="org.apache.commons.dbcp.BasicDataSource"
    destroy-method="close"
    p:driverClassName="com.mysql.jdbc.Driver"
    p:url="jdbc:mysql://localhost:3306/mydb"
    p:username="me"/>

    <bean id="bankDataAccessObject"
    class="com.example.banking.persistence.BankDataAccessObject"
    p:dataSource-ref="appDataSource"/>

    <bean id="bank"
    class="com.example.banking.model.Bank"
    p:dataAccessObject-ref="bankDataAccessObject"/>
    ...
</beans>
  • Each bean is nested with a domain object for Bank proxied (wrapped) by a data accessor object (DAO), which itself is proxied

Figure 11-3

  • A client when interacting, thinks they are invoking getAccounts() on a Bank object
  • In reality they are talking to the outer layer

In the application, a few line are needed to ask the DI container for the top-level objects in the system, as specified in the XML file.

    XmlBeanFactory bf =
        new XmlBeanFactory(new ClassPathResource("app.xml", getClass()))
    Bank bank = (Bank) bf.getBean('bank')

Listing 11-2 shows the Bank object re-rewritten in EJB3

// Listing 11-5: An EJB3 Bank EJB
package com.example.banking.model;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.Collection;
@Entity
@Table(name = "BANKS")
public class Bank implements java.io.Serializable {
    @Id @GeneratedValue(strategy=GenerationType.AUTO)
    private int id;

    @Embeddable // An object “inlined” in Bank’s DB row
    public class Address {
        protected String streetAddr1;
        protected String streetAddr2;
        protected String city;
        protected String state;
        protected String zipCode;
 }

 @Embedded
 private Address address;

 @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER,
    mappedBy="bank")
 private Collection<Account> accounts = new ArrayList<Account>();

 public int getId() {
    return id;
 }

 public void setId(int id) {
    this.id = id;
 }
 public void addAccount(Account account) {
    account.setBank(this);
    accounts.add(account);
 }
 public Collection<Account> getAccounts() {
    return accounts;
 }
 public void setAccounts(Collection<Account> accounts) {
    this.accounts = accounts;
 }
}
  • Cleaner than the original EJB2 code
  • Some/all persistence information can be moved, leaving a pure POJO

Test Drive the System Architecture

  • Decoupling architecture concerns allows us to test drive architecture
  • Not necessary to do Big Design Up Front (completing the program's design and perfecting it before the actual implementation begins)
  • This means we can start projects with 'naively simple' (but decoupled) architecture
  • Still require some general expectations of scope (avoid scope creep, use mvp), goals, and a project schedule

Optimise Decision Making

  • We know it is best to give responsibilities to the most qualified person
  • It is also best to postpone decisions until the last possible moment
  • "The agility provided by a POJO system with modularised concerns allows us to make optimal, just-in-time decisions, based on the most recent knowledge. The complexity of these decisions is also reduced"

Use Standards Wisely, When They Add Demonstrable Value

"Standards make it easier to reuse ideas and components, recruit people with relevant experience, encapsulate good ideas, and wire components together. However, the process of creating standards can sometimes take too long for industry to wait, and some standards lost touch with the real needs of the adopters they arte intended to serve"

Systems Need Domain-Specific Languages

  • DSL: Separate, small scripting languages or API's in standard languages
  • Permit code to be written in a structured form
  • When used correctly, raise the abstraction level above code idioms and design patterns

"Domain-Specific Languages allow all levels of abstraction and all domains in the application to be expressed as POJO's, from high-level policy to low-level details"

Conclusion

  • Systems must be clean too
  • If domain logic gets obscured, quality suffers as bugs find it easier to hide
  • If agility is compromised, productivity is lost and TDD benefits are gone
  • Intent should be clear
  • Always use the simplest thing that can possible work