Monday, 13 January 2025

RACI chart

Previously we discussed the various steps a product manger is involved in during the innovation and operation phases of a product lifecycle. One may wonder how one person could be capable of so many aspects of product development and management? The answer is they're not. Product mangers need to be a jack of all trades, they need familiarity in all aspects of the product process.

Facilitation: Product mangers, need a strong background in driving initiatives forward, they need to be comfortable speaking in front of senior management; this requires excellent public speaking capabilities along with organisational abilities and the ability to mediate multiple points of view and align a group along one objective. 

Design: Product managers must have a strong understanding of UX design, they need to be able to understand how research is conducted, that the right research is conducted, and that the right audience is being targeted. Product managers also need to often act as the middle man between engineering and design ensuring that designs are validated as feasible and optimal.

Engineering: Product managers must be able to engage with engineering teams, ensuring that engineering understands the designs as intended. Product managers also need to provide enough guidance to ensure that the product is not over or under engineered; factoring in budget and timelines.

Marketing: Product managers need to engage with marketing teams, for an internal initiative, it'll be change management, regardless, the Product manager must specify who the target audience is, what is the value proportion, that the marketing or change process aligns with the overall vision and mission of not just the product/service, but the organisation.

Project management: product managers often times need to work very closely with project managers or even take one many of those responsibilities themselves working cross functionally to ensure the product/service is delivered on time and on budget.

Customer support: Product managers need to be able to ensure that Customer support has everything they need to engage with customers confidently and effectively. 

Ideally product managers have a familiarity of all of the above, but will generally possess a proclivity towards the aspects which align with their background. There is no right or wrong, just which strengths are appropriate to which context.

The product mangers job at the end of the day is to guide the innovation and operation processes, while making sure that each contributor and stakeholder aligns to vision and mission of the product or service; with so many moving parts it's very simple lose track of who is responsible for what. Luckily we can borrow a simple artefact from project management known as the RACI chart.

Responsible: The individual or group who is/are responsible for the actual work of a deliverable, they are the ones doing the "work". 

Accountable: refers to the individual who leads those responsible, this individual must have the authority over those responsible and will be judged by the final output, for example the marketing strategy, their will most likely be a number of contributors, but only one senior who is able to identify what should be done, and what done looks like.

Consulted: refers to the individuals(s) who have expertise or will be impacted downstream and should be consulted throughout each process.

Informed: refers generally to stakeholders who want/need to be kept up to date with progress.

Throughout the innovation and operational processes of a product's lifespan, their can be dozens if not hundreds of tasks; a RACI chart allows us to keep track of who is/was responsible, accountable, consulted and informed throughout the process. Depending on the Product/Service this chart can span anywhere between one to dozens of pages.

A RACI chart could look something like this

The tasks and roles above are not important, it's the concept of clearly being able to understand which role is responsible, accountable, consulted, or informed regarding which task. A RACI chart will target the appropriate communications to the appropriate audience, ensuring that the right people have the information they need while minimising information overlaod. 

Friday, 10 January 2025

Product innovation

There are 8 steps which increase the likelihood that a product will successfully enter the market, though the order of the steps makes logical sense, in practice it is normal to move back and fourth between steps; a two steps forward, one step back sort of situation. This is due to the fact that as the product team moves from an idea through to a prototype they gain valuable insights into the market, the technology, the value and looping those insights back into previous decisions only makes sense. The primary objective is to launch a successful product, whatever pragmatic approach maximises that goal is only beneficial

Conception

Product innovation begins with initial conception, during this phase the initial idea or ideas are explored and shortlisted, the aim is to align business value with innovation. During this phase the goal is to identify business stakeholders, align on a single mission statement, and identify a potential market.

Validation

A business case needs to be created, its aim is to clearly articulate the value of the product, the market need, a competitive analysis if applicable, and a clear vision of how the product has the potential to generate value for the organisation. This business case is not a guarantee that the product will be a success, it is a sales pitch to leadership to show that the proposed product's value outweighs any opportunity cost.

Investigation

After the business case is validated, customer research needs to be conducts, how this is accomplished depends on the target audience and the organisational capabilities. The goal is to contrive a customer value proposition.

Design

