The principles of SOLID are guidelines that can be applied to software development to improve legibility and maintainability of software applications. This article by Stefano Tempesta, MCC & MVP, explores best practices and design patterns for developing SharePoint SOLID Web Parts in TypeScript, using the new client-side SharePoint Framework, and improving compliance to the five SOLID principles.
Being SOLID
The term SOLID is a mnemonic acronym for five design principles intended to make software designs more understandable, flexible and maintainable:
- Single responsibility: An object should have only a single responsibility.
- Open/closed: An object should be open for extension but closed for modification.
- Liskov substitution: An object should be replaceable with instances of its subtype without altering the correct functioning of the software.
- Interface segregation: When different behaviours are expected in an object, the object should implement many specific interfaces rather than one general-purpose interface.
- Dependency inversion: An object should not have direct dependency on other concrete objects, but only on abstractions.
Typically, these software design principles are applied to large software development projects in order to guarantee a higher level of maintainability of the software, and loose object coupling, which encourages a microservice-oriented architecture. But nothing prevents us from being SOLID also in relatively smaller applications such as SharePoint web parts.
A not so SOLID web part
By following the clearly described steps for building your first SharePoint client-side web part, we can easily create a web part with the SharePoint Framework in a few minutes. However, let me tell you, this is just a very simple web part, far from being SOLID-compliant. Let’s have a look at what the pitfalls are, or “code smells”, of such simple web part.
Let’s assume we want to build a web part that shows some famous quotes, stored in a SharePoint custom list called “Quotes”, which contains a field for the Author and a field for the Quote itself. The first step, is to create a new folder and the run the Yeoman SharePoint Generator. Yeoman will install the required dependencies and it will scaffold the solution files along with the web part. We call this web part “MyQuotes” and we choose not to use any JavaScript framework for the purpose.
A few minutes later, Yeoman is done. We can open the solution with VS Code (or any other TypeScript editor) and add the code for reading the quotes from a SharePoint list.
The MyQuotesWebPart.ts file in the src\webparts\myQuotes folder defines the main entry point for the web part. TheMyQuotesWebPart class extends the BaseClientSideWebPart. Any client-side web part should extend the BaseClientSideWebPart class to be defined as a valid web part, and this base class defines the minimal functionality that is required to build a web part with SPFx.
The render() method is used to render the web part inside a page, as part of its DOM (and not as an iframe, as it happens for traditional SharePoint add-ins). Here, in the render method, is where we want to make the necessary changes to display our famous quotes. We start by changing the title of the web part to “Famous Quotes”, and then adding a container with id = “quotesContainer” to use to fill with data coming from a SharePoint list. This <div> is populated by the method renderData(), invoked immediately after.
public render(): void { this.domElement.innerHTML = ` <div class="${styles.myQuotes}"> <div class="${styles.container}"> <div class="ms-Grid-row ms-bgColor-themeDark ms-fontColor-white ${styles.row}"> <div class="ms-Grid-col ms-lg10 ms-xl8 ms-xlPush2 ms-lgPush1"> <span class="ms-font-xl ms-fontColor-white">Famous Quotes</span> <div class="ms-font-l ms-fontColor-white" id="quotesContainer"></div> </div> </div> </div> </div>`; this.renderData(); }
Local and live environments
SharePoint Workbench gives us the flexibility to test web parts in our local environment and from a SharePoint site. SharePoint Framework aids this capability by helping us understand which environment our web part is running from by using the Environment class. This is what renderData() does to differentiate between a local environment and an online environment. Depending on the environment type, two different methods are invoked:
- getRealData() to retrieve list items from the SharePoint “Quotes” list using the helper class spHttpClient to execute REST API requests against SharePoint.
- getMockData() to return sample quotes when testing our web part locally.
A third private method, renderQuotes() generates the HTML code for rendering a list of quotes. This is irrespective on the environment type, as data, either mock or real, is obtained before invoking this method.
private renderData(): void { if (Environment.type === EnvironmentType.SharePoint || Environment.type === EnvironmentType.ClassicSharePoint) { this.getRealData().then((response) => { this.renderQuotes(response.value); }); } else if (Environment.type === EnvironmentType.Local) { this.getMockData().then((response) => { this.renderQuotes(response.value); }); } }
Mocking data
We need to define an interface that models the SharePoint list items that represent a quote. We actually need two interfaces:
- IQuote, which defines the Author and Quote fields that we want to display in the web part. This represents a single quote.
- IQuotes, which represents a collection of quotes.
export interface IQuotes { Quotes: IQuote[]; } export interface IQuote { Author: string; Quote: string; }
To see sample quotes in the local Workbench, we need an object that returns mock data. We can define this object in a separate TypeScript file, say “MockData.ts”, and then reference this file in MyQuoteWebPart.ts. MockData.ts itself refences (i.e. import in TypeScript) the IQuote interface defined in MyQuotesWebPart.
import { IQuote } from './MyQuotesWebPart'; export default class MockData { private static _items: IQuote[] = [ { Author: 'Author 1', Quote: 'Quote 1' }, { Author: 'Author 2', Quote: 'Quote 2' }, { Author: 'Author 3', Quote: 'Quote 3' }]; public static get(): Promise<IQuote[]> { return new Promise<IQuote[]>((resolve) => { resolve(MockData._items); }); } }
import MockData from './MockData';
The getMockData() method returns a list of sample quotes defined in the MockData object.
private getMockData(): Promise<IQuotes> { return MockData.get() .then((data: IQuote[]) => { var listData: IQuotes = { Quotes: data }; return listData; }) as Promise<IQuotes>; }
Displaying the web part
Quotes are rendered in HTML and added to a container in the web part DOM by the renderQuotes() method. This method loops the IQuote array and, for each item, defined the HTML code for showing a quote and its author.
private renderQuotes(items: IQuote[]): void { let html: string = ''; items.forEach((item: IQuote) => { html += ` <div>${escape(item.Quote)}</div> <div class="${styles.author}">${escape(item.Author)}</div> `; }); const listContainer: Element = this.domElement.querySelector('#quotesContainer'); listContainer.innerHTML = html; }
We can now open the local SharePoint Workbench with the “gulp serve” command and admire our web part in all its beauty!
After deploying the web part to a SharePoint site, we can open the SharePoint Workbench hosted in SharePoint and see real quotes defined in the “Quotes” custom list.
The code smells
So far so good, the web part works as expected, except it’s not so good, really. At least, it’s not SOLID good. These are the violations to the SOLID principles that I can identify for this web part:
- No single responsibility in the MyQuotesWebPart. This class has methods for retrieving data, from both local and live environments, as well as displaying it. This should be done in two separate objects, one to read quotes from the SharePoint list, and the other one to write (display) quotes on screen.
- The web part class is tightly coupled to the mock data object and the Quotes custom list. There is a direct dependency on these objects. If they don’t exist, the web part won’t work.
- The if condition on the environment type checking for local or SharePoint environment prevents abstraction of the dependency (hard reference to spHttpClient to retrieve live data) and its replacement with a subtype, as a mock providing sample data should be.
In merit to the third point, basically providing mock data should not be a switch condition but an injection of a subtype of the real data object. So, it’s time to refactor the web part to be more SOLID!
Refactoring to SOLID
The first change to apply is a simple separation of files. Instead of adding the definition of the IQuote and IQuotes interfaces in the same TypeScript file where the web part object is defined, let’s separate them into a QuoteContracts.ts file. We’ll also add a DataReader.ts file the contains the definition of the IDataReader interface and DataReaderFactory class that we are going to use in the web part to abstract the dependency on the actual data store, and then the two implementations for reading quotes from the “Quotes” SharePoint custom list (SPDataReader.ts) and its subtype MockDataReader (in MockDataReader.ts). The bad code of the web part is in the MyQuotesWebPart.BAD.ts file, just for comparison.
We now want to remove the hard-coded dependency to SharePoint data and mock data in the MyQuotes web part, controlled by the condition on the environment. Ideally, I would introduce an abstract data reader, and inject the dependency in the constructor of the web part. This would require an Inversion of Control container, but (a) I am not aware of any IoC library for TypeScript, specifically working with SPFx (feedback welcome here), and (b) it’s an overkill solution for just a web part with one dependency.
I’ll refactor the web part to use a more traditional abstract factory pattern, defined in the constructor. The web part defined a private data reader (IDataReader type) and obtains a concrete instance from the data reader factory (DataReaderFactoryobject) passing the current web part context.
export default class MyQuotesWebPart extends BaseClientSideWebPart<IMyQuotesWebPartProps> { constructor() { super(); this._dataReader = DataReaderFactory.getReader(this.context); } private _dataReader : IDataReader;
The IDataReader interface simply defines a method getData() that returns IQuotes, that is a collection of quotes. And the DataReaderFactory object decides whether to return real data from SharePoint or mock data depending on the environment type.
export interface IDataReader { getData(): Promise<IQuotes>; } export class DataReaderFactory { public static getReader(context: IWebPartContext) { if (Environment.type === EnvironmentType.SharePoint || Environment.type === EnvironmentType.ClassicSharePoint) { return new SPDataReader(context, "Quotes"); } else if (Environment.type === EnvironmentType.Local) { return new MockDataReader(); } } }
This will make the renderData() method in MyQuotesWebPart very simple, without the differentiation on data source depending on the environment.
private renderData(): void { this._dataReader.getData().then((response) => { this.renderQuotes(response.Quotes); }); }
The actual data source is defined in the factory. The SPDataReader class implements the IDataReader interface and, in the implemented getData() method, utilizes the spHttpClient object, obtained by the web part context, to retrieve a list of items from the “Quotes” list. The dependency on the web part context and the name of the custom list is injected via its constructor.
export class SPDataReader implements IDataReader { constructor(context: IWebPartContext, listName: string) { this._context = context; this._listName = listName; }
The MockDataReader object extends SPDataReader, and because sample data is hard-coded in the class itself, there is no need to specific any web part context or list name. Strictly speaking, there is no need to implement MockDataReader as a subtype of SPDataReader. MockDataReader can “easily” implement the IDataReader interface and be used interchangeably with SPDataReader. However, by subtyping MockDataReader from SPDataReader, we can prove the Liskov principle, which states that the functionality of the web part won’t change if an object is replaced with an instance of its subtype.
export class MockDataReader extends SPDataReader { constructor() { super(null, null); }
The entire solution is available on my Github repository for download: https://github.com/stefanotempesta/SharePoint/myquotes-webpart
Reference:
Tempesta, S. (2018). SOLID Web Parts with SharePoint Framework – Microsoft Developer Network. [online] Available at: https://c7solutions.com/2018/02/office-365-retention-policies-and-hybrid-public-folders [Accessed 13 Mar. 2018].