Once the product manager understands what the value to both the business and market should be, the design process can proceed. This process generally follows the double diamond design thinking methodology, in short it's an iterative process which follows a divergent and convergent pattern of thinking; iterating over investigation, synthesis, exploration, design. this phase bleeds between the previous investigation and subsequent prototype phases.

Prototyping

After a rigorous design process, the product should be qualified before launch, a or multiple prototypes should be built and used to beta test the product with real customers, to get real feedback and qualify that the product will be adopted by the market.

Goto market

In parallel with the prototyping phase, a marketing strategy as well as a product roadmap need to be defined. The product roadmap lays out at a high level which epics/features will be developed and delivered, often times product rollouts are planned 2 to 5 iterations into the future. These iterations may not be 100% qualified, however they provide a broad brushstroke as to the direction of the product, the planned epics/feature for these iterations can, and should change as the organisation learns more from the market and their customers.

Build

Assuming the prototype phase went successfully it is time to produce the real thing, during this time as production capabilities are being ramped up, the product manger works on a launch plan, reviews the marketing strategy, focuses all their energy on the introduction of the product into the market.

Launch

It is important to remember though the product manager is responsible for all of the various plans and strategies throughout the innovation process, they are not the ones who actually produce all of theses artifacts. Product managers rely on change managers, marketing manager, logistics experts, designers, SMEs, etc; they are responsible for everything coming together.

Conclusion

In conclusion the steps above will not ensure success, but they will increase the likilihood of it. It is also important to keep in mind that regardless of how much capital has been spent, on product innovation, it is infinetly cheaper to fail before building and launching a product than after, always ask yourself does this realistically have a chance of success?

Saturday, 4 January 2025

Product lifecycle

The product lifecycle is made up of four high level phases, during the operational life of a product it will move through these phases from market introduction through to product retirement. 

Introduction
This is the phase which the product first hits the market, during this phase the Product manager informs the market, makes the product available to its target audience, collects feedback from early adopter on how to potentially improve the product and iterates quickly and strategically.





Growth
During this phase the market begins to adopt the product and demand increases, potential competitors may also enter the market with similar products; the Product manager must step up marketing efforts as well as ensure that the product is available. The goal is to continue to iterate the product with new features based on customer and market input and capture as much of the market share as possible.
Maturity
As the product enters this phase, growth tends to flatten out, most of the market has been captured by yourself or competitors, there are no more untapped customers, during this phase the marketing strategy changes from acquiring new customers, to stealing your competitors customers. The product generally has iterations with fewer new features.



Decline
As the market sentiment changes or new disruptive technologies begin to emerge the product enteres its decline phase, during this phase the product manger's goal is to timely and gracefully remove the product from the market while developing the subsiquent iteration to provide the marekt with the same value, however either leverging the new disruptive technology or adhering to new market sentiment.
As the product moves through the four phases the demand for the product grows, levels off, and finally begins to decline, unlike a project manager, the product manager would see the initiative through from its inception all the way to its retirement, here we only focus on the operational lifecycle, in my next post I'll discuss at a high level how to conceive, validate, design, and launch a product.

All products will face an end-of-life market event, that is to say as the market changes, be it for socio-economical reasons or due to disruptive technology almost all products will face retirement. A savvy product manager will foresee this shift in the market before it happens, and begin a wind-down of the product while starting the innovation process for its replacement. 

Products are not valuable in themselves, their value extends to the service they provide. Often times as in the classic example of vinyl records, a-tracks, tapes, music CDs, Digital music players, and now streaming services the demand for the service was always the same, however the mechanism which the market consumes the service shifts as new technology emerges.

Another example is the horse drawn buggy, to the combustion engine, and now electric car, the service the market demands has always stayed the same, however the medium has changed, the difference between these two examples, is that the catalyst for the electric car is arguably not technology. The number one driver is consumer concern over the impact of carbon emissions, the technology just makes the transition feasible; without the driver, despite the technology existing the product would fail.

The above is a high level abstraction, the four phases are far more involved and iterative in nature. As the product moves through its phases the rate of innovation slows and the source of primary insights shifts, though one should never ignore any insights regardless of their source, one should keep in mind the Sigmund Freud's theory that the conscious and unconscious mind imply that what people think, say, and do can diverge due to psychological conflicts and repressions. Or more succinctly:

What people think they do
What people say they do 
What people actually do


are three distinct things

For this reason as the product matures, we begin to rely more heavily on data and analytics, rather than interviews and surveys. That is not to say that those insights are not valuable, however they should be validated with data once it is available. The reason this data is more valuable in the growth and maturity phases, is due to the fact that often times when analysing early adopters one will find that their is only one or two distinct personas, as the product matures and adoption grows; we gain more insights from various personas providing the evidence to cast a wider net.

In the early introduction phase iterations will generally be rapid in nature, what "rapid" means depends on the industry, in software this could mean monthly if not weekly, however in the transportation industry this could mean annually. During this phase it's best to gain insights from early adopters, these insights can propel your product into an exponential growth phase, or they could result in the death of your product. It is important during the introduction phase to focus on aligning iterations to the value proposition. There will be a lot of early adopter feedback, however it is important to qualify this input with long term vision and strategy while maintaining early adopter enthusiasm.

Ideally the product will gain traction and transition to an exponential growth phase, this phase is often the result of strong marketing and early adopter satisfaction. During this phase one should focus on market analysis, look for customers you want and understand how your product can fill the need(s) they have. Iterations will slow down, again depending on the industry, however the goal will be to capture as much market share as possible without frustrating existing customers with changes.

Over time your customer base will grow, competitors will introduce their own products which will solve the same customer problem. During this maturity phase the market becomes saturated, there are no more new "virgin" customers; during this phase the product manager must walk the tightrope of keeping existing customers satisfied, while trying to entice the remaining market to switch to their product. During this phase one should rely more heavily on analytics, how are customers using the product, how can you improve engagement, how can you offer more value than your competitors. During the maturity phase enhancements may be annual and they should be evidence-driven and focused on maintaining customers, while looking to entice competitor's clients to switch.

Finally the product will eventually enter the decline phase, this phase can catch even experienced product managers off guard, most likely a disruptive technology emerges and a new product enters the market which provides the same value proposition, but faster, better, cheaper or any other reason which causes the market to shift to the alternative. Ideally the product manager foresaw this disruptive technology or shift in market sentiment and has already been working on transitioning their existing customers to the next iteration leveraging the new disruptive technology or adhering to market sentiment. Enhancements will be strategic in nature, aimed at transitioning existing customers to the next iteration of the product.

Conclusion

The operational product lifecycle is a minefield, initially it focuses on rapid iteration and entering the growth phase as soon as possible and ideally before competitors. Once the product reaches maturity and the market is saturated, the aim is to maintain customers, while enticing new ones to switch to your product. Finally it is essential to be prepared for an end-of-life market event and have a transition strategy to ride the next wave of innovation or market sentiment. In the ideal world it is your organisation that provides the innovation to disrupt the market.

Thursday, 2 January 2025

Stacy matrix

The Stacey Matrix is a framework developed by Ralph Stacey, a British organizational theorist. It's purpose is to help organizations understand the level of uncertainty and complexity in decision-making. It provides guidance on which approaches to problem-solving and management are most suitable in different situations.


The Stacey matrix groups potential projects into four main categories:

Simple: The requirements are clear, there is consensus not only what needs to be built, but the technology that will be used. Projects like this are generally best delivered using a traditional sequential approach. 

Complicated - political: Projects fall into this category when stakeholders agree that something needs to be done, however there isn't consensus on what to do; in situations like this it is best to fall back on UX-research and traditional analysis methods, to further investigate the problem, moving forward without a common vision of success is il-advised.

Complicated - technical: If the stakeholders agree on what needs to be done, however they do not agree on how it should be delivered and choosing the appropriate technology through investigation is not possible, it is best to preform a proof of concept. In a POC the technical doubt is targeted, the development team demonstrates that the chosen technology is fit for purpose. This technical doubt can come in the form of complex features, performance benchmarks, security, or even usability. 

Complicated: whether the the complication stems from a political or a technological challenge, it is best to resolve this and either classify the project as Simple or Complex, this no mans land is often where projects land, generally projects that start without a clear objective or a validated technology have a high risk of failure, irregardless of methodology.

Complex category: these are projects that despite best efforts, there remains a significant level of risk associated with the unknown, either what needs to be delivered or how it needs to be delivered. Projects which fall into this "complex" category are well suited for an iterative approach, the specific methodology, be it Scrum, Kanban, MVP, etc needs to be assessed on a case by case basis.

Chaotic category: initiatives that fall into this category are best avoided, with an extreme amount of uncertainty in regards to both technology as well as scope, it is best to resolve this level of doubt or to consider the opportunity cost of pursuing such a high risk project. Unless absolutely necessary 

To properly classify a project we have to assess the level of certainty of what needs to be accomplished along with how it will be accomplished. 

Assessment of what

  1. Do we know exactly with certainty that we are targeting the problem and not a symptom?
  2. Do we know the business value of the Solution?
  3. Do we have a clear value proposition?
  4. Do we know exactly what the outcome will look like?
  5. What evidence do we have supporting the above?

Assessment of how

  1. Do we know exactly what technology stack will the solution be implemented with?
  2. Do we have infrastructure diagrams proving the solution is feasible?
  3. Do we have software architectural diagrams proving the solution is feasible?
  4. Do we know with absolute certainty that the technology is fit for purpose?
  5. Do we have the sign off of the technical team which is to implement the solution?
If we can answer yes to all of the above, then the project is an excellent candidate for a traditional sequential approach, however if the answer to just one of the above is no, then an iterative approach maybe more effective and less risky. If we don't know one of the above, then further research needs to be conducted.


Thursday, 22 August 2024

Proxy pattern

The proxy pattern provides a surrogate or placeholder for another object in order to control access to it. In essence what that means is that the proxy acts as a middle man between the target and the source,  this may sound familiar; it is more or less exactly what the facade, adapter, and decorator patterns do. The difference however is in the intent or reason behind these patterns, in the case of the proxy pattern we have several different types of proxies:

Remote proxy: A remote proxy acts as a local representative for an object located in a different address space, such as on a different machine or server. It facilitates communication and data transfer between the client and the remote object. It abstracts the details of network communication, allowing the client to interact with a remote object seamlessly.

Virtual proxy: A virtual proxy acts as a placeholder for an object that is expensive to create or load. It delays the instantiation or loading of the actual object until it is genuinely needed. Provides a “stand-in” for an actual object, which is instantiated only on demand.

Protection proxy: A protection proxy controls access to an object by implementing additional security, permissions, or authentication mechanisms. It determines whether the client is authorised to interact with the object. It acts as a gatekeeper, allowing or denying access based on specific rules.

Logging proxy: Records details about the interactions with the real object, often for debugging, monitoring, or analytics purposes.

Caching proxy: A caching proxy stores frequently accessed data or responses to improve performance and reduce resource usage.

To name a few, despite the difference in intent they accomplish their goal in the same way, the key concept to a proxy is that it eavesdrops on a call. Let's hypothetically say that we have the following configuration.


Above we depict a contrived, but fairly common configuration, we have an object which we create locally and a rest service to load and save that object to and from some sort of cloud repository. Now as before let's imagine that it is a significantly more intricate, and let's also say that perhaps these objects are significantly more intricate and take up much more memory, but don't change very much. To solve a problem such as that a caching proxy would make perfect sense. Our UML would look like the following.


Exactly like the decorator pattern, the proxy has this "is a" and "has a" relationsip, however unlike the decorator pattern the intent of a proxy is not to add or change functionality, but to intercept, do something, and then pass the original message back to the caller. 

In code it could like like the following without the proxy



interface IIngredient {
id: string;
name: string;
unit: number;
pricePerUnit: number;
}

class Ingredient implements IIngredient {
id: string;
name: string;
unit: number;
pricePerUnit: number;

constructor(id: string, name: string, unit: number, pricePerUnit: number) {
this.id = id;
this.name = name;
this.unit = unit;
this.pricePerUnit = pricePerUnit;
}

getPrice(): number {
return this.unit * this.pricePerUnit;
}

getDescription(): string {
const quantity = this.unit;
const parts = `${quantity < 1 ? 'part' : 'parts'}`;
const name = this.name;

return `${quantity} ${parts} ${name}`;
}
}

interface IIngredientService {
url: string;

getIngredient(id:string): Promise<Ingredient[]>;
createIngredient(ingredient: Ingredient): Promise<Ingredient>;
updateIngredient(ingredient: Ingredient): Promise<Ingredient>;
}

class IngredientService implements IIngredientService {
url: string;

constructor(url: string) {
this.url = url;
}

async getIngredient(id: string): Promise<Ingredient[]> {
const response = await fetch(`${this.url}/ingredients/${id}`);
const data = await response.json();
return data.map((item: any) => new Ingredient(item.id, item.name, item.unit, item.pricePerUnit));
}

async createIngredient(ingredient: Ingredient): Promise<Ingredient> {
const response = await fetch(`${this.url}/ingredients`, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(ingredient)
});
const data = await response.json();
return new Ingredient(data.id, data.name, data.unit, data.pricePerUnit);
}

async updateIngredient(ingredient: Ingredient): Promise<Ingredient> {
const response = await fetch(`${this.url}/ingredients/${ingredient.id}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(ingredient)
});
const data = await response.json();
return new Ingredient(data.id, data.name, data.unit, data.pricePerUnit);
}
}


and then if we add the proxy it could like like the following


class ProxyCache implements IIngredientService {
url: string;
private cache: Map<string, Ingredient[]> = new Map();
private ingredientService: IngredientService;

constructor(url: string) {
this.url = url;
this.ingredientService = new IngredientService(url);
}

async getIngredient(id: string): Promise<Ingredient[]> {
if (this.cache.has(id)) {
return this.cache.get(id)!;
} else {
const ingredients = await this.ingredientService.getIngredient(id);
this.cache.set(id, ingredients);
return ingredients;
}
}

async createIngredient(ingredient: Ingredient): Promise<Ingredient> {
const newIngredient = await this.ingredientService.createIngredient(ingredient);
this.cache.set(newIngredient.id, [newIngredient]);
return newIngredient;
}

async updateIngredient(ingredient: Ingredient): Promise<Ingredient> {
const updatedIngredient = await this.ingredientService.updateIngredient(ingredient);
this.cache.set(updatedIngredient.id, [updatedIngredient]);
return updatedIngredient;
}

async savetoCache(ingredient: IIngredient): Promise<void> {
this.cache.set(ingredient.id, [ingredient as Ingredient]);
}

async loadFromCache(id: string): Promise<IIngredient[]> {
if (this.cache.has(id)) {
return this.cache.get(id)!;
} else {
throw new Error('Ingredient not found in cache');
}
}
}


notice that our proxy class, makes no changes to the request, it simply stores it in a cache, then on a subsequent call it checks if the request is in the cache before making a network call.

Decorator pattern

The Decorator Pattern is a design pattern used to dynamically add new functionality at runtime to an object, without altering their structure. This ability to modify an objects behaviour dynamically is the advantage over using class inheritance. When this need to dynamically alter an object's functionality does not exist than the decorator pattern is most likely over engineering a solution. 

It is most appropriate for situations in which either you find yourself creating dozens of classes that are extremely similar, but have slightly variation functionality (known as class explosion), or in situations where their is a high likelihood that you will need to append existing functionality in the future while maintaining its original capabilities.

You can think of decorators as wrappers, for a concrete class, they can add functionality or modify existing behaviour. 

In the above illustration, you can start to visualise how the decorator works, it wraps a concrete class and either modifies functionality or adds functionality, in theory you could have an unlimited number of wrappers. Below is a UML representation of how this works


Notice that the IDecorator interface, not only implements or extends either the base interface or concrete class, but it also has a reference to to that base definition or implementation. This creates both a "Has a" and "Is A" relationship with the base concrete class. This provides us with two advantages, firstly and most commonly this allows us to dynamically add functionality and behaviour to existing classes at runtime, the most common example of this beverage pricing and naming. 

The most common example used to teach the decorator pattern is a rather contrived coffee house example. The coffeehouse example lends itself very well to illustrate how this pattern works, however it's not really how one would leverage this pattern in production code. Instead of the coffee example, I'm going to create a cocktail one. I realise that it sounds exactly the same, and to a degree it is. The reason I'm going with a cocktail example is because after the we understand the fundamentals of how the decorator pattern works. The cocktail example acts as an excellent jump off point to emphasise a real world problem which this pattern is meant to solve.

Let's start with an ingredient interface and class, each beverage will be made up of a combination of ingredients which will determine the price of the finished drink.


interface IIngredient {
name: string;
pricePerUnit: number;
unit: number
}

class Ingredient implements IIngredient {
name: string;
pricePerUnit: number;
unit: number

constructor(name: string, price: number, unit: number) {
this.name = name;
this.pricePerUnit = pricePerUnit;
this.unit = unit;
}
}

At the very base level, whether we are talking wine, cocktails, mocktails, coffee, or Italian sodas, every beverage is going to have a price, and a recipe, at this point it would make sense to include an array of Ingredients in our Beverage class, however it would completely defeat the need for the decorator pattern, for this reason, though this example articulates the use of the decorator pattern rather well, it's not a practical one. 


abstract class Beverage {
abstract getPrice(): number;
abstract getRecipe(): string;
}

Notice that anything that inherits from the abstract Beverage class is going to have to implement both the getPrice and getRecipe functions. Next let's create a cocktail class which extends the abstract Beverage class and implements the abstract methods getPrice and getRecipe().


export default class Cocktail extends Beverage {
name: string;

constructor(name: string) {
super();
this.name = name;
}
getPrice(): number {
return 0;
}

getRecipe(): string {
return `A ${this.name} made with`;
}
}

With that complete we have a UML diagram like so


notice that our Ingredient interface and class are not really attached to anything, let's now introduce the idea of a decorator and fix that.


abstract class CocktailDecorator extends Beverage {
base: Beverage;
ingredient: IIngredient;

constructor(base: Beverage, name: string, price: number, oz: number) {
this.base = base;
this.ingredient = new Ingredient(name, price, oz);
}

getPrice = (): number => {
const price = this.ingredient.ozPrice;
const quantity = this.ingredient.ozQuantity

const thisPrice = price * quantity;
const basePrice = this.base.getPrice();

return basePrice + thisPrice;
}

getRecipe = (): string => {
const prefix = this.base.getRecipe();
const conjunction = this.base instanceof Cocktail ? ":" : ",";
const quantity = this.ingredient.ozQuantity;
const parts = `${quantity < 1 ? 'part' : 'parts'}`
const name = this.ingredient.name;

return `${prefix}${conjunction} ${quantity} ${parts} ${name}`;
};
}

Here we have this interesting "is a" and "has a" relationship, this is what allows us to wrap not just anything that inherits from the abstract Beverage class, but it also lets us modify the functionality of anything defined in the Beverage class. notice that our decorator uses the ingredient to calculate the price as well as the recipe.

Lets create three variations of our decorator


export class VodkaDecorator extends CocktailDecorator {
constructor(base: Beverage, oz: number) {
super(base, "Vodka", 4, oz);
}
}

export class GinDecorator extends CocktailDecorator {
constructor(base: Beverage, oz: number) {
super(base, "Gin", 5, oz);
}
}

export class VermouthDecorator extends CocktailDecorator {
constructor(base: Beverage, oz: number) {
super(base, "Vermouth", 1, oz);
}
}

Now we can instantiate a either a gin or vodka martini like so


const vodkaMartini = new VermouthDecorator(new VodkaDecorator(new Cocktail("Vodka martini"), 2), .25);
const ginMartini = new VermouthDecorator(new GinDecorator(new Cocktail("Gin martini"), 2), .25);

This demonstrates how we can use the decorator pattern to append functionality to an existing base implementation, notice that in this case we create this daisy chain of classes which implement and have an instance of the Beverage class, down to the base concrete implementation which only implements the abstract Beverage class. 

One thing to keep in mind is that nesting order matters, when we call 

console.log(vodkaMartini.getRecipe());
console.log(vodkaMartini.getPrice());
console.log(ginMartini.getRecipe());
console.log(ginMartini.getPrice());

Each getRecipe and getPrice function is called from the outer down to the inner wrapper, this is why when we defined the getRecipe class in the base CocktailDecorator we leverage this idea of a prefix, so that the concretes GetRecipe implementation would be at the front of the output.


One thing to keep in mind when using the decorator pattern, any functionality that has been altered by the decorators is called from the outer ring down to centre. 

Our final UML looks something like this:


As mentioned before, this is a contrived example and though it illustrates how the decorator pattern works, it does not provide a realistic example of what context to actually use it in. For a problem like this, it would be far more reasonable for the cocktail class to be instantiated with an array of ingredients with an option to add/remove ingredients; so let's refactor our code to more appropriate version



notice how we've simplified our models 


interface IIngredient {
name: string;
ozPrice: number;
ozQuantity: number;
}

abstract class Priced {
abstract getPrice(): number;
abstract getDescription(): string;
}

export class Ingredient extends Priced implements IIngredient {
name: string;
ozPrice: number;
ozQuantity: number;

constructor(name: string, ozPrice: number, ozQuantity: number) {
super();
this.name = name;
this.ozPrice = ozPrice;
this.ozQuantity = ozQuantity;
}

getPrice(): number {
return this.ozPrice * this.ozQuantity;
}
getDescription(): string {
const quantity = this.ozQuantity;
const parts = `${quantity < 1 ? 'part' : 'parts'}`
const name = this.name;

return `${quantity} ${parts} ${name}`;
}
}

export default class Cocktail extends Priced {
name: string;
ingredients: Ingredient[]

constructor(name: string, ingredients:Ingredient[]) {
super();
this.name = name;
this.ingredients = ingredients;
}
getPrice(): number {
return this.ingredients.reduce((sum, item)=> sum + item.getPrice(), 0);
}

getDescription(): string {
return this.name + ': ' + this.ingredients.reduce((recipe, item)=> recipe + `, ${item.getPrice()}`, "");
}

addIngredient (ingredient: Ingredient) {
const i = this.ingredients.find(i => i.name = ingredient.name);
if(i)
i.ozQuantity += ingredient.ozQuantity;
else
this.ingredients.push(ingredient);
}

removeIngredient(ingredient: Ingredient){
const i = this.ingredients.findIndex(i => i.name = ingredient.name);
if(i > -1) {
const t = this.ingredients[i];
t.ozQuantity -= ingredient.ozQuantity;
if(t.ozQuantity < 0)
this.ingredients.splice(i,1);
}
}
}

we've significantly reduced the complexity of our code, now let's create a menu object; makes sense right, it's reasonable for a predefined menu of cocktails to exist for patrons to choose from with the ability to modify them, swap out the alcohol for a preferred brand, or add a double shot.


class Menu{
cocktails: Cocktail[]
constructor(cocktails: Cocktail[]){
this.cocktails = cocktails;
}

listCocktails(): [string, number][]{
return this.cocktails.map(c=> [c.getDescription(), c.getPrice()])
}
}

Let's say we go live with our solution, and it works as far as we know, we can define cocktails, their ingredients, and generate a menu. Now hypothetically let's say someone orders a dirty martini with 10 extra shots of vodka, well that's fine and dandy, but what about glasses? don't they have a capacity? now our example is rather trivial and i would say forget the open/closed principle and just refactor the code; and in this context no one would disagree with me, however in financial software that has thousands of legacy lines of code, no one would let you make that change for fear of introducing more bugs. 

Now for an intricate situation like that it would make sense to introduce a decorator, this way we can ensure that the original code continues to function, but we can now extend our system without risking legacy code.


export class GlassDecorator extends Cocktail{
base: Cocktail;
maxVolume: number;

constructor(base: Cocktail, maxVolume: number) {
super(base.name, base.ingredients);
this.base = base;
this.maxVolume = maxVolume
}

getFilledVolume(): number{
return this.base.ingredients.reduce((sum, i)=> sum + i.ozQuantity, 0);
}

override addIngredient(ingredient: Ingredient): void {
let filledVolume = this.getFilledVolume();
if(ingredient.ozQuantity + filledVolume > this.maxVolume)
throw Error("the glass does not have enough volume");
this.base.addIngredient(ingredient);
}
}

Our final UML looks like the following.


We can now use our Cocktail class and Glass decorator like so, :

const ginMartini = new Cocktail("gin martini", [gin, vermouth]);
const glass = new GlassDecorator(ginMartini, 5);

glass.addIngredient(new Ingredient("ice", 0, 4))

ginMartini.getDescription();
ginMartini.getPrice();

In conclusion the decorator pattern is useful when you want to add functionality to legacy code, but you want to mitigate any chance of impacting existing code. The change in behaviour impacts from the outside in. meaning that any function that is overridden or appended is first executed on the outer ring of decorators first.


A quick note on UML class diagrams, I have been showing code before the Class diagram. This is not how my actual or anyones work flow should go, software engineers use UML diagrams to plan out their code, to optimally structure it before writing code